1use lopdf::{
60 dictionary, Dictionary, Document, LoadOptions, Object, ObjectId, Stream, StringFormat,
61};
62use std::cell::Cell;
63use std::collections::{HashMap, HashSet};
64use std::fmt::Write as FmtWrite;
65#[cfg(not(target_arch = "wasm32"))]
66use std::thread;
67#[cfg(not(target_arch = "wasm32"))]
68use std::time::Duration;
69
70thread_local! {
85 static FLATTEN_DEPTH: Cell<u32> = const { Cell::new(0) };
86}
87
88#[cfg(feature = "xfa-js-sandboxed")]
89use crate::dynamic::apply_dynamic_scripts_with_runtime;
90use crate::dynamic::{
91 apply_dynamic_scripts, apply_dynamic_scripts_with_mode, runtime_diag_enabled,
92 DynamicScriptOutcome, FormDomMatchEntry, JsExecutionMode, OutputQuality,
93};
94use crate::error::{Result, XfaError};
95use crate::extract::extract_xfa_from_bytes;
96use crate::flatten_trace;
97use crate::font_bridge::{
98 font_variant_key, pdf_glyph_name_to_unicode, CidFontInfo, EmbeddedFontData, PdfBaseEncoding,
99 PdfSimpleEncoding, PdfSourceFont, ResolvedFont, XfaFontResolver, XfaFontSpec,
100};
101use crate::image_bridge::embed_image;
102use crate::javascript_policy::{self, JavaScriptEntryPoint};
103use crate::merger::FormMerger;
104use crate::render_bridge::{
105 generate_all_overlays, generate_field_values_overlays, unicode_to_winansi, FontMetricsData,
106 PageOverlay, XfaRenderConfig,
107};
108use xfa_dom_resolver::data_dom::DataDom;
109use xfa_layout_engine::form::{DrawContent, FormNodeId, FormNodeStyle, FormTree};
110use xfa_layout_engine::layout::{
111 LayoutContent, LayoutDom, LayoutEngine, LayoutNode, LayoutProfile,
112};
113
114#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
129enum PipelineStage {
130 Extract = 0,
131 Bind = 1,
132 Layout = 2,
133 Render = 3,
134 Embed = 4,
135 Write = 5,
136 Cleanup = 6,
137}
138
139fn create_minimal_pdf_document() -> Document {
140 let mut doc = Document::new();
141 let pages_id = doc.add_object(Object::Dictionary(dictionary! {
142 "Type" => Object::Name(b"Pages".to_vec()),
143 "Kids" => Object::Array(vec![]),
144 "Count" => Object::Integer(0)
145 }));
146 let catalog_id = doc.add_object(Object::Dictionary(dictionary! {
147 "Type" => Object::Name(b"Catalog".to_vec()),
148 "Pages" => Object::Reference(pages_id)
149 }));
150 doc.trailer.set("Root", Object::Reference(catalog_id));
151 doc
152}
153
154#[derive(Debug, Clone, Default)]
156pub struct LayoutDump {
157 pub pages: Vec<LayoutDumpEntry>,
159 pub dynamic_scripts: DynamicScriptOutcome,
161 pub output_quality: OutputQuality,
163}
164
165#[derive(Debug, Clone)]
167pub struct LayoutDumpEntry {
168 pub page_num: u32,
170 pub page_height: f64,
172 pub used_height: f64,
174 pub overflow_to_next: bool,
176 pub first_overflow_element: Option<String>,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
197pub enum XfaRenderingPolicy {
198 #[default]
202 SavedStateFaithful,
203 FreshMergeExperimental,
211}
212
213impl XfaRenderingPolicy {
214 #[must_use]
217 pub const fn as_str(self) -> &'static str {
218 match self {
219 Self::SavedStateFaithful => "saved_state_faithful",
220 Self::FreshMergeExperimental => "fresh_merge_experimental",
221 }
222 }
223
224 #[must_use]
227 pub fn from_token(token: &str) -> Option<Self> {
228 match token.trim().to_ascii_lowercase().as_str() {
229 "saved-state" | "saved_state" | "saved_state_faithful" | "savedstatefaithful" => {
230 Some(Self::SavedStateFaithful)
231 }
232 "fresh-merge"
233 | "fresh_merge"
234 | "fresh_merge_experimental"
235 | "freshmergeexperimental" => Some(Self::FreshMergeExperimental),
236 _ => None,
237 }
238 }
239
240 #[must_use]
245 pub const fn is_supported(self) -> bool {
246 true
247 }
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Default)]
252pub struct FlattenMetadata {
253 pub dynamic_scripts: DynamicScriptOutcome,
255 pub output_quality: OutputQuality,
257 pub rendering_policy: XfaRenderingPolicy,
259 pub fresh_merge_admitted_nodes: usize,
271}
272
273impl FlattenMetadata {
274 fn from_dynamic_scripts(dynamic_scripts: DynamicScriptOutcome) -> Self {
275 let output_quality = dynamic_scripts.output_quality;
276 Self {
277 dynamic_scripts,
278 output_quality,
279 rendering_policy: XfaRenderingPolicy::SavedStateFaithful,
280 fresh_merge_admitted_nodes: 0,
281 }
282 }
283}
284
285struct FlattenOutput {
286 pdf_bytes: Vec<u8>,
287 layout_dump: LayoutDump,
288 metadata: FlattenMetadata,
289}
290
291impl FlattenOutput {
292 fn new(
293 pdf_bytes: Vec<u8>,
294 mut layout_dump: LayoutDump,
295 dynamic_scripts: DynamicScriptOutcome,
296 ) -> Self {
297 let output_quality = dynamic_scripts.output_quality;
298 let metadata = FlattenMetadata::from_dynamic_scripts(dynamic_scripts.clone());
299 layout_dump.dynamic_scripts = dynamic_scripts;
300 layout_dump.output_quality = output_quality;
301 Self {
302 pdf_bytes,
303 layout_dump,
304 metadata,
305 }
306 }
307
308 fn without_dump(pdf_bytes: Vec<u8>) -> Self {
309 Self::new(
310 pdf_bytes,
311 LayoutDump::default(),
312 DynamicScriptOutcome::default(),
313 )
314 }
315}
316
317pub fn is_pdf_encrypted(pdf_bytes: &[u8]) -> bool {
319 Document::load_mem(pdf_bytes)
320 .map(|doc| doc.trailer.get(b"Encrypt").is_ok())
321 .unwrap_or(false)
322}
323
324enum DecryptResult {
325 NotEncrypted,
326 Decrypted(Vec<u8>),
327 NeedsPassword,
328}
329
330fn try_decrypt_pdf(pdf_bytes: &[u8], password: Option<&str>) -> DecryptResult {
344 let mut doc = match Document::load_mem(pdf_bytes) {
345 Ok(d) => d,
346 Err(_) => return DecryptResult::NotEncrypted, };
348
349 if doc.was_encrypted() {
353 let mut buf = Vec::new();
355 match doc.save_to(&mut buf) {
356 Ok(()) => return DecryptResult::Decrypted(buf),
357 Err(_) => return DecryptResult::NeedsPassword,
358 }
359 }
360
361 if doc.trailer.get(b"Encrypt").is_ok() {
362 if let Some(pw) = password.filter(|p| !p.is_empty()) {
364 if let Ok(mut decrypted) =
365 Document::load_mem_with_options(pdf_bytes, LoadOptions::with_password(pw))
366 {
367 decrypted.trailer.remove(b"Encrypt");
368 let mut buf = Vec::new();
369 if decrypted.save_to(&mut buf).is_ok() {
370 return DecryptResult::Decrypted(buf);
371 }
372 return DecryptResult::NeedsPassword;
373 }
374 }
375 match Document::load_mem_with_options(pdf_bytes, LoadOptions::with_password("")) {
377 Ok(mut decrypted_doc) => {
378 decrypted_doc.trailer.remove(b"Encrypt");
379 let mut buf = Vec::new();
380 match decrypted_doc.save_to(&mut buf) {
381 Ok(()) => return DecryptResult::Decrypted(buf),
382 Err(_) => return DecryptResult::NeedsPassword,
383 }
384 }
385 Err(_) => return DecryptResult::NeedsPassword,
386 }
387 }
388
389 DecryptResult::NotEncrypted
390}
391
392fn page_has_fields(nodes: &[LayoutNode], tree: &FormTree) -> bool {
406 use xfa_layout_engine::form::{FieldKind, FormNodeType};
407 nodes.iter().any(|n| {
408 let is_data_field = matches!(tree.get(n.form_node).node_type, FormNodeType::Field { .. })
411 && !matches!(
412 tree.meta(n.form_node).field_kind,
413 FieldKind::Signature | FieldKind::Button | FieldKind::Barcode
414 );
415 is_data_field || page_has_fields(&n.children, tree)
416 })
417}
418
419fn page_has_field_data(nodes: &[LayoutNode], tree: &FormTree) -> bool {
424 use xfa_layout_engine::form::FormNodeType;
425 nodes.iter().any(|n| {
426 matches!(
427 &tree.get(n.form_node).node_type,
428 FormNodeType::Field { value } if !value.is_empty()
429 ) || page_has_field_data(&n.children, tree)
430 })
431}
432
433fn page_field_counts(nodes: &[LayoutNode], tree: &FormTree) -> (usize, usize, usize) {
435 use xfa_layout_engine::form::FormNodeType;
436 let mut total = 0;
437 let mut empty = 0;
438 let mut nonempty = 0;
439 for n in nodes {
440 if let FormNodeType::Field { value } = &tree.get(n.form_node).node_type {
441 total += 1;
442 if value.trim().is_empty() {
443 empty += 1;
444 } else {
445 nonempty += 1;
446 }
447 }
448 let (t, e, ne) = page_field_counts(&n.children, tree);
449 total += t;
450 empty += e;
451 nonempty += ne;
452 }
453 (total, empty, nonempty)
454}
455
456fn page_static_draw_chars(nodes: &[LayoutNode]) -> usize {
459 let mut total = 0usize;
460 for n in nodes {
461 match &n.content {
462 LayoutContent::Text(t) => total += t.chars().count(),
463 LayoutContent::Draw(DrawContent::Text(t)) => total += t.chars().count(),
464 LayoutContent::WrappedText {
465 lines, from_field, ..
466 } if !*from_field => {
467 total += lines.iter().map(|l| l.chars().count()).sum::<usize>();
468 }
469 _ => {}
470 }
471 total += page_static_draw_chars(&n.children);
472 }
473 total
474}
475
476fn page_has_visible_content(nodes: &[LayoutNode]) -> bool {
487 nodes.iter().any(|n| {
488 let self_visible = match &n.content {
489 LayoutContent::None => false,
490 LayoutContent::Text(t) => !t.trim().is_empty(),
491 LayoutContent::WrappedText { lines, .. } => lines.iter().any(|l| !l.trim().is_empty()),
492 LayoutContent::Draw(DrawContent::Text(t)) => !t.trim().is_empty(),
493 LayoutContent::Draw(_) => true,
495 LayoutContent::Field { .. } => true,
498 LayoutContent::Image { .. } => true,
499 };
500 self_visible || page_has_visible_content(&n.children)
501 })
502}
503
504fn suppression_trust_layout_enabled() -> bool {
515 matches!(std::env::var("XFA_SUPPRESSION_TRUST_LAYOUT"), Ok(v) if {
516 let v = v.trim();
517 v == "1" || v.eq_ignore_ascii_case("on") || v.eq_ignore_ascii_case("true")
518 })
519}
520
521fn harvest_mode_enabled() -> bool {
531 matches!(std::env::var("XFA_JS_HARVEST_MODE"), Ok(v) if {
532 let v = v.trim();
533 v == "1" || v.eq_ignore_ascii_case("on") || v.eq_ignore_ascii_case("true")
534 })
535}
536
537fn snapshot_nonempty_field_ids(tree: &FormTree) -> HashSet<FormNodeId> {
542 use xfa_layout_engine::form::FormNodeType;
543 let mut ids = HashSet::new();
544 for i in 0..tree.nodes.len() {
545 let id = FormNodeId(i);
546 if let FormNodeType::Field { value } = &tree.get(id).node_type {
547 if !value.trim().is_empty() {
548 ids.insert(id);
549 }
550 }
551 }
552 ids
553}
554
555fn page_has_field_data_snapshot(nodes: &[LayoutNode], snapshot: &HashSet<FormNodeId>) -> bool {
558 nodes.iter().any(|n| {
559 snapshot.contains(&n.form_node) || page_has_field_data_snapshot(&n.children, snapshot)
560 })
561}
562
563fn page_form_node_signature(nodes: &[LayoutNode], out: &mut Vec<usize>) {
567 for n in nodes {
568 out.push(n.form_node.0);
569 page_form_node_signature(&n.children, out);
570 }
571}
572
573fn build_parent_map(tree: &FormTree) -> Vec<usize> {
578 let mut parent = vec![usize::MAX; tree.nodes.len()];
579 for (pid, node) in tree.nodes.iter().enumerate() {
580 for &child in &node.children {
581 if child.0 < parent.len() {
582 parent[child.0] = pid;
583 }
584 }
585 }
586 parent
587}
588
589fn page_data_bound_count(nodes: &[LayoutNode], tree: &FormTree) -> usize {
591 let mut c = 0;
592 for n in nodes {
593 if tree.meta(n.form_node).bound_data_node.is_some() {
594 c += 1;
595 }
596 c += page_data_bound_count(&n.children, tree);
597 }
598 c
599}
600
601fn page_repeating_ancestor(
604 distinct_ids: &[usize],
605 tree: &FormTree,
606 parent_map: &[usize],
607) -> Option<usize> {
608 use xfa_layout_engine::form::FormNodeId;
609 for &start in distinct_ids {
610 let mut cur = start;
611 let mut depth = 0;
612 while cur != usize::MAX && depth < 4096 {
613 if cur < tree.nodes.len() && tree.get(FormNodeId(cur)).occur.is_repeating() {
614 return Some(cur);
615 }
616 cur = parent_map.get(cur).copied().unwrap_or(usize::MAX);
617 depth += 1;
618 }
619 }
620 None
621}
622
623fn compute_suppression_diags(
624 layout: &LayoutDom,
625 tree: &FormTree,
626 pre_js_nonempty: Option<&HashSet<FormNodeId>>,
627) -> Vec<flatten_trace::PageSuppressionDiag> {
628 let parent_map = build_parent_map(tree);
629 let n = layout.pages.len();
630 let trust_layout = suppression_trust_layout_enabled();
631 let raw: Vec<(bool, bool, bool)> = layout
635 .pages
636 .iter()
637 .map(|p| {
638 let hd = match pre_js_nonempty {
639 Some(snap) => page_has_field_data_snapshot(&p.nodes, snap),
640 None => page_has_field_data(&p.nodes, tree),
641 };
642 (p.runtime_instantiated, page_has_fields(&p.nodes, tree), hd)
643 })
644 .collect();
645 let raw_keep = |i: usize| -> bool {
646 let (rt, hf, hd) = raw[i];
650 rt || hd || !hf
651 };
652 let any_keep = (0..n).any(raw_keep);
653
654 let mut sigs: Vec<Vec<usize>> = Vec::with_capacity(n);
656 for p in &layout.pages {
657 let mut s = Vec::new();
658 page_form_node_signature(&p.nodes, &mut s);
659 s.sort_unstable();
660 s.dedup();
661 sigs.push(s);
662 }
663
664 let mut diags = Vec::with_capacity(n);
665 for i in 0..n {
666 let (rt, hf, hd) = raw[i];
667 let (fc, ef, nf) = page_field_counts(&layout.pages[i].nodes, tree);
668 let static_chars = page_static_draw_chars(&layout.pages[i].nodes);
669 let dup = (0..i)
670 .find(|&j| sigs[j] == sigs[i])
671 .map_or(-1, |j| j as i64);
672 let (keep, reason) = if n <= 1 {
673 (true, "single_page")
674 } else if rt {
675 (true, "runtime_instantiated")
676 } else if hf && hd {
677 (true, "has_field_data")
678 } else if !hf {
679 (true, "no_fields_static_kept")
680 } else if any_keep {
681 if trust_layout && page_has_visible_content(&layout.pages[i].nodes) {
684 (true, "trust_layout_kept")
685 } else {
686 (false, "data_empty_dropped")
687 }
688 } else {
689 (true, "all_empty_kept")
690 };
691
692 let data_bound = page_data_bound_count(&layout.pages[i].nodes, tree);
694 let repeating_ancestor = page_repeating_ancestor(&sigs[i], tree, &parent_map);
695 let under_repeating = repeating_ancestor.is_some();
696 let occur_template_id = repeating_ancestor.map_or(-1, |id| id as i64);
697 let has_data = nf > 0 || data_bound > 0;
698 let page_reason = if rt {
699 "root_page"
700 } else if under_repeating && !has_data {
701 "repeated_empty_instance"
702 } else if under_repeating {
703 "occur_instance"
704 } else if has_data {
705 "continuation"
706 } else if static_chars > 0 {
707 "static_page_area"
708 } else {
709 "unknown"
710 };
711 let suppression_safe_to_drop = page_reason == "repeated_empty_instance";
712 let provenance_confidence = if rt || under_repeating || has_data {
713 "exact"
714 } else if static_chars > 0 {
715 "inferred"
716 } else {
717 "unknown"
718 };
719
720 diags.push(flatten_trace::PageSuppressionDiag {
721 page_index: i,
722 keep,
723 reason,
724 field_count: fc,
725 empty_field_count: ef,
726 nonempty_field_count: nf,
727 static_draw_text_chars: static_chars,
728 distinct_form_nodes: sigs[i].len(),
729 duplicate_of_page: dup,
730 runtime_instantiated: rt,
731 under_repeating_subform: under_repeating,
732 occur_template_id,
733 data_bound_nodes_count: data_bound,
734 page_reason,
735 suppression_safe_to_drop,
736 provenance_confidence,
737 });
738 }
739 diags
740}
741
742#[must_use = "flattened PDF bytes must be used; discarding them loses output"]
785pub fn flatten_xfa_to_pdf(pdf_bytes: &[u8]) -> Result<Vec<u8>> {
786 flatten_xfa_to_pdf_internal(
787 pdf_bytes,
788 false,
789 XfaRenderingPolicy::SavedStateFaithful,
790 None,
791 )
792 .map(|out| out.pdf_bytes)
793}
794#[must_use = "flattened PDF bytes and layout dump must be used; discarding them loses output"]
803pub fn flatten_xfa_to_pdf_with_layout_dump(pdf_bytes: &[u8]) -> Result<(Vec<u8>, LayoutDump)> {
804 let out = flatten_xfa_to_pdf_internal(
805 pdf_bytes,
806 true,
807 XfaRenderingPolicy::SavedStateFaithful,
808 None,
809 )?;
810 Ok((out.pdf_bytes, out.layout_dump))
811}
812
813#[must_use = "flattened PDF bytes and metadata must be used; discarding them loses output"]
821pub fn flatten_xfa_to_pdf_with_metadata(pdf_bytes: &[u8]) -> Result<(Vec<u8>, FlattenMetadata)> {
822 let out = flatten_xfa_to_pdf_internal(
823 pdf_bytes,
824 false,
825 XfaRenderingPolicy::SavedStateFaithful,
826 None,
827 )?;
828 Ok((out.pdf_bytes, out.metadata))
829}
830
831#[must_use = "flattened PDF bytes, layout dump, and metadata must be used; discarding them loses output"]
840pub fn flatten_xfa_to_pdf_with_layout_dump_and_metadata(
841 pdf_bytes: &[u8],
842) -> Result<(Vec<u8>, LayoutDump, FlattenMetadata)> {
843 let out = flatten_xfa_to_pdf_internal(
844 pdf_bytes,
845 true,
846 XfaRenderingPolicy::SavedStateFaithful,
847 None,
848 )?;
849 Ok((out.pdf_bytes, out.layout_dump, out.metadata))
850}
851
852#[must_use = "flattened PDF bytes must be used; discarding them loses output"]
863pub fn flatten_xfa_to_pdf_with_policy(
864 pdf_bytes: &[u8],
865 policy: XfaRenderingPolicy,
866) -> Result<Vec<u8>> {
867 flatten_xfa_to_pdf_with_policy_and_metadata(pdf_bytes, policy).map(|(bytes, _)| bytes)
868}
869
870#[must_use = "flattened PDF bytes and metadata must be used; discarding them loses output"]
879pub fn flatten_xfa_to_pdf_with_policy_and_metadata(
880 pdf_bytes: &[u8],
881 policy: XfaRenderingPolicy,
882) -> Result<(Vec<u8>, FlattenMetadata)> {
883 let out = flatten_xfa_to_pdf_internal(pdf_bytes, false, policy, None)?;
885 let mut metadata = out.metadata;
886 metadata.rendering_policy = policy;
887 Ok((out.pdf_bytes, metadata))
888}
889
890#[must_use = "flattened PDF bytes must be used; discarding them loses output"]
905pub fn flatten_xfa_to_pdf_with_password(pdf_bytes: &[u8], password: &str) -> Result<Vec<u8>> {
906 flatten_xfa_to_pdf_internal(
907 pdf_bytes,
908 false,
909 XfaRenderingPolicy::SavedStateFaithful,
910 Some(password),
911 )
912 .map(|out| out.pdf_bytes)
913}
914
915fn flatten_xfa_to_pdf_internal(
916 pdf_bytes: &[u8],
917 collect_layout_dump: bool,
918 policy: XfaRenderingPolicy,
919 password: Option<&str>,
920) -> Result<FlattenOutput> {
921 let depth = FLATTEN_DEPTH.with(|d| d.get());
930 if depth >= 1 {
931 return Err(XfaError::LayoutFailed(
932 "flatten_xfa_to_pdf called recursively — aborting to prevent stack overflow".into(),
933 ));
934 }
935 FLATTEN_DEPTH.with(|d| d.set(depth + 1));
936 struct DepthGuard;
938 impl Drop for DepthGuard {
939 fn drop(&mut self) {
940 FLATTEN_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
941 }
942 }
943 let _depth_guard = DepthGuard;
944
945 if !pdf_bytes.windows(9).any(|w| w == b"/AcroForm")
949 && !pdf_bytes.windows(7).any(|w| w == b"xdp:xdp")
950 {
951 return Ok(FlattenOutput::without_dump(pdf_bytes.to_vec()));
952 }
953
954 let decrypted;
957 let pdf_bytes = match try_decrypt_pdf(pdf_bytes, password) {
958 DecryptResult::NotEncrypted => pdf_bytes,
959 DecryptResult::Decrypted(bytes) => {
960 decrypted = bytes;
961 &decrypted
962 }
963 DecryptResult::NeedsPassword => {
964 return Err(XfaError::Encrypted(
965 "PDF is encrypted and requires a password".into(),
966 ));
967 }
968 };
969
970 let packets = match extract_xfa_from_bytes(pdf_bytes.to_vec()) {
972 Ok(p) => p,
973 Err(_) => {
974 return static_fallback(pdf_bytes).map(FlattenOutput::without_dump);
978 }
979 };
980
981 let template_xml = match packets.template() {
982 Some(t) => strip_undefined_xml_entities(t),
983 None => {
984 return static_fallback(pdf_bytes).map(FlattenOutput::without_dump);
987 }
988 };
989
990 if is_corrupt_xfa_template(pdf_bytes.len(), &template_xml) {
994 return static_fallback(pdf_bytes).map(FlattenOutput::without_dump);
995 }
996
997 let datasets_xml_owned = packets.datasets().map(strip_undefined_xml_entities);
1005 let form_xml_owned = packets.get_packet("form").map(|s| s.to_string());
1006
1007 #[cfg(not(target_arch = "wasm32"))]
1008 {
1009 const FLATTEN_TIMEOUT: Duration = Duration::from_secs(30);
1013 let pdf_bytes_ref = pdf_bytes.to_vec();
1014 let template_xml_owned = template_xml.clone();
1015
1016 const FLATTEN_STACK: usize = 32 * 1024 * 1024;
1027 let handle = thread::Builder::new()
1028 .stack_size(FLATTEN_STACK)
1029 .spawn(move || {
1030 xfa_flatten_inner(
1031 &pdf_bytes_ref,
1032 &template_xml_owned,
1033 datasets_xml_owned.as_deref(),
1034 form_xml_owned.as_deref(),
1035 collect_layout_dump,
1036 policy,
1037 )
1038 })
1039 .map_err(XfaError::Io)?;
1040
1041 match handle.join() {
1042 Ok(Ok(out)) => Ok(out),
1043 Ok(Err(e @ XfaError::UnsupportedFeature(_))) => Err(e),
1044 Ok(Err(e)) => {
1045 eprintln!("XFA flatten failed: {e:?}");
1046 static_fallback(pdf_bytes).map(FlattenOutput::without_dump)
1047 }
1048 Err(_) => {
1049 eprintln!("XFA flatten timed out after {:?}", FLATTEN_TIMEOUT);
1050 static_fallback(pdf_bytes).map(FlattenOutput::without_dump)
1051 }
1052 }
1053 }
1054
1055 #[cfg(target_arch = "wasm32")]
1059 {
1060 match xfa_flatten_inner(
1061 pdf_bytes,
1062 &template_xml,
1063 datasets_xml_owned.as_deref(),
1064 form_xml_owned.as_deref(),
1065 collect_layout_dump,
1066 policy,
1067 ) {
1068 Ok(out) => Ok(out),
1069 Err(e @ XfaError::UnsupportedFeature(_)) => Err(e),
1070 Err(e) => {
1071 eprintln!("XFA flatten failed: {e:?}");
1072 static_fallback(pdf_bytes).map(FlattenOutput::without_dump)
1073 }
1074 }
1075 }
1076}
1077
1078#[cfg(feature = "xfa-js-sandboxed")]
1085fn collect_declared_container_names(template_xml: &str) -> std::collections::HashSet<String> {
1086 let mut names = std::collections::HashSet::new();
1087 if let Ok(doc) = roxmltree::Document::parse(template_xml) {
1088 for node in doc.descendants() {
1089 if node.is_element()
1090 && matches!(
1091 node.tag_name().name(),
1092 "subform" | "subformSet" | "exclGroup" | "area"
1093 )
1094 {
1095 if let Some(name) = node.attribute("name") {
1096 if !name.is_empty() {
1097 names.insert(name.to_string());
1098 }
1099 }
1100 }
1101 }
1102 }
1103 names
1104}
1105
1106fn xfa_flatten_inner(
1108 pdf_bytes: &[u8],
1109 template_xml: &str,
1110 datasets_xml: Option<&str>,
1111 form_xml: Option<&str>,
1112 collect_layout_dump: bool,
1113 policy: XfaRenderingPolicy,
1114) -> Result<FlattenOutput> {
1115 let mut _stage = PipelineStage::Extract;
1118
1119 log::debug!(
1121 "XFA flatten: {} bytes input, template={} bytes",
1122 pdf_bytes.len(),
1123 template_xml.len()
1124 );
1125
1126 let data_dom = if let Some(ds_xml) = datasets_xml {
1127 DataDom::from_xml(ds_xml)
1128 .map_err(|e| XfaError::ParseFailed(format!("datasets parse: {e}")))?
1129 } else {
1130 DataDom::new()
1131 };
1132
1133 let image_files = match Document::load_mem(pdf_bytes) {
1136 Ok(doc) => extract_embedded_images(&doc),
1137 Err(_) => HashMap::new(),
1138 };
1139
1140 if template_xml.contains("barcode") {
1144 log::warn!("XFA barcode elements found but not supported — rendered as empty boxes");
1145 }
1146 if template_xml.contains("<signature") || template_xml.contains("<Signature") {
1147 log::warn!("XFA signature elements found but not supported — elements skipped");
1148 }
1149 if javascript_policy::template_mentions_javascript(template_xml) {
1150 log::warn!(
1151 "{}",
1152 javascript_policy::execution_denied_message(JavaScriptEntryPoint::XfaEventHook)
1153 );
1154 }
1155
1156 debug_assert!(
1158 _stage <= PipelineStage::Bind,
1159 "pipeline stage order violated: expected <= Bind"
1160 );
1161 _stage = PipelineStage::Bind;
1162
1163 let trace_image_files = image_files.len();
1165 let merger = FormMerger::new(&data_dom).with_image_files(image_files);
1166 let (mut tree, root_id) = merger
1167 .merge(template_xml)
1168 .map_err(|e| XfaError::ParseFailed(format!("template merge: {e}")))?;
1169
1170 log::debug!("XFA bind: {} form nodes created", tree.nodes.len());
1171
1172 let pre_js_nonempty_fields: Option<HashSet<FormNodeId>> = if harvest_mode_enabled() {
1188 Some(snapshot_nonempty_field_ids(&tree))
1189 } else {
1190 None
1191 };
1192 let dynamic_scripts = match std::env::var("XFA_JS_EXECUTION_MODE")
1193 .ok()
1194 .map(|s| s.to_ascii_lowercase())
1195 .as_deref()
1196 {
1197 Some("strict") => {
1198 apply_dynamic_scripts_with_mode(&mut tree, root_id, JsExecutionMode::Strict)?
1199 }
1200 Some("sandboxed") | Some("sandboxed_runtime") => {
1201 #[cfg(feature = "xfa-js-sandboxed")]
1205 {
1206 use crate::js_runtime::{NullRuntime, QuickJsRuntime, XfaJsRuntime};
1207 match QuickJsRuntime::new() {
1208 Ok(mut rt) => {
1209 rt.set_data_handle(&data_dom as *const _);
1210 rt.set_declared_subform_names(collect_declared_container_names(
1216 template_xml,
1217 ));
1218 apply_dynamic_scripts_with_runtime(
1219 &mut tree,
1220 root_id,
1221 JsExecutionMode::SandboxedRuntime,
1222 &mut rt,
1223 )?
1224 }
1225 Err(_) => apply_dynamic_scripts_with_runtime(
1226 &mut tree,
1227 root_id,
1228 JsExecutionMode::SandboxedRuntime,
1229 &mut NullRuntime::new(),
1230 )?,
1231 }
1232 }
1233 #[cfg(not(feature = "xfa-js-sandboxed"))]
1234 apply_dynamic_scripts_with_mode(&mut tree, root_id, JsExecutionMode::SandboxedRuntime)?
1235 }
1236 _ => apply_dynamic_scripts(&mut tree, root_id)?,
1237 };
1238 if dynamic_scripts.output_quality != OutputQuality::Exact {
1239 log::warn!(
1243 "XFA script metadata: output_quality={} js_present={} js_skipped={} other_skipped={} formcalc_run={} formcalc_errors={} js_executed={} js_runtime_errors={} js_timeouts={} js_oom={} js_host_calls={} js_mutations={} js_instance_writes={} js_list_writes={} js_binding_errors={} js_resolve_failures={} js_data_reads={} js_unsupported_host_calls={} js_probe_skips={}",
1244 dynamic_scripts.output_quality.as_str(),
1245 dynamic_scripts.js_present,
1246 dynamic_scripts.js_skipped,
1247 dynamic_scripts.other_skipped,
1248 dynamic_scripts.formcalc_run,
1249 dynamic_scripts.formcalc_errors,
1250 dynamic_scripts.js_executed,
1251 dynamic_scripts.js_runtime_errors,
1252 dynamic_scripts.js_timeouts,
1253 dynamic_scripts.js_oom,
1254 dynamic_scripts.js_host_calls,
1255 dynamic_scripts.js_mutations,
1256 dynamic_scripts.js_instance_writes,
1257 dynamic_scripts.js_list_writes,
1258 dynamic_scripts.js_binding_errors,
1259 dynamic_scripts.js_resolve_failures,
1260 dynamic_scripts.js_data_reads,
1261 dynamic_scripts.js_unsupported_host_calls,
1262 dynamic_scripts.js_probe_skips,
1263 );
1264 eprintln!(
1265 "XFA script metadata: output_quality={} js_present={} js_skipped={} other_skipped={} formcalc_run={} formcalc_errors={} js_executed={} js_runtime_errors={} js_timeouts={} js_oom={} js_host_calls={} js_mutations={} js_instance_writes={} js_list_writes={} js_binding_errors={} js_resolve_failures={} js_data_reads={} js_unsupported_host_calls={} js_probe_skips={}",
1266 dynamic_scripts.output_quality.as_str(),
1267 dynamic_scripts.js_present,
1268 dynamic_scripts.js_skipped,
1269 dynamic_scripts.other_skipped,
1270 dynamic_scripts.formcalc_run,
1271 dynamic_scripts.formcalc_errors,
1272 dynamic_scripts.js_executed,
1273 dynamic_scripts.js_runtime_errors,
1274 dynamic_scripts.js_timeouts,
1275 dynamic_scripts.js_oom,
1276 dynamic_scripts.js_host_calls,
1277 dynamic_scripts.js_mutations,
1278 dynamic_scripts.js_instance_writes,
1279 dynamic_scripts.js_list_writes,
1280 dynamic_scripts.js_binding_errors,
1281 dynamic_scripts.js_resolve_failures,
1282 dynamic_scripts.js_data_reads,
1283 dynamic_scripts.js_unsupported_host_calls,
1284 dynamic_scripts.js_probe_skips,
1285 );
1286 }
1287
1288 let admit_databound_override = std::env::var("XFA_FORMDOM_ADMIT_DATABOUND")
1301 .map(|v| {
1302 let v = v.trim();
1303 !(v.is_empty()
1304 || v == "0"
1305 || v.eq_ignore_ascii_case("off")
1306 || v.eq_ignore_ascii_case("false"))
1307 })
1308 .unwrap_or(true);
1309 let (fresh_merge_admitted, form_dom_match_failures, form_dom_match_log) =
1310 if let Some(fxml) = form_xml {
1311 apply_form_dom_presence(&mut tree, root_id, fxml, policy, admit_databound_override)
1312 } else {
1313 (0, 0, Vec::new())
1314 };
1315
1316 let resolved_fonts = resolve_template_fonts(template_xml, pdf_bytes);
1319 inject_resolved_metrics(&mut tree, &resolved_fonts);
1320
1321 debug_assert!(
1323 _stage <= PipelineStage::Layout,
1324 "pipeline stage order violated: expected <= Layout"
1325 );
1326 _stage = PipelineStage::Layout;
1327
1328 let engine = LayoutEngine::new(&tree);
1329 let (mut layout, mut layout_dump) = if collect_layout_dump {
1330 let (layout, profile) = engine
1331 .layout_with_profile(root_id)
1332 .map_err(|e| XfaError::LayoutFailed(format!("{e:?}")))?;
1333 (layout, Some(layout_dump_from_profile(profile)))
1334 } else {
1335 let layout = engine
1336 .layout(root_id)
1337 .map_err(|e| XfaError::LayoutFailed(format!("{e:?}")))?;
1338 (layout, None)
1339 };
1340
1341 if layout.pages.is_empty() {
1342 return Err(XfaError::LayoutFailed("layout produced 0 pages".into()));
1343 }
1344
1345 log::debug!("XFA layout: {} pages produced", layout.pages.len());
1346
1347 let trace_pages_produced = layout.pages.len();
1359 let trace_suppression = if flatten_trace::enabled() {
1362 compute_suppression_diags(&layout, &tree, pre_js_nonempty_fields.as_ref())
1363 } else {
1364 Vec::new()
1365 };
1366 let trust_layout = suppression_trust_layout_enabled();
1370 if layout.pages.len() > 1 {
1371 let keep: Vec<bool> = layout
1372 .pages
1373 .iter()
1374 .map(|p| {
1375 if p.runtime_instantiated {
1380 true
1381 } else if page_has_fields(&p.nodes, &tree) {
1382 let has_field_data = match pre_js_nonempty_fields {
1393 Some(ref snap) => page_has_field_data_snapshot(&p.nodes, snap),
1394 None => page_has_field_data(&p.nodes, &tree),
1395 };
1396 has_field_data || (trust_layout && page_has_visible_content(&p.nodes))
1397 } else {
1398 true
1399 }
1400 })
1401 .collect();
1402 let any_keep = keep.iter().any(|&k| k);
1403 if any_keep {
1404 let mut idx = 0;
1405 layout.pages.retain(|_| {
1406 let k = keep[idx];
1407 idx += 1;
1408 k
1409 });
1410 if let Some(ref mut dump) = layout_dump {
1411 let mut idx = 0;
1412 dump.pages.retain(|_| {
1413 let k = keep[idx];
1414 idx += 1;
1415 k
1416 });
1417 }
1418 }
1419 }
1423
1424 if let Some(ref mut dump) = layout_dump {
1425 renumber_layout_dump_pages(dump);
1426 }
1427
1428 debug_assert!(
1430 _stage <= PipelineStage::Render,
1431 "pipeline stage order violated: expected <= Render"
1432 );
1433 _stage = PipelineStage::Render;
1434
1435 let mut doc = match Document::load_mem(pdf_bytes) {
1436 Ok(d) => d,
1437 Err(_) => {
1438 eprintln!("lopdf load failed, creating minimal PDF structure for XFA layout");
1439 create_minimal_pdf_document()
1440 }
1441 };
1442
1443 debug_assert!(
1445 _stage <= PipelineStage::Embed,
1446 "pipeline stage order violated: expected <= Embed"
1447 );
1448 _stage = PipelineStage::Embed;
1449
1450 let (font_map, embedded_font_objects, metrics_data) =
1457 embed_resolved_fonts(&mut doc, &resolved_fonts, &layout);
1458
1459 let config = XfaRenderConfig {
1460 font_map: std::sync::Arc::new(font_map),
1461 font_metrics_data: std::sync::Arc::new(metrics_data),
1462 ..Default::default()
1463 };
1464
1465 let overlays = generate_all_overlays(&layout, &config)
1466 .map_err(|e| XfaError::LayoutFailed(format!("overlay generation: {e:?}")))?;
1467
1468 log::debug!(
1469 "XFA render: {} content streams generated ({} bytes total)",
1470 overlays.len(),
1471 overlays
1472 .iter()
1473 .map(|o| o.content_stream.len())
1474 .sum::<usize>()
1475 );
1476
1477 let font_ids: [ObjectId; 3] = [
1479 doc.add_object(Object::Dictionary(dictionary! {
1480 "Type" => Object::Name(b"Font".to_vec()),
1481 "Subtype" => Object::Name(b"Type1".to_vec()),
1482 "BaseFont" => Object::Name(b"Times-Roman".to_vec()),
1483 "Encoding" => Object::Name(b"WinAnsiEncoding".to_vec())
1484 })),
1485 doc.add_object(Object::Dictionary(dictionary! {
1486 "Type" => Object::Name(b"Font".to_vec()),
1487 "Subtype" => Object::Name(b"Type1".to_vec()),
1488 "BaseFont" => Object::Name(b"Helvetica".to_vec()),
1489 "Encoding" => Object::Name(b"WinAnsiEncoding".to_vec())
1490 })),
1491 doc.add_object(Object::Dictionary(dictionary! {
1492 "Type" => Object::Name(b"Font".to_vec()),
1493 "Subtype" => Object::Name(b"Type1".to_vec()),
1494 "BaseFont" => Object::Name(b"Courier".to_vec()),
1495 "Encoding" => Object::Name(b"WinAnsiEncoding".to_vec())
1496 })),
1497 ];
1498
1499 let existing_page_ids: Vec<ObjectId> = doc.page_iter().collect();
1500 let n_layout = overlays.len();
1501 let n_existing = existing_page_ids.len();
1502
1503 let is_static_form = template_xml.contains("baseProfile=\"interactiveForms\"");
1516 let has_static_content = pages_have_static_content(&doc);
1517
1518 let overlay_is_substantial = overlays.iter().any(|o| o.content_stream.len() > 1000);
1542 let preserve_static =
1549 is_static_form || n_layout < n_existing || has_static_content && overlay_is_substantial;
1550
1551 debug_assert!(
1553 _stage <= PipelineStage::Write,
1554 "pipeline stage order violated: expected <= Write"
1555 );
1556 _stage = PipelineStage::Write;
1557
1558 let mut trace_widgets_baked = 0usize;
1559 let mut trace_excess_deleted = 0usize;
1560 if preserve_static {
1561 let baked = flatten_widget_appearances(&mut doc);
1562 trace_widgets_baked = baked;
1563 if baked == 0 {
1564 if let Ok(fv_overlays) = generate_field_values_overlays(&layout, &config) {
1570 for (i, overlay) in fv_overlays.iter().enumerate() {
1571 if i < n_existing && !overlay.content_stream.is_empty() {
1572 let _ = overlay_page_content(
1573 &mut doc,
1574 existing_page_ids[i],
1575 overlay,
1576 &font_ids,
1577 &embedded_font_objects,
1578 );
1579 }
1580 }
1581 }
1582 }
1583 } else {
1588 for (i, overlay) in overlays.iter().enumerate() {
1596 if i < n_existing {
1597 let lp = &layout.pages[i];
1598 write_page_content(
1599 &mut doc,
1600 existing_page_ids[i],
1601 overlay,
1602 &font_ids,
1603 &embedded_font_objects,
1604 Some(lp.width),
1605 Some(lp.height),
1606 )?;
1607 } else {
1608 let lp = &layout.pages[i];
1609 add_new_page(
1610 &mut doc,
1611 lp.width,
1612 lp.height,
1613 overlay,
1614 &font_ids,
1615 &embedded_font_objects,
1616 )?;
1617 }
1618 }
1619
1620 for &page_id in &existing_page_ids[..n_existing.min(n_layout)] {
1625 bake_checkbox_radio_ap_marks(&mut doc, page_id);
1626 }
1627 }
1628
1629 if n_layout < n_existing && !preserve_static {
1637 let excess: Vec<u32> = ((n_layout + 1) as u32..=(n_existing as u32))
1640 .rev()
1641 .collect();
1642 trace_excess_deleted = excess.len();
1643 doc.delete_pages(&excess);
1644 }
1645
1646 if is_static_form {
1647 for &page_id in &existing_page_ids {
1653 strip_widget_annotations(&mut doc, page_id);
1654 }
1655 } else {
1656 for &page_id in existing_page_ids.iter().take(n_layout.min(n_existing)) {
1659 if let Ok(Object::Dictionary(ref mut dict)) = doc.get_object_mut(page_id) {
1660 dict.remove(b"Annots");
1661 }
1662 }
1663 }
1664
1665 debug_assert!(
1667 _stage <= PipelineStage::Cleanup,
1668 "pipeline stage order violated: expected <= Cleanup"
1669 );
1670 #[allow(unused_assignments)]
1671 {
1672 _stage = PipelineStage::Cleanup;
1673 }
1674
1675 remove_acroform(&mut doc);
1676 let stripped_js = javascript_policy::strip_javascript_for_flatten(&mut doc);
1677 if stripped_js > 0 {
1678 log::warn!("stripped {stripped_js} JavaScript action(s) from flattened output");
1679 }
1680
1681 let trace_ctx = if flatten_trace::enabled() {
1684 let (acroform_removed, xfa_removed_structural, needs_rendering_removed) =
1685 catalog_cleanup_status(&doc);
1686 Some((
1687 acroform_removed,
1688 xfa_removed_structural,
1689 needs_rendering_removed,
1690 doc.page_iter().count(),
1691 layout
1692 .pages
1693 .iter()
1694 .filter(|p| p.runtime_instantiated)
1695 .count(),
1696 ))
1697 } else {
1698 None
1699 };
1700
1701 let mut out = Vec::new();
1702 doc.save_to(&mut out)
1703 .map_err(|e| XfaError::LayoutFailed(format!("save: {e}")))?;
1704
1705 if let Some((
1706 acroform_removed,
1707 xfa_removed_structural,
1708 needs_rendering_removed,
1709 output_page_count,
1710 runtime_pages,
1711 )) = trace_ctx
1712 {
1713 let js_mode =
1714 std::env::var("XFA_JS_EXECUTION_MODE").unwrap_or_else(|_| "best_effort_static".into());
1715 flatten_trace::emit(&flatten_trace::TraceInputs {
1716 suppression: &trace_suppression,
1717 input_bytes: pdf_bytes.len(),
1718 template_bytes: template_xml.len(),
1719 js_execution_mode: &js_mode,
1720 flatten_path: if preserve_static {
1721 "static_preserve"
1722 } else {
1723 "dynamic"
1724 },
1725 template_packet_found: true,
1726 datasets_packet_found: datasets_xml.is_some(),
1727 form_packet_found: form_xml.is_some(),
1728 image_files: trace_image_files,
1729 tree: &tree,
1730 scripts: &dynamic_scripts,
1731 layout: &layout,
1732 pages_produced: trace_pages_produced,
1733 pages_after_suppression: layout.pages.len(),
1734 runtime_instantiated_pages: runtime_pages,
1735 overlays: &overlays,
1736 n_layout,
1737 n_existing,
1738 is_static_form,
1739 has_static_content,
1740 preserve_static,
1741 excess_pages_deleted: trace_excess_deleted,
1742 widgets_baked: trace_widgets_baked,
1743 acroform_removed,
1744 xfa_removed_structural,
1745 needs_rendering_removed,
1746 javascript_actions_stripped: stripped_js,
1747 output_bytes: out.len(),
1748 output_page_count,
1749 });
1750 }
1751
1752 let mut dynamic_scripts = dynamic_scripts;
1754 dynamic_scripts.form_dom_match_failures = form_dom_match_failures;
1755 dynamic_scripts.form_dom_match_log = form_dom_match_log;
1756
1757 let mut flatten_out = FlattenOutput::new(out, layout_dump.unwrap_or_default(), dynamic_scripts);
1758 flatten_out.metadata.fresh_merge_admitted_nodes = fresh_merge_admitted;
1759 Ok(flatten_out)
1760}
1761
1762fn catalog_cleanup_status(doc: &Document) -> (bool, bool, bool) {
1766 let root_id = match doc.trailer.get(b"Root") {
1767 Ok(Object::Reference(id)) => *id,
1768 _ => return (true, true, true),
1769 };
1770 let Ok(cat) = doc.get_dictionary(root_id) else {
1771 return (true, true, true);
1772 };
1773 let acroform_present = cat.get(b"AcroForm").is_ok();
1774 let needs_rendering_present = cat.get(b"NeedsRendering").is_ok();
1775 let direct_xfa = cat.get(b"XFA").is_ok();
1776 let acroform_xfa = cat
1777 .get(b"AcroForm")
1778 .ok()
1779 .and_then(|o| match o {
1780 Object::Reference(id) => doc.get_dictionary(*id).ok(),
1781 Object::Dictionary(d) => Some(d),
1782 _ => None,
1783 })
1784 .map(|d| d.get(b"XFA").is_ok())
1785 .unwrap_or(false);
1786 (
1787 !acroform_present,
1788 !(direct_xfa || acroform_xfa),
1789 !needs_rendering_present,
1790 )
1791}
1792
1793fn layout_dump_from_profile(profile: LayoutProfile) -> LayoutDump {
1794 LayoutDump {
1795 pages: profile
1796 .pages
1797 .into_iter()
1798 .enumerate()
1799 .map(|(idx, page)| LayoutDumpEntry {
1800 page_num: idx as u32 + 1,
1801 page_height: page.page_height,
1802 used_height: page.used_height,
1803 overflow_to_next: page.overflow_to_next,
1804 first_overflow_element: page.first_overflow_element,
1805 })
1806 .collect(),
1807 ..Default::default()
1808 }
1809}
1810
1811fn renumber_layout_dump_pages(dump: &mut LayoutDump) {
1812 for (idx, page) in dump.pages.iter_mut().enumerate() {
1813 page.page_num = idx as u32 + 1;
1814 }
1815}
1816
1817pub(crate) fn extract_embedded_images(doc: &Document) -> HashMap<String, Vec<u8>> {
1827 let mut images = HashMap::new();
1828
1829 fn deref_dict<'a>(doc: &'a Document, obj: &'a Object) -> Option<&'a Dictionary> {
1831 match obj {
1832 Object::Reference(id) => doc.get_dictionary(*id).ok(),
1833 Object::Dictionary(d) => Some(d),
1834 _ => None,
1835 }
1836 }
1837
1838 fn extract_stream(doc: &Document, obj: &Object) -> Option<Vec<u8>> {
1840 let stream_obj = match obj {
1841 Object::Reference(id) => doc.get_object(*id).ok()?,
1842 other => other,
1843 };
1844 if let Object::Stream(ref stream) = *stream_obj {
1845 let mut s = stream.clone();
1846 let _ = s.decompress();
1847 Some(s.content.clone())
1848 } else {
1849 None
1850 }
1851 }
1852
1853 let catalog = match doc.catalog() {
1855 Ok(c) => c,
1856 Err(_) => return images,
1857 };
1858 let names_obj = match catalog.get(b"Names") {
1859 Ok(obj) => obj,
1860 Err(_) => {
1861 eprintln!("[img-href] no /Names in catalog");
1862 return images;
1863 }
1864 };
1865 let names_dict = match deref_dict(doc, names_obj) {
1866 Some(d) => d,
1867 None => return images,
1868 };
1869 let ef_obj = match names_dict
1872 .get(b"XFAImages")
1873 .or_else(|_| names_dict.get(b"EmbeddedFiles"))
1874 {
1875 Ok(obj) => obj,
1876 Err(_) => return images,
1877 };
1878 let ef_dict = match deref_dict(doc, ef_obj) {
1879 Some(d) => d,
1880 None => return images,
1881 };
1882
1883 let names_arr_obj = match ef_dict.get(b"Names") {
1885 Ok(obj) => obj,
1886 Err(_) => return images,
1887 };
1888 let names_array = match names_arr_obj {
1889 Object::Array(arr) => arr,
1890 Object::Reference(id) => match doc.get_object(*id) {
1891 Ok(Object::Array(arr)) => arr,
1892 _ => return images,
1893 },
1894 _ => return images,
1895 };
1896
1897 let mut i = 0;
1899 while i + 1 < names_array.len() {
1900 let name = match &names_array[i] {
1901 Object::String(bytes, _) => String::from_utf8_lossy(bytes).to_string(),
1902 _ => {
1903 i += 2;
1904 continue;
1905 }
1906 };
1907
1908 let value_ref = &names_array[i + 1];
1912
1913 if let Some(filespec) = deref_dict(doc, value_ref) {
1915 if let Ok(ef_obj) = filespec.get(b"EF") {
1916 if let Some(ef) = deref_dict(doc, ef_obj) {
1917 if let Ok(f_ref) = ef.get(b"F") {
1918 if let Some(data) = extract_stream(doc, f_ref) {
1919 images.insert(name.clone(), data);
1920 i += 2;
1921 continue;
1922 }
1923 }
1924 }
1925 }
1926 }
1927
1928 if let Some(data) = extract_stream(doc, value_ref) {
1930 images.insert(name.clone(), data);
1931 }
1932
1933 i += 2;
1934 }
1935 images
1936}
1937
1938#[doc(hidden)]
1956pub fn extract_embedded_fonts(doc: &Document) -> Vec<EmbeddedFontData> {
1957 let mut fonts = Vec::new();
1958 let mut seen = std::collections::HashSet::new();
1959 for (&font_object_id, obj) in &doc.objects {
1960 let dict = match obj.as_dict() {
1961 Ok(d) => d,
1962 Err(_) => continue,
1963 };
1964 let is_font =
1965 dict.get(b"Type").ok().and_then(|o| o.as_name().ok()) == Some(b"Font".as_slice());
1966 if !is_font {
1967 continue;
1968 }
1969 let base_font = match dict.get(b"BaseFont").ok().and_then(|o| o.as_name().ok()) {
1970 Some(n) => String::from_utf8_lossy(n).to_string(),
1971 None => continue,
1972 };
1973
1974 let pdf_widths = extract_font_widths(dict);
1975 let pdf_encoding = extract_font_encoding(doc, dict);
1976 let pdf_source_font =
1977 extract_simple_pdf_source_font(doc, font_object_id, dict, pdf_widths.as_ref());
1978
1979 if let Some((stream_id, data)) = extract_font_from_direct_fd(doc, dict, &base_font) {
1981 if seen.insert(stream_id) {
1982 store_font_data(
1983 &mut fonts,
1984 &base_font,
1985 data,
1986 pdf_widths.clone(),
1987 pdf_encoding.clone(),
1988 pdf_source_font,
1989 );
1990 }
1991 continue;
1992 }
1993
1994 if let Some((stream_id, data)) = extract_cidfont_data(doc, dict, &base_font, &seen) {
1998 if seen.insert(stream_id) {
1999 let cid_widths = extract_cid_font_widths(doc, dict);
2000 store_font_data(&mut fonts, &base_font, data, cid_widths, None, None);
2001 }
2002 continue;
2003 }
2004
2005 if let Some(source_font) = pdf_source_font {
2006 store_font_data(
2012 &mut fonts,
2013 &base_font,
2014 Vec::new(),
2015 pdf_widths.clone(),
2016 pdf_encoding.clone(),
2017 Some(source_font),
2018 );
2019 }
2020 }
2021 fonts
2022}
2023
2024fn extract_font_widths(dict: &lopdf::Dictionary) -> Option<(u16, Vec<u16>)> {
2026 let first_char = dict.get(b"FirstChar").ok()?.as_i64().ok()? as u16;
2027 let _last_char = dict.get(b"LastChar").ok()?.as_i64().ok()? as u16;
2028 let widths_array = dict.get(b"Widths").ok()?.as_array().ok()?;
2029 let widths: Vec<u16> = widths_array
2030 .iter()
2031 .filter_map(|w| w.as_i64().ok().map(|v| v as u16))
2032 .collect();
2033 if widths.is_empty() {
2034 return None;
2035 }
2036 Some((first_char, widths))
2037}
2038
2039fn extract_cid_font_widths(
2056 doc: &Document,
2057 type0_dict: &lopdf::Dictionary,
2058) -> Option<(u16, Vec<u16>)> {
2059 let descendants = type0_dict.get(b"DescendantFonts").ok()?.as_array().ok()?;
2060 let desc_ref = descendants.first()?;
2061 let cid_dict = match desc_ref {
2062 Object::Reference(id) => doc.get_dictionary(*id).ok()?,
2063 Object::Dictionary(d) => d,
2064 _ => return None,
2065 };
2066
2067 let default_width = cid_dict
2068 .get(b"DW")
2069 .ok()
2070 .and_then(|o| o.as_i64().ok())
2071 .unwrap_or(1000) as u16;
2072
2073 let w_array = cid_dict.get(b"W").ok()?;
2074 let w_array = match resolve_object(doc, w_array) {
2075 Some(obj) => obj.as_array().ok()?,
2076 None => return None,
2077 };
2078
2079 if w_array.is_empty() {
2080 return None;
2081 }
2082
2083 let mut entries: Vec<(u16, u16)> = Vec::new();
2085 let mut i = 0;
2086 while i < w_array.len() {
2087 let cid_start = match w_array[i].as_i64() {
2088 Ok(v) => v as u16,
2089 Err(_) => {
2090 i += 1;
2091 continue;
2092 }
2093 };
2094 i += 1;
2095 if i >= w_array.len() {
2096 break;
2097 }
2098
2099 if let Ok(widths_arr) = w_array[i].as_array() {
2101 for (j, w_obj) in widths_arr.iter().enumerate() {
2103 if let Ok(w) = w_obj.as_i64() {
2104 entries.push((cid_start + j as u16, w as u16));
2105 }
2106 }
2107 i += 1;
2108 } else if let Ok(cid_last) = w_array[i].as_i64() {
2109 i += 1;
2111 if i >= w_array.len() {
2112 break;
2113 }
2114 if let Ok(width) = w_array[i].as_i64() {
2115 let cid_last = cid_last as u16;
2116 for cid in cid_start..=cid_last {
2117 entries.push((cid, width as u16));
2118 }
2119 }
2120 i += 1;
2121 } else {
2122 i += 1;
2123 }
2124 }
2125
2126 if entries.is_empty() {
2127 return None;
2128 }
2129
2130 let min_cid = entries
2132 .iter()
2133 .map(|(c, _)| *c)
2134 .min()
2135 .expect("entries is non-empty");
2136 let max_cid = entries
2137 .iter()
2138 .map(|(c, _)| *c)
2139 .max()
2140 .expect("entries is non-empty");
2141 let len = (max_cid - min_cid + 1) as usize;
2142 let mut widths = vec![default_width; len];
2143 for (cid, w) in &entries {
2144 widths[(*cid - min_cid) as usize] = *w;
2145 }
2146
2147 Some((min_cid, widths))
2148}
2149
2150fn extract_font_encoding(doc: &Document, dict: &lopdf::Dictionary) -> Option<PdfSimpleEncoding> {
2162 let encoding_obj = resolve_object(doc, dict.get(b"Encoding").ok()?)?;
2163 let encoding_dict = encoding_obj.as_dict().ok()?;
2164 let differences_array = resolve_object(doc, encoding_dict.get(b"Differences").ok()?)?
2165 .as_array()
2166 .ok()?;
2167
2168 let base_encoding = encoding_dict
2169 .get(b"BaseEncoding")
2170 .ok()
2171 .and_then(|obj| resolve_object(doc, obj))
2172 .and_then(|obj| obj.as_name().ok())
2173 .and_then(PdfBaseEncoding::from_pdf_name)
2174 .unwrap_or(PdfBaseEncoding::WinAnsi);
2175
2176 let mut differences = Vec::new();
2177 let mut current_code: Option<u8> = None;
2178 for item in differences_array {
2179 let item = resolve_object(doc, item)?;
2180 if let Ok(code) = item.as_i64() {
2181 current_code = u8::try_from(code).ok();
2182 continue;
2183 }
2184
2185 let Some(name) = item.as_name().ok() else {
2186 continue;
2187 };
2188 let Some(code) = current_code else {
2189 continue;
2190 };
2191 let Some(glyph_name) = std::str::from_utf8(name).ok() else {
2192 continue;
2193 };
2194 if let Some(unicode) = pdf_glyph_name_to_unicode(glyph_name) {
2195 differences.push((code, unicode));
2196 }
2197 current_code = code.checked_add(1);
2198 }
2199
2200 if differences.is_empty() {
2201 return None;
2202 }
2203
2204 Some(PdfSimpleEncoding {
2205 base_encoding,
2206 differences,
2207 })
2208}
2209
2210fn extract_simple_pdf_source_font(
2211 doc: &Document,
2212 font_object_id: ObjectId,
2213 dict: &lopdf::Dictionary,
2214 pdf_widths: Option<&(u16, Vec<u16>)>,
2215) -> Option<PdfSourceFont> {
2216 pdf_widths?;
2217
2218 let subtype = dict.get(b"Subtype").ok().and_then(|obj| obj.as_name().ok());
2219 if subtype == Some(b"Type0".as_slice()) {
2220 return None;
2221 }
2222
2223 let encoding_obj = dict
2232 .get(b"Encoding")
2233 .ok()
2234 .and_then(|obj| resolve_object(doc, obj));
2235 match encoding_obj {
2236 Some(obj) if obj.as_name().ok() == Some(b"WinAnsiEncoding".as_slice()) => {}
2237 Some(obj) => {
2238 let base = obj
2239 .as_dict()
2240 .ok()
2241 .and_then(|enc| enc.get(b"BaseEncoding").ok())
2242 .and_then(|base| resolve_object(doc, base))
2243 .and_then(|base| base.as_name().ok());
2244 if base != Some(b"WinAnsiEncoding".as_slice()) {
2245 return None;
2246 }
2247 if obj
2248 .as_dict()
2249 .ok()
2250 .and_then(|enc| enc.get(b"Differences").ok())
2251 .is_some()
2252 {
2253 return None;
2254 }
2255 }
2256 None => return None,
2257 }
2258
2259 Some(PdfSourceFont {
2260 object_id: font_object_id,
2261 })
2262}
2263
2264fn resolve_object<'a>(doc: &'a Document, obj: &'a Object) -> Option<&'a Object> {
2265 match obj {
2266 Object::Reference(id) => doc.get_object(*id).ok(),
2267 other => Some(other),
2268 }
2269}
2270
2271fn extract_font_from_direct_fd(
2273 doc: &Document,
2274 font_dict: &lopdf::Dictionary,
2275 _base_font: &str,
2276) -> Option<(lopdf::ObjectId, Vec<u8>)> {
2277 let fd_id = font_dict.get(b"FontDescriptor").ok()?.as_reference().ok()?;
2278 let fd = doc.get_dictionary(fd_id).ok()?;
2279
2280 let font_stream_id = fd
2281 .get(b"FontFile2")
2282 .or_else(|_| fd.get(b"FontFile3"))
2283 .or_else(|_| fd.get(b"FontFile"))
2284 .ok()?
2285 .as_reference()
2286 .ok()?;
2287
2288 let stream = doc
2289 .get_object(font_stream_id)
2290 .and_then(|o| o.as_stream())
2291 .ok()?;
2292
2293 let data = stream
2294 .get_plain_content()
2295 .unwrap_or_else(|_| stream.content.clone());
2296
2297 if data.is_empty() {
2298 return None;
2299 }
2300
2301 Some((font_stream_id, data))
2302}
2303
2304fn extract_cidfont_data(
2309 doc: &Document,
2310 font_dict: &lopdf::Dictionary,
2311 _base_font: &str,
2312 seen: &std::collections::HashSet<lopdf::ObjectId>,
2313) -> Option<(lopdf::ObjectId, Vec<u8>)> {
2314 let descendants = font_dict.get(b"DescendantFonts").ok()?.as_array().ok()?;
2316
2317 for desc_ref in descendants {
2319 let desc_id = desc_ref.as_reference().ok()?;
2320 let desc_dict = doc.get_dictionary(desc_id).ok()?;
2321
2322 let fd_id = desc_dict.get(b"FontDescriptor").ok()?.as_reference().ok()?;
2324 let fd = doc.get_dictionary(fd_id).ok()?;
2325
2326 let font_stream_id = fd
2328 .get(b"FontFile3")
2329 .or_else(|_| fd.get(b"FontFile2"))
2330 .or_else(|_| fd.get(b"FontFile"))
2331 .ok()?
2332 .as_reference()
2333 .ok()?;
2334
2335 if seen.contains(&font_stream_id) {
2336 continue;
2337 }
2338
2339 let stream = doc
2340 .get_object(font_stream_id)
2341 .and_then(|o| o.as_stream())
2342 .ok()?;
2343
2344 let data = stream
2345 .get_plain_content()
2346 .unwrap_or_else(|_| stream.content.clone());
2347
2348 if !data.is_empty() {
2349 return Some((font_stream_id, data));
2350 }
2351 }
2352 None
2353}
2354
2355fn store_font_data(
2357 fonts: &mut Vec<EmbeddedFontData>,
2358 base_font: &str,
2359 data: Vec<u8>,
2360 pdf_widths: Option<(u16, Vec<u16>)>,
2361 pdf_encoding: Option<PdfSimpleEncoding>,
2362 pdf_source_font: Option<PdfSourceFont>,
2363) {
2364 let clean_name = if let Some(pos) = base_font.find('+') {
2365 base_font[pos + 1..].to_string()
2366 } else {
2367 base_font.to_string()
2368 };
2369 let allow_family_alias = family_alias_is_regular_face(&clean_name, &data);
2370
2371 fonts.push(EmbeddedFontData {
2373 name: clean_name.clone(),
2374 data: data.clone(),
2375 pdf_widths: pdf_widths.clone(),
2376 pdf_encoding: pdf_encoding.clone(),
2377 pdf_source_font,
2378 });
2379
2380 if let Ok(face) = ttf_parser::Face::parse(&data, 0) {
2384 for name_record in face.names() {
2385 let allow_alias = match name_record.name_id {
2386 ttf_parser::name_id::FAMILY => allow_family_alias,
2387 ttf_parser::name_id::FULL_NAME | ttf_parser::name_id::POST_SCRIPT_NAME => true,
2388 _ => false,
2389 };
2390 if !allow_alias {
2391 continue;
2392 }
2393 if let Some(alias) = name_record.to_string() {
2394 if alias != clean_name {
2395 fonts.push(EmbeddedFontData {
2396 name: alias,
2397 data: data.clone(),
2398 pdf_widths: pdf_widths.clone(),
2399 pdf_encoding: pdf_encoding.clone(),
2400 pdf_source_font,
2401 });
2402 }
2403 }
2404 }
2405 }
2406
2407 let normalized = ps_name_to_family(&clean_name);
2411 if allow_family_alias && normalized != clean_name {
2412 fonts.push(EmbeddedFontData {
2413 name: normalized,
2414 data,
2415 pdf_widths,
2416 pdf_encoding,
2417 pdf_source_font,
2418 });
2419 }
2420}
2421
2422fn family_alias_is_regular_face(clean_name: &str, data: &[u8]) -> bool {
2423 if let Ok(face) = ttf_parser::Face::parse(data, 0) {
2424 if face.is_bold() || face.is_italic() {
2425 return false;
2426 }
2427 }
2428
2429 let lower = clean_name.to_ascii_lowercase();
2430 !lower.contains("bold") && !lower.contains("italic") && !lower.contains("oblique")
2431}
2432
2433fn ps_name_to_family(ps_name: &str) -> String {
2438 let base = ps_name
2440 .strip_suffix("PSMT")
2441 .or_else(|| ps_name.strip_suffix("PS-BoldItalicMT"))
2442 .or_else(|| ps_name.strip_suffix("PS-BoldMT"))
2443 .or_else(|| ps_name.strip_suffix("PS-ItalicMT"))
2444 .or_else(|| ps_name.strip_suffix("-BoldItalicMT"))
2445 .or_else(|| ps_name.strip_suffix("-BoldMT"))
2446 .or_else(|| ps_name.strip_suffix("-ItalicMT"))
2447 .or_else(|| ps_name.strip_suffix("MT"))
2448 .or_else(|| ps_name.strip_suffix("-Regular"))
2449 .or_else(|| ps_name.strip_suffix("-Bold"))
2450 .or_else(|| ps_name.strip_suffix("-Italic"))
2451 .or_else(|| ps_name.strip_suffix("-BoldItalic"))
2452 .unwrap_or(ps_name);
2453 let mut result = String::with_capacity(base.len() + 4);
2456 for (i, ch) in base.chars().enumerate() {
2457 if i > 0 && ch.is_uppercase() {
2458 let prev = base.as_bytes()[i - 1] as char;
2459 if prev.is_lowercase() {
2460 result.push(' ');
2461 }
2462 }
2463 result.push(ch);
2464 }
2465 result
2466}
2467
2468struct TemplateFontEntry {
2470 typeface: String,
2471 weight: Option<String>,
2472 posture: Option<String>,
2473 generic_family: Option<String>,
2474}
2475
2476fn collect_template_font_entries(template_xml: &str) -> Vec<TemplateFontEntry> {
2477 let mut entries = Vec::new();
2478 let mut seen = std::collections::HashSet::new();
2479 if let Ok(xml_doc) = roxmltree::Document::parse(template_xml) {
2480 for node in xml_doc.descendants() {
2481 if node.tag_name().name() == "font" {
2482 if let Some(typeface) = node.attribute("typeface") {
2483 let name = typeface.to_string();
2484 let weight = node.attribute("weight").map(|s| s.to_string());
2485 let posture = node.attribute("posture").map(|s| s.to_string());
2486 let generic_family = node.attribute("genericFamily").map(|s| s.to_string());
2487 let key = font_variant_key(&name, weight.as_deref(), posture.as_deref());
2488 if !name.is_empty() && seen.insert(key.to_lowercase()) {
2489 entries.push(TemplateFontEntry {
2490 typeface: name,
2491 weight,
2492 posture,
2493 generic_family,
2494 });
2495 }
2496 }
2497 }
2498 }
2499 }
2500 entries
2501}
2502
2503fn embed_font_in_pdf(doc: &mut Document, font: &ResolvedFont) -> ObjectId {
2504 let font_stream = Stream::new(
2505 dictionary! {
2506 "Length" => Object::Integer(font.data.len() as i64),
2507 "Length1" => Object::Integer(font.data.len() as i64)
2508 },
2509 font.data.clone(),
2510 );
2511 let font_file_id = doc.add_object(Object::Stream(font_stream));
2512
2513 let upem = font.units_per_em as f64;
2514 let scale = 1000.0 / upem.max(1.0);
2515 let ascent = (font.ascender as f64 * scale) as i64;
2516 let descent = (font.descender as f64 * scale) as i64;
2517 let cap_height = (ascent as f64 * 0.7) as i64;
2518 let base_name = font.name.replace(' ', "-");
2519
2520 let fd = dictionary! {
2521 "Type" => Object::Name(b"FontDescriptor".to_vec()),
2522 "FontName" => Object::Name(base_name.as_bytes().to_vec()),
2523 "Flags" => Object::Integer(32),
2524 "FontBBox" => Object::Array(vec![
2525 Object::Integer(0),
2526 Object::Integer(descent),
2527 Object::Integer(1000),
2528 Object::Integer(ascent),
2529 ]),
2530 "ItalicAngle" => Object::Integer(0),
2531 "Ascent" => Object::Integer(ascent),
2532 "Descent" => Object::Integer(descent),
2533 "CapHeight" => Object::Integer(cap_height),
2534 "StemV" => Object::Integer(80),
2535 "FontFile2" => Object::Reference(font_file_id)
2536 };
2537 let fd_id = doc.add_object(Object::Dictionary(fd));
2538
2539 let cid_info = font.cid_font_info().unwrap_or(CidFontInfo {
2541 widths: vec![500],
2542 gid_to_unicode: vec![],
2543 });
2544
2545 let widths_inner: Vec<Object> = cid_info
2547 .widths
2548 .iter()
2549 .map(|&w| Object::Integer(w as i64))
2550 .collect();
2551 let w_array = vec![Object::Integer(0), Object::Array(widths_inner)];
2552
2553 let cid_font = dictionary! {
2554 "Type" => Object::Name(b"Font".to_vec()),
2555 "Subtype" => Object::Name(b"CIDFontType2".to_vec()),
2556 "BaseFont" => Object::Name(base_name.as_bytes().to_vec()),
2557 "CIDSystemInfo" => Object::Dictionary(dictionary! {
2558 "Registry" => Object::String(b"Adobe".to_vec(), StringFormat::Literal),
2559 "Ordering" => Object::String(b"Identity".to_vec(), StringFormat::Literal),
2560 "Supplement" => Object::Integer(0)
2561 }),
2562 "FontDescriptor" => Object::Reference(fd_id),
2563 "W" => Object::Array(w_array),
2564 "CIDToGIDMap" => Object::Name(b"Identity".to_vec())
2565 };
2566 let cid_font_id = doc.add_object(Object::Dictionary(cid_font));
2567
2568 let tounicode_data = generate_tounicode_cmap(&cid_info.gid_to_unicode);
2570 let tounicode_stream = Stream::new(
2571 dictionary! { "Length" => Object::Integer(tounicode_data.len() as i64) },
2572 tounicode_data,
2573 );
2574 let tounicode_id = doc.add_object(Object::Stream(tounicode_stream));
2575
2576 let type0_font = dictionary! {
2578 "Type" => Object::Name(b"Font".to_vec()),
2579 "Subtype" => Object::Name(b"Type0".to_vec()),
2580 "BaseFont" => Object::Name(base_name.as_bytes().to_vec()),
2581 "Encoding" => Object::Name(b"Identity-H".to_vec()),
2582 "DescendantFonts" => Object::Array(vec![Object::Reference(cid_font_id)]),
2583 "ToUnicode" => Object::Reference(tounicode_id)
2584 };
2585 doc.add_object(Object::Dictionary(type0_font))
2586}
2587
2588fn generate_tounicode_cmap(gid_to_unicode: &[(u16, char)]) -> Vec<u8> {
2590 let mut cmap = String::with_capacity(gid_to_unicode.len() * 24 + 256);
2591 cmap.push_str("/CIDInit /ProcSet findresource begin\n");
2592 cmap.push_str("12 dict begin\n");
2593 cmap.push_str("begincmap\n");
2594 cmap.push_str("/CIDSystemInfo\n");
2595 cmap.push_str("<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n");
2596 cmap.push_str("/CMapName /Adobe-Identity-UCS def\n");
2597 cmap.push_str("/CMapType 2 def\n");
2598 cmap.push_str("1 begincodespacerange\n");
2599 cmap.push_str("<0000> <FFFF>\n");
2600 cmap.push_str("endcodespacerange\n");
2601 for chunk in gid_to_unicode.chunks(100) {
2602 let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
2603 for &(gid, ch) in chunk {
2604 let _ = writeln!(cmap, "<{:04X}> <{:04X}>", gid, ch as u32);
2605 }
2606 cmap.push_str("endbfchar\n");
2607 }
2608 cmap.push_str("endcmap\n");
2609 cmap.push_str("CMapName currentdict /CMap defineresource pop\n");
2610 cmap.push_str("end\nend\n");
2611 cmap.into_bytes()
2612}
2613
2614pub(crate) fn resolve_template_fonts(
2621 template_xml: &str,
2622 pdf_bytes: &[u8],
2623) -> HashMap<String, ResolvedFont> {
2624 let mut resolved = HashMap::new();
2625 let entries = collect_template_font_entries(template_xml);
2626 if entries.is_empty() {
2627 return resolved;
2628 }
2629 let source_doc = match Document::load_mem(pdf_bytes) {
2630 Ok(d) => d,
2631 Err(_) => return resolved,
2632 };
2633 let embedded_fonts = extract_embedded_fonts(&source_doc);
2634 let mut resolver = XfaFontResolver::new(embedded_fonts);
2635 for entry in &entries {
2636 let spec = XfaFontSpec::from_xfa_attrs(
2637 &entry.typeface,
2638 entry.weight.as_deref(),
2639 entry.posture.as_deref(),
2640 None,
2641 entry.generic_family.as_deref(),
2642 );
2643 let key = font_variant_key(
2644 &entry.typeface,
2645 entry.weight.as_deref(),
2646 entry.posture.as_deref(),
2647 );
2648 match resolver.resolve(&spec) {
2649 Ok(font) => {
2650 resolved.insert(key, font);
2651 }
2652 Err(e) => {
2653 eprintln!("Font resolution failed for '{}': {}", entry.typeface, e);
2654 }
2655 }
2656 }
2657 resolved
2658}
2659
2660pub(crate) fn inject_resolved_metrics(
2669 tree: &mut xfa_layout_engine::form::FormTree,
2670 resolved: &HashMap<String, ResolvedFont>,
2671) {
2672 for i in 0..tree.nodes.len() {
2673 let id = xfa_layout_engine::form::FormNodeId(i);
2674 let style = &tree.meta(id).style;
2675 let font_family = style.font_family.clone();
2676 let font_weight = style.font_weight.clone();
2677 let font_style = style.font_style.clone();
2678 if let Some(ref family) = font_family {
2679 let variant_key =
2681 font_variant_key(family, font_weight.as_deref(), font_style.as_deref());
2682 let base_key = font_variant_key(family, None, None);
2683 let font = resolved
2684 .get(&variant_key)
2685 .or_else(|| resolved.get(&base_key));
2686 if let Some(font) = font {
2687 let (_first_char, widths) = font.pdf_glyph_widths();
2688 let node = tree.get_mut(id);
2689 node.font.resolved_widths = Some(widths);
2690 node.font.resolved_upem = Some(font.units_per_em);
2691 node.font.resolved_ascender = Some(font.ascender);
2692 node.font.resolved_descender = Some(font.descender);
2693 }
2694 }
2695 }
2696}
2697
2698fn simple_encoding_unicode_to_code_map(encoding: &PdfSimpleEncoding) -> HashMap<u16, u8> {
2703 let mut map = HashMap::new();
2704 for (code, unicode) in encoding.code_to_unicode_table().into_iter().enumerate() {
2705 if let Some(cp) = unicode {
2706 map.entry(cp).or_insert(code as u8);
2707 }
2708 }
2709 map
2710}
2711
2712fn add_text_chars_for_font(
2713 chars_by_font: &mut HashMap<String, HashSet<char>>,
2714 font_family: Option<&str>,
2715 font_weight: Option<&str>,
2716 font_style: Option<&str>,
2717 text: &str,
2718) {
2719 let Some(family) = font_family else {
2720 return;
2721 };
2722 if text.is_empty() {
2723 return;
2724 }
2725 let chars: Vec<char> = text.chars().filter(|c| !c.is_control()).collect();
2726 if chars.is_empty() {
2727 return;
2728 }
2729
2730 let variant = font_variant_key(family, font_weight, font_style);
2731 chars_by_font
2732 .entry(variant)
2733 .or_default()
2734 .extend(chars.iter().copied());
2735 chars_by_font
2736 .entry(family.to_string())
2737 .or_default()
2738 .extend(chars);
2739}
2740
2741fn add_text_chars_for_style(
2742 chars_by_font: &mut HashMap<String, HashSet<char>>,
2743 style: &FormNodeStyle,
2744 text: &str,
2745) {
2746 add_text_chars_for_font(
2747 chars_by_font,
2748 style.font_family.as_deref(),
2749 style.font_weight.as_deref(),
2750 style.font_style.as_deref(),
2751 text,
2752 );
2753}
2754
2755fn collect_used_chars_from_layout_node(
2756 node: &LayoutNode,
2757 chars_by_font: &mut HashMap<String, HashSet<char>>,
2758) {
2759 match &node.content {
2760 LayoutContent::Text(t) => add_text_chars_for_style(chars_by_font, &node.style, t),
2761 LayoutContent::Field { value, .. } => {
2762 add_text_chars_for_style(chars_by_font, &node.style, value)
2763 }
2764 LayoutContent::WrappedText { lines, .. } => {
2765 for line in lines {
2766 add_text_chars_for_style(chars_by_font, &node.style, line);
2767 }
2768 }
2769 LayoutContent::Draw(DrawContent::Text(t)) => {
2770 add_text_chars_for_style(chars_by_font, &node.style, t)
2771 }
2772 _ => {}
2773 }
2774
2775 if let Some(caption) = &node.style.caption_text {
2776 add_text_chars_for_style(chars_by_font, &node.style, caption);
2777 }
2778
2779 if let Some(spans) = &node.style.rich_text_spans {
2780 for span in spans {
2781 add_text_chars_for_font(
2782 chars_by_font,
2783 span.font_family
2784 .as_deref()
2785 .or(node.style.font_family.as_deref()),
2786 span.font_weight
2787 .as_deref()
2788 .or(node.style.font_weight.as_deref()),
2789 span.font_style
2790 .as_deref()
2791 .or(node.style.font_style.as_deref()),
2792 &span.text,
2793 );
2794 }
2795 }
2796
2797 for child in &node.children {
2798 collect_used_chars_from_layout_node(child, chars_by_font);
2799 }
2800}
2801
2802fn collect_used_chars_by_font(layout: &LayoutDom) -> HashMap<String, HashSet<char>> {
2803 let mut chars_by_font = HashMap::new();
2804 for page in &layout.pages {
2805 for node in &page.nodes {
2806 collect_used_chars_from_layout_node(node, &mut chars_by_font);
2807 }
2808 }
2809 chars_by_font
2810}
2811
2812fn simple_font_can_encode_char(font: &ResolvedFont, ch: char) -> bool {
2813 if ch.is_ascii() {
2814 return true;
2815 }
2816 if let Some(encoding) = &font.pdf_encoding {
2817 let Ok(cp) = u16::try_from(ch as u32) else {
2818 return false;
2819 };
2820 return encoding
2821 .code_to_unicode_table()
2822 .into_iter()
2823 .flatten()
2824 .any(|u| u == cp);
2825 }
2826 unicode_to_winansi(ch).is_some()
2827}
2828
2829fn variant_key_base_name(key: &str) -> Option<&str> {
2830 key.strip_suffix("_Bold_Italic")
2831 .or_else(|| key.strip_suffix("_Bold_Normal"))
2832 .or_else(|| key.strip_suffix("_Normal_Italic"))
2833 .or_else(|| key.strip_suffix("_Normal_Normal"))
2834}
2835
2836#[allow(clippy::type_complexity)]
2837fn embed_resolved_fonts(
2838 doc: &mut Document,
2839 resolved: &HashMap<String, ResolvedFont>,
2840 layout: &LayoutDom,
2841) -> (
2842 HashMap<String, String>,
2843 Vec<(String, ObjectId)>,
2844 HashMap<String, FontMetricsData>,
2845) {
2846 let mut font_map = HashMap::new();
2847 let mut font_objects = Vec::new();
2848 let mut metrics_data = HashMap::new();
2849 let used_chars_by_font = collect_used_chars_by_font(layout);
2850 for (idx, (name, font)) in resolved.iter().enumerate() {
2851 let resource_name = format!("XFA_F{}", idx);
2852 let used_chars = used_chars_by_font
2865 .get(name)
2866 .or_else(|| used_chars_by_font.get(&font.name))
2867 .or_else(|| variant_key_base_name(name).and_then(|base| used_chars_by_font.get(base)));
2868 let source_can_encode_all_text = used_chars.is_none_or(|chars| {
2869 chars
2870 .iter()
2871 .all(|ch| simple_font_can_encode_char(font, *ch))
2872 });
2873 let (obj_id, render_font_data) = if let Some(source_font) = font.pdf_source_font {
2874 if source_can_encode_all_text || font.data.is_empty() {
2875 (source_font.object_id, None)
2876 } else {
2877 (embed_font_in_pdf(doc, font), Some(font.data.clone()))
2878 }
2879 } else {
2880 (embed_font_in_pdf(doc, font), Some(font.data.clone()))
2881 };
2882 font_map.insert(name.clone(), format!("/{}", resource_name));
2883 font_objects.push((resource_name, obj_id));
2884 let (_first_char, widths) = font.pdf_glyph_widths();
2885 metrics_data.insert(
2886 name.clone(),
2887 FontMetricsData {
2888 widths,
2889 upem: font.units_per_em,
2890 ascender: font.ascender,
2891 descender: font.descender,
2892 font_data: render_font_data,
2893 face_index: font.face_index,
2894 simple_unicode_to_code: font
2895 .pdf_encoding
2896 .as_ref()
2897 .map(simple_encoding_unicode_to_code_map),
2898 },
2899 );
2900 }
2901 (font_map, font_objects, metrics_data)
2902}
2903
2904fn static_fallback(pdf_bytes: &[u8]) -> Result<Vec<u8>> {
2911 let mut doc = match Document::load_mem(pdf_bytes) {
2912 Ok(d) => d,
2913 Err(e) => {
2914 eprintln!("static_fallback: lopdf load failed ({e}), returning original bytes");
2915 return Ok(pdf_bytes.to_vec());
2916 }
2917 };
2918 strip_widgets_and_acroform(&mut doc);
2919 javascript_policy::strip_javascript_for_flatten(&mut doc);
2920 let mut out = Vec::new();
2921 if let Err(e) = doc.save_to(&mut out) {
2922 eprintln!("static_fallback: save failed ({e}), returning original bytes");
2923 return Ok(pdf_bytes.to_vec());
2924 }
2925 Ok(out)
2926}
2927
2928std::thread_local! {
2931 static FORM_DOM_MATCH_LOG: std::cell::RefCell<Option<Vec<FormDomMatchEntry>>> =
2932 const { std::cell::RefCell::new(None) };
2933}
2934
2935pub(crate) fn apply_form_dom_presence(
2953 tree: &mut FormTree,
2954 root_id: FormNodeId,
2955 form_xml: &str,
2956 policy: XfaRenderingPolicy,
2957 admit_databound_override: bool,
2958) -> (usize, usize, Vec<FormDomMatchEntry>) {
2959 use xfa_layout_engine::form::{FormNodeType, Presence};
2960
2961 let Ok(doc) = roxmltree::Document::parse(form_xml) else {
2962 return (0, 0, Vec::new());
2963 };
2964
2965 fn clone_subtree(tree: &mut FormTree, src_id: FormNodeId) -> FormNodeId {
2967 let node = tree.get(src_id).clone();
2968 let meta = tree.meta(src_id).clone();
2969 let child_ids: Vec<FormNodeId> = node.children.clone();
2971 let mut new_node = node;
2972 new_node.children = Vec::new();
2973 let mut new_meta = meta;
2975 new_meta.xfa_id = None;
2976 let new_id = tree.add_node_with_meta(new_node, new_meta);
2977 for &child_id in &child_ids {
2979 let cloned_child = clone_subtree(tree, child_id);
2980 tree.get_mut(new_id).children.push(cloned_child);
2981 }
2982 new_id
2983 }
2984
2985 fn extract_field_value(xml_field: roxmltree::Node<'_, '_>) -> Option<String> {
2988 let value_el = xml_field
2989 .children()
2990 .find(|c| c.is_element() && c.tag_name().name() == "value")?;
2991 let inner = value_el.children().find(|c| c.is_element())?;
2993 inner.text().map(|t| t.to_string())
2994 }
2995
2996 fn child_matches(tree: &FormTree, fid: FormNodeId, xml_tag: &str, xml_name: &str) -> bool {
3000 use xfa_layout_engine::form::FormNodeType;
3001 let node = tree.get(fid);
3002 match (xml_tag, &node.node_type) {
3003 ("pageSet", FormNodeType::PageSet) => true,
3004 ("pageArea", FormNodeType::PageArea { .. }) => node.name == xml_name,
3005 ("subform", FormNodeType::Subform | FormNodeType::Area | FormNodeType::ExclGroup) => {
3006 node.name == xml_name
3007 }
3008 ("field", FormNodeType::Field { .. }) => node.name == xml_name,
3009 ("draw", FormNodeType::Draw(_) | FormNodeType::Image { .. }) => node.name == xml_name,
3010 _ => false,
3011 }
3012 }
3013
3014 fn apply_recursive(
3017 tree: &mut FormTree,
3018 form_node_id: FormNodeId,
3019 xml_node: roxmltree::Node<'_, '_>,
3020 policy: XfaRenderingPolicy,
3021 admit_databound_override: bool,
3022 ) -> usize {
3023 let mut admitted: usize = 0;
3024 let xml_tag = xml_node.tag_name().name();
3025 if !matches!(
3026 xml_tag,
3027 "subform" | "field" | "form" | "pageSet" | "pageArea"
3028 ) {
3029 return 0;
3030 }
3031
3032 if xml_tag == "subform" || xml_tag == "field" || xml_tag == "pageArea" {
3034 if let Some(pres) = xml_node.attribute("presence") {
3035 if pres == "hidden" {
3036 if std::env::var("XFA_PRESENCE_PROV").ok().as_deref() == Some("1") {
3039 eprintln!(
3040 "XFA_PRESENCE_PROV site=formdom_explicit id={} name={:?} tag={}",
3041 form_node_id.0,
3042 tree.get(form_node_id).name,
3043 xml_tag
3044 );
3045 }
3046 tree.meta_mut(form_node_id).presence = Presence::Hidden;
3047 }
3048 }
3049 }
3050
3051 if xml_tag == "field" {
3054 if let Some(val) = extract_field_value(xml_node) {
3055 if let FormNodeType::Field { ref value, .. } = tree.get(form_node_id).node_type {
3056 if value.is_empty() {
3057 tree.get_mut(form_node_id).node_type = FormNodeType::Field { value: val };
3058 }
3059 }
3060 }
3061 return 0; }
3063
3064 let xml_children: Vec<roxmltree::Node<'_, '_>> = xml_node
3071 .children()
3072 .filter(|c| {
3073 c.is_element()
3074 && matches!(
3075 c.tag_name().name(),
3076 "subform" | "field" | "draw" | "pageSet" | "pageArea"
3077 )
3078 })
3079 .collect();
3080
3081 let inside_page_set = xml_tag == "pageSet";
3096 let uniform_page_area_template = if inside_page_set {
3097 let mut names: Vec<&str> = xml_children
3098 .iter()
3099 .filter(|c| c.tag_name().name() == "pageArea")
3100 .map(|c| c.attribute("name").unwrap_or(""))
3101 .collect();
3102 names.sort_unstable();
3103 names.dedup();
3104 names.len() == 1
3105 } else {
3106 false
3107 };
3108
3109 let mut xml_groups: Vec<((&str, &str), Vec<roxmltree::Node<'_, '_>>)> = Vec::new();
3114 for &xc in &xml_children {
3115 let xtag = xc.tag_name().name();
3116 let xname = xc.attribute("name").unwrap_or("");
3117 let key = (xtag, xname);
3118 if let Some(last) = xml_groups.last_mut() {
3119 if last.0 == key {
3120 last.1.push(xc);
3121 continue;
3122 }
3123 }
3124 xml_groups.push((key, vec![xc]));
3125 }
3126
3127 let mut form_children = tree.get(form_node_id).children.clone();
3129 let mut used = vec![false; form_children.len()];
3130
3131 for (gkey, group_xml_nodes) in &xml_groups {
3132 let (gtag, gname) = *gkey;
3133 let xml_count = group_xml_nodes.len();
3134
3135 let existing: Vec<(usize, FormNodeId)> = form_children
3137 .iter()
3138 .enumerate()
3139 .filter(|(i, &fid)| !used[*i] && child_matches(tree, fid, gtag, gname))
3140 .map(|(i, &fid)| (i, fid))
3141 .collect();
3142 let existing_count = existing.len();
3143
3144 let expansion_allowed = if gtag == "pageArea" {
3153 inside_page_set && uniform_page_area_template
3154 } else {
3155 true
3156 };
3157
3158 if expansion_allowed && xml_count > existing_count && existing_count > 0 {
3160 let template_id = existing[0].1;
3161 let last_existing_idx = existing.last().expect("existing_count > 0").0;
3164 let insert_pos = last_existing_idx + 1;
3165 let clones_needed = xml_count - existing_count;
3166 let mut new_ids = Vec::new();
3167 for _ in 0..clones_needed {
3168 let cloned = clone_subtree(tree, template_id);
3169 new_ids.push(cloned);
3170 }
3171 for (offset, new_id) in new_ids.iter().enumerate() {
3173 form_children.insert(insert_pos + offset, *new_id);
3174 used.insert(insert_pos + offset, false);
3175 }
3176 tree.get_mut(form_node_id).children = form_children.clone();
3178 }
3179
3180 if gtag == "pageArea" && expansion_allowed && xml_count > existing_count {
3187 let to_mark: Vec<FormNodeId> = form_children
3188 .iter()
3189 .copied()
3190 .filter(|&fid| child_matches(tree, fid, gtag, gname))
3191 .collect();
3192 for fid in to_mark {
3193 tree.meta_mut(fid).runtime_instantiated_page = true;
3194 }
3195 }
3196
3197 for (group_idx, &xc) in group_xml_nodes.iter().enumerate() {
3199 let matched = form_children
3201 .iter()
3202 .enumerate()
3203 .skip(if group_idx > 0 {
3204 form_children
3206 .iter()
3207 .enumerate()
3208 .rfind(|(i, &fid)| used[*i] && child_matches(tree, fid, gtag, gname))
3209 .map(|(i, _)| i + 1)
3210 .unwrap_or(0)
3211 } else {
3212 0
3213 })
3214 .find(|(i, &fid)| !used[*i] && child_matches(tree, fid, gtag, gname));
3215 if let Some((idx, &fid)) = matched {
3216 used[idx] = true;
3217 admitted += apply_recursive(tree, fid, xc, policy, admit_databound_override);
3218 }
3219 }
3220 }
3221
3222 let has_subform_children = xml_children
3232 .iter()
3233 .any(|c| c.tag_name().name() == "subform");
3234 if has_subform_children {
3235 for (i, &fid) in form_children.iter().enumerate() {
3236 if used[i] {
3237 continue;
3238 }
3239 let child_node = tree.get(fid);
3240 if matches!(child_node.node_type, FormNodeType::Subform)
3243 && !child_node.name.is_empty()
3244 {
3245 let meta = tree.meta(fid);
3266 let admit_unmatched_databound = (policy
3267 == XfaRenderingPolicy::FreshMergeExperimental
3268 || admit_databound_override)
3269 && !matches!(meta.presence, Presence::Hidden | Presence::Inactive)
3270 && !meta.is_zero_instance_prototype
3271 && meta.bound_data_node.is_some()
3272 && !meta.data_bind_none;
3273
3274 if std::env::var("XFA_PRESENCE_PROV").ok().as_deref() == Some("1") {
3275 let site = if admit_unmatched_databound {
3276 if policy == XfaRenderingPolicy::FreshMergeExperimental {
3277 "formdom_unmatched_fresh_merge_admitted"
3278 } else {
3279 "formdom_unmatched_databound_admitted"
3280 }
3281 } else {
3282 "formdom_unmatched"
3283 };
3284 eprintln!(
3285 "XFA_PRESENCE_PROV site={site} id={} name={:?}",
3286 fid.0, child_node.name
3287 );
3288 }
3289
3290 if admit_unmatched_databound {
3291 admitted += 1;
3294 } else {
3295 let suppressed_name = child_node.name.clone();
3299 let suppressed_id = fid.0;
3300 tree.meta_mut(fid).presence = Presence::Hidden;
3301 crate::flatten::FORM_DOM_MATCH_LOG.with(|cell| {
3302 if let Some(ref mut log) = *cell.borrow_mut() {
3303 if log.len() < 200 {
3304 log.push(crate::dynamic::FormDomMatchEntry {
3305 template_node_id: suppressed_id,
3306 template_node_name: suppressed_name,
3307 reason: "formdom_unmatched_suppressed".to_string(),
3308 });
3309 }
3310 }
3311 });
3312 }
3313 }
3314 }
3315 }
3316 admitted
3317 }
3318
3319 let form_root = doc.root_element();
3321 let form_root_subform = form_root
3322 .children()
3323 .find(|c| c.is_element() && c.tag_name().name() == "subform");
3324
3325 let diag_on = runtime_diag_enabled() || crate::flatten_trace::enabled();
3327 if diag_on {
3328 FORM_DOM_MATCH_LOG.with(|cell| {
3329 *cell.borrow_mut() = Some(Vec::new());
3330 });
3331 }
3332
3333 let mut total_admitted: usize = 0;
3334 if let Some(xml_root_sf) = form_root_subform {
3335 let root_children = tree.get(root_id).children.clone();
3336 let root_name = xml_root_sf.attribute("name").unwrap_or("");
3337 for &child_id in &root_children {
3338 if tree.get(child_id).name == root_name {
3339 total_admitted += apply_recursive(
3340 tree,
3341 child_id,
3342 xml_root_sf,
3343 policy,
3344 admit_databound_override,
3345 );
3346 break;
3347 }
3348 }
3349 }
3350
3351 let match_log = if diag_on {
3353 FORM_DOM_MATCH_LOG.with(|cell| cell.borrow_mut().take().unwrap_or_default())
3354 } else {
3355 Vec::new()
3356 };
3357 let match_failures = match_log.len();
3358 (total_admitted, match_failures, match_log)
3359}
3360
3361fn is_corrupt_xfa_template(pdf_size: usize, template_xml: &str) -> bool {
3365 if pdf_size >= 1024 {
3367 return false;
3368 }
3369 match roxmltree::Document::parse(template_xml) {
3371 Ok(doc) => {
3372 let root = doc.root_element();
3373 !root.children().any(|c| {
3374 c.is_element()
3375 && matches!(c.tag_name().name(), "subform" | "pageSet" | "subformSet")
3376 })
3377 }
3378 Err(_) => true, }
3380}
3381
3382fn strip_undefined_xml_entities(xml: &str) -> String {
3399 let predefined = ["lt", "gt", "amp", "quot", "apos"];
3400 let mut result = String::with_capacity(xml.len());
3401 let bytes = xml.as_bytes();
3402 let mut pos = 0;
3403
3404 while let Some(rel_amp_pos) = xml[pos..].find('&') {
3405 let amp_pos = pos + rel_amp_pos;
3406 result.push_str(&xml[pos..amp_pos]);
3407
3408 if let Some((entity_name, next_pos)) = parse_xml_entity_reference(xml, amp_pos) {
3409 if entity_name.starts_with('#') || predefined.contains(&entity_name) {
3413 result.push_str(&xml[amp_pos..next_pos]);
3414 }
3415 pos = next_pos;
3416 } else {
3417 result.push('&');
3419 pos = amp_pos + 1;
3420 }
3421 }
3422
3423 if pos < bytes.len() {
3424 result.push_str(&xml[pos..]);
3425 }
3426 result
3427}
3428
3429fn parse_xml_entity_reference(xml: &str, amp_pos: usize) -> Option<(&str, usize)> {
3430 let bytes = xml.as_bytes();
3431 let start = amp_pos + 1;
3432 let first = *bytes.get(start)?;
3433
3434 if first == b'#' {
3436 let mut idx = start + 1;
3437 if matches!(bytes.get(idx), Some(b'x' | b'X')) {
3438 idx += 1;
3439 let hex_start = idx;
3440 while matches!(
3441 bytes.get(idx),
3442 Some(b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F')
3443 ) {
3444 idx += 1;
3445 }
3446 if idx == hex_start || !matches!(bytes.get(idx), Some(b';')) {
3447 return None;
3448 }
3449 } else {
3450 let digits_start = idx;
3451 while matches!(bytes.get(idx), Some(b'0'..=b'9')) {
3452 idx += 1;
3453 }
3454 if idx == digits_start || !matches!(bytes.get(idx), Some(b';')) {
3455 return None;
3456 }
3457 }
3458 return Some((&xml[start..idx], idx + 1));
3459 }
3460
3461 if !is_xml_name_start(first) {
3464 return None;
3465 }
3466
3467 let mut idx = start + 1;
3468 while let Some(&b) = bytes.get(idx) {
3469 if b == b';' {
3470 return Some((&xml[start..idx], idx + 1));
3471 }
3472 if !is_xml_name_char(b) {
3473 return None;
3474 }
3475 idx += 1;
3476 }
3477 None
3478}
3479
3480fn is_xml_name_start(byte: u8) -> bool {
3481 matches!(byte, b':' | b'_' | b'A'..=b'Z' | b'a'..=b'z')
3482}
3483
3484fn is_xml_name_char(byte: u8) -> bool {
3485 is_xml_name_start(byte) || matches!(byte, b'-' | b'.' | b'0'..=b'9')
3486}
3487
3488fn pages_have_static_content(doc: &Document) -> bool {
3501 for page_id in doc.page_iter() {
3502 let streams = page_content_streams(doc, page_id);
3503 if streams.is_empty() {
3504 continue;
3505 }
3506
3507 let mut text_op_count = 0usize;
3513 for stream in &streams {
3514 if is_xfa_placeholder_stream(stream) || is_watermark_stream(stream) {
3515 continue;
3516 }
3517 text_op_count += count_text_operators(stream);
3518 }
3519
3520 if text_op_count >= 5 {
3521 return true;
3522 }
3523 }
3524 false
3525}
3526
3527fn page_content_streams(doc: &Document, page_id: ObjectId) -> Vec<Vec<u8>> {
3528 let Ok(page_dict) = doc.get_dictionary(page_id) else {
3529 return Vec::new();
3530 };
3531
3532 match page_dict.get(b"Contents") {
3533 Ok(Object::Array(arr)) => arr
3534 .iter()
3535 .filter_map(|object| resolve_stream_content(doc, object))
3536 .collect(),
3537 Ok(Object::Reference(id)) => match doc.get_object(*id) {
3538 Ok(Object::Array(arr)) => arr
3539 .iter()
3540 .filter_map(|object| resolve_stream_content(doc, object))
3541 .collect(),
3542 Ok(object) => resolve_stream_content(doc, object).into_iter().collect(),
3543 Err(_) => Vec::new(),
3544 },
3545 Ok(object) => resolve_stream_content(doc, object).into_iter().collect(),
3546 Err(_) => Vec::new(),
3547 }
3548}
3549
3550fn resolve_stream_content(doc: &Document, object: &Object) -> Option<Vec<u8>> {
3551 let stream = match object {
3552 Object::Reference(id) => doc.get_object(*id).ok()?.as_stream().ok()?,
3553 Object::Stream(stream) => stream,
3554 _ => return None,
3555 };
3556
3557 stream
3558 .get_plain_content()
3559 .ok()
3560 .or_else(|| Some(stream.content.clone()))
3561}
3562
3563fn count_text_operators(stream: &[u8]) -> usize {
3565 let mut count = 0;
3566 for window in stream.windows(3) {
3567 if (window[0] == b' ' || window[0] == b')' || window[0] == b']')
3568 && window[1] == b'T'
3569 && (window[2] == b'j' || window[2] == b'J')
3570 {
3571 count += 1;
3572 }
3573 }
3574 count
3575}
3576
3577fn bake_checkbox_radio_ap_marks(doc: &mut Document, page_id: ObjectId) -> usize {
3587 let annots = page_annotations(doc, page_id);
3588 if annots.is_empty() {
3589 return 0;
3590 }
3591
3592 let mut baked = 0usize;
3593 let mut overlay_ops = Vec::new();
3594
3595 for annot in &annots {
3596 let Some(annot_id) = annot.as_reference().ok() else {
3597 continue;
3598 };
3599 let Ok(annot_dict) = doc.get_dictionary(annot_id).cloned() else {
3600 continue;
3601 };
3602
3603 let is_widget = annot_dict
3604 .get(b"Subtype")
3605 .ok()
3606 .and_then(|obj| obj.as_name().ok())
3607 == Some(&b"Widget"[..]);
3608 if !is_widget {
3609 continue;
3610 }
3611
3612 let ap = match annot_dict.get(b"AP").ok().and_then(|o| o.as_dict().ok()) {
3616 Some(ap) => ap.clone(),
3617 None => continue,
3618 };
3619 let normal_obj = match ap.get(b"N").ok() {
3620 Some(obj) => obj.clone(),
3621 None => continue,
3622 };
3623
3624 let states: Dictionary = match &normal_obj {
3626 Object::Reference(id) => match doc.get_object(*id).ok().cloned() {
3627 Some(Object::Dictionary(d)) => d,
3628 _ => continue, },
3630 Object::Dictionary(d) => d.clone(),
3631 _ => continue,
3632 };
3633
3634 if matches!(selected_widget_state(&annot_dict), Some(state) if state == b"Off") {
3635 continue;
3636 }
3637
3638 let on_id = states
3640 .iter()
3641 .filter(|(name, _)| name.as_slice() != b"Off")
3642 .find_map(|(_, obj)| match obj {
3643 Object::Reference(id) => Some(*id),
3644 _ => None,
3645 });
3646 let Some(ap_id) = on_id else { continue };
3647
3648 match doc.get_object(ap_id).ok() {
3650 Some(Object::Stream(_)) => {}
3651 _ => continue,
3652 }
3653
3654 let Some(rect) = annotation_rect(&annot_dict) else {
3655 continue;
3656 };
3657
3658 let xobject_name = format!("XfaCbAp{}", baked);
3659 add_xobject_to_page_resources(doc, page_id, &xobject_name, ap_id);
3660 write_ops(
3661 &mut overlay_ops,
3662 format_args!(
3663 "q 1 0 0 1 {:.3} {:.3} cm /{} Do Q\n",
3664 rect[0], rect[1], xobject_name
3665 ),
3666 );
3667 baked += 1;
3668 }
3669
3670 if !overlay_ops.is_empty() {
3671 append_to_page_content(doc, page_id, &overlay_ops);
3672 }
3673
3674 baked
3675}
3676
3677fn is_xfa_placeholder_stream(stream: &[u8]) -> bool {
3678 const PLACEHOLDER_MARKERS: [&[u8]; 5] = [
3679 b"Please wait",
3680 b"Adobe Reader",
3681 b"reader_download",
3682 b"display this type of document",
3683 b"To view the full contents",
3684 ];
3685
3686 PLACEHOLDER_MARKERS
3687 .iter()
3688 .any(|marker| contains_ascii_case_insensitive(stream, marker))
3689}
3690
3691fn is_watermark_stream(stream: &[u8]) -> bool {
3695 const WATERMARK_MARKERS: [&[u8]; 3] =
3696 [b"Evaluation Only", b"Qoppa Software", b"For Evaluation"];
3697 WATERMARK_MARKERS
3698 .iter()
3699 .any(|marker| contains_ascii_case_insensitive(stream, marker))
3700}
3701
3702fn contains_ascii_case_insensitive(haystack: &[u8], needle: &[u8]) -> bool {
3703 haystack
3704 .windows(needle.len())
3705 .any(|window| window.eq_ignore_ascii_case(needle))
3706}
3707
3708fn write_ops(buf: &mut Vec<u8>, args: std::fmt::Arguments<'_>) {
3709 use std::fmt::Write as _;
3710
3711 let mut text = String::new();
3712 let _ = text.write_fmt(args);
3713 buf.extend_from_slice(text.as_bytes());
3714}
3715
3716fn flatten_widget_appearances(doc: &mut Document) -> usize {
3724 let page_ids: Vec<ObjectId> = doc.page_iter().collect();
3725 let mut flattened = 0usize;
3726
3727 for page_id in page_ids {
3728 let annots = page_annotations(doc, page_id);
3729 if annots.is_empty() {
3730 continue;
3731 }
3732
3733 let mut retained = Vec::new();
3734 let mut overlay_ops = Vec::new();
3735
3736 for annot in annots {
3737 let Some(annot_id) = annot.as_reference().ok() else {
3738 retained.push(annot);
3739 continue;
3740 };
3741
3742 let Ok(annot_dict) = doc.get_dictionary(annot_id).cloned() else {
3743 retained.push(annot);
3744 continue;
3745 };
3746
3747 let is_widget = annot_dict
3748 .get(b"Subtype")
3749 .ok()
3750 .and_then(|obj| obj.as_name().ok())
3751 == Some(&b"Widget"[..]);
3752 if !is_widget {
3753 retained.push(annot);
3754 continue;
3755 }
3756
3757 let Some(rect) = annotation_rect(&annot_dict) else {
3758 retained.push(Object::Reference(annot_id));
3759 continue;
3760 };
3761 let Some(ap_id) = resolve_widget_normal_appearance(doc, &annot_dict) else {
3762 retained.push(Object::Reference(annot_id));
3763 continue;
3764 };
3765
3766 let xobject_name = format!("XfaAp{}", flattened);
3767 add_xobject_to_page_resources(doc, page_id, &xobject_name, ap_id);
3768 write_ops(
3769 &mut overlay_ops,
3770 format_args!(
3771 "q 1 0 0 1 {:.3} {:.3} cm /{} Do Q\n",
3772 rect[0], rect[1], xobject_name
3773 ),
3774 );
3775 flattened += 1;
3776 }
3777
3778 if overlay_ops.is_empty() {
3779 continue;
3780 }
3781
3782 append_to_page_content(doc, page_id, &overlay_ops);
3783 set_page_annotations(doc, page_id, retained);
3784 }
3785
3786 flattened
3787}
3788
3789fn strip_widget_annotations(doc: &mut Document, page_id: ObjectId) {
3791 let annots = page_annotations(doc, page_id);
3792 if annots.is_empty() {
3793 return;
3794 }
3795 let mut retained = Vec::new();
3796 for annot in &annots {
3797 let is_widget = annot
3798 .as_reference()
3799 .ok()
3800 .and_then(|id| doc.get_dictionary(id).ok())
3801 .and_then(|d| d.get(b"Subtype").ok())
3802 .and_then(|obj| obj.as_name().ok())
3803 == Some(&b"Widget"[..]);
3804 if !is_widget {
3805 retained.push(annot.clone());
3806 }
3807 }
3808 set_page_annotations(doc, page_id, retained);
3809}
3810
3811fn page_annotations(doc: &Document, page_id: ObjectId) -> Vec<Object> {
3812 let Ok(page_dict) = doc.get_dictionary(page_id) else {
3813 return Vec::new();
3814 };
3815
3816 match page_dict.get(b"Annots") {
3817 Ok(Object::Array(arr)) => arr.clone(),
3818 Ok(Object::Reference(id)) => doc
3819 .get_object(*id)
3820 .ok()
3821 .and_then(|obj| obj.as_array().ok().cloned())
3822 .unwrap_or_default(),
3823 _ => Vec::new(),
3824 }
3825}
3826
3827fn set_page_annotations(doc: &mut Document, page_id: ObjectId, annots: Vec<Object>) {
3828 if let Ok(Object::Dictionary(ref mut page_dict)) = doc.get_object_mut(page_id) {
3829 if annots.is_empty() {
3830 page_dict.remove(b"Annots");
3831 } else {
3832 page_dict.set("Annots", Object::Array(annots));
3833 }
3834 }
3835}
3836
3837fn annotation_rect(dict: &Dictionary) -> Option<[f32; 4]> {
3838 let rect = dict.get(b"Rect").ok()?.as_array().ok()?;
3839 if rect.len() != 4 {
3840 return None;
3841 }
3842 Some([
3843 rect[0].as_float().ok()?,
3844 rect[1].as_float().ok()?,
3845 rect[2].as_float().ok()?,
3846 rect[3].as_float().ok()?,
3847 ])
3848}
3849
3850fn resolve_widget_normal_appearance(
3851 doc: &mut Document,
3852 annot_dict: &Dictionary,
3853) -> Option<ObjectId> {
3854 let ap = annot_dict.get(b"AP").ok()?.as_dict().ok()?;
3855 let normal = ap.get(b"N").ok()?;
3856 resolve_appearance_object(doc, annot_dict, normal)
3857}
3858
3859fn resolve_appearance_object(
3860 doc: &mut Document,
3861 annot_dict: &Dictionary,
3862 object: &Object,
3863) -> Option<ObjectId> {
3864 match object {
3865 Object::Reference(id) => match doc.get_object(*id).ok()?.clone() {
3866 Object::Stream(_) => Some(*id),
3867 Object::Dictionary(states) => resolve_appearance_state(doc, annot_dict, &states),
3868 _ => None,
3869 },
3870 Object::Stream(stream) => Some(doc.add_object(Object::Stream(stream.clone()))),
3871 Object::Dictionary(states) => resolve_appearance_state(doc, annot_dict, states),
3872 _ => None,
3873 }
3874}
3875
3876fn resolve_appearance_state(
3877 doc: &mut Document,
3878 annot_dict: &Dictionary,
3879 states: &Dictionary,
3880) -> Option<ObjectId> {
3881 if let Some(state) = selected_widget_state(annot_dict) {
3882 if let Ok(object) = states.get(state) {
3883 if let Some(id) = resolve_appearance_object(doc, annot_dict, object) {
3884 return Some(id);
3885 }
3886 }
3887 if state == b"Off" {
3888 return None;
3891 }
3892 }
3893
3894 for fallback in [b"Yes".as_slice(), b"On".as_slice(), b"Off".as_slice()] {
3895 if let Ok(object) = states.get(fallback) {
3896 if let Some(id) = resolve_appearance_object(doc, annot_dict, object) {
3897 return Some(id);
3898 }
3899 }
3900 }
3901
3902 for (_name, object) in states.iter() {
3903 if let Some(id) = resolve_appearance_object(doc, annot_dict, object) {
3904 return Some(id);
3905 }
3906 }
3907
3908 None
3909}
3910
3911fn selected_widget_state(annot_dict: &Dictionary) -> Option<&[u8]> {
3912 annot_dict
3913 .get(b"AS")
3914 .ok()
3915 .and_then(|obj| obj.as_name().ok())
3916 .or_else(|| annot_dict.get(b"V").ok().and_then(|obj| obj.as_name().ok()))
3917}
3918
3919fn add_xobject_to_page_resources(
3920 doc: &mut Document,
3921 page_id: ObjectId,
3922 name: &str,
3923 xobject_id: ObjectId,
3924) {
3925 let resources_ref = doc.get_dictionary(page_id).ok().and_then(|page_dict| {
3926 page_dict
3927 .get(b"Resources")
3928 .ok()
3929 .and_then(|obj| obj.as_reference().ok())
3930 });
3931
3932 if let Some(resources_id) = resources_ref {
3933 let xobject_ref = doc.get_dictionary(resources_id).ok().and_then(|resources| {
3934 resources
3935 .get(b"XObject")
3936 .ok()
3937 .and_then(|obj| obj.as_reference().ok())
3938 });
3939
3940 if let Some(xobject_dict_id) = xobject_ref {
3941 if let Ok(Object::Dictionary(ref mut xobjects)) = doc.get_object_mut(xobject_dict_id) {
3942 xobjects.set(name, Object::Reference(xobject_id));
3943 return;
3944 }
3945 }
3946
3947 if let Ok(Object::Dictionary(ref mut resources)) = doc.get_object_mut(resources_id) {
3948 add_xobject_to_resources_dict(resources, name, xobject_id);
3949 return;
3950 }
3951 }
3952
3953 let inline_xobject_ref = doc.get_dictionary(page_id).ok().and_then(|page_dict| {
3954 page_dict
3955 .get(b"Resources")
3956 .ok()
3957 .and_then(|obj| obj.as_dict().ok())
3958 .and_then(|resources| {
3959 resources
3960 .get(b"XObject")
3961 .ok()
3962 .and_then(|obj| obj.as_reference().ok())
3963 })
3964 });
3965
3966 if let Some(xobject_dict_id) = inline_xobject_ref {
3967 if let Ok(Object::Dictionary(ref mut xobjects)) = doc.get_object_mut(xobject_dict_id) {
3968 xobjects.set(name, Object::Reference(xobject_id));
3969 return;
3970 }
3971 }
3972
3973 if let Ok(Object::Dictionary(ref mut page_dict)) = doc.get_object_mut(page_id) {
3974 if let Ok(Object::Dictionary(ref mut resources)) = page_dict.get_mut(b"Resources") {
3975 add_xobject_to_resources_dict(resources, name, xobject_id);
3976 return;
3977 }
3978
3979 let mut resources = Dictionary::new();
3980 add_xobject_to_resources_dict(&mut resources, name, xobject_id);
3981 page_dict.set("Resources", Object::Dictionary(resources));
3982 }
3983}
3984
3985fn add_xobject_to_resources_dict(resources: &mut Dictionary, name: &str, xobject_id: ObjectId) {
3986 if let Ok(Object::Dictionary(ref mut xobjects)) = resources.get_mut(b"XObject") {
3987 xobjects.set(name, Object::Reference(xobject_id));
3988 } else {
3989 let mut xobjects = Dictionary::new();
3990 xobjects.set(name, Object::Reference(xobject_id));
3991 resources.set("XObject", Object::Dictionary(xobjects));
3992 }
3993}
3994
3995fn append_to_page_content(doc: &mut Document, page_id: ObjectId, data: &[u8]) {
3996 let new_stream_id = doc.add_object(Object::Stream(Stream::new(dictionary! {}, data.to_vec())));
3997
3998 let contents = doc
3999 .get_dictionary(page_id)
4000 .ok()
4001 .and_then(|page_dict| page_dict.get(b"Contents").ok().cloned());
4002
4003 let new_contents = match contents {
4010 Some(existing) => {
4011 let mut flattened = Vec::new();
4012 flatten_page_contents_entries(doc, existing, &mut flattened);
4013 flattened.push(Object::Reference(new_stream_id));
4014 if flattened.len() == 1 {
4015 flattened.pop().expect("flattened.len() == 1")
4017 } else {
4018 Object::Array(flattened)
4019 }
4020 }
4021 None => Object::Reference(new_stream_id),
4022 };
4023
4024 if let Ok(Object::Dictionary(ref mut page_dict)) = doc.get_object_mut(page_id) {
4025 page_dict.set("Contents", new_contents);
4026 }
4027}
4028
4029fn flatten_page_contents_entries(doc: &mut Document, object: Object, out: &mut Vec<Object>) {
4030 match object {
4031 Object::Reference(id) => match doc.get_object(id).cloned() {
4032 Ok(Object::Array(items)) => {
4033 for item in items {
4034 flatten_page_contents_entries(doc, item, out);
4035 }
4036 }
4037 _ => out.push(Object::Reference(id)),
4038 },
4039 Object::Array(items) => {
4040 for item in items {
4041 flatten_page_contents_entries(doc, item, out);
4042 }
4043 }
4044 Object::Stream(stream) => {
4045 let stream_id = doc.add_object(Object::Stream(stream));
4046 out.push(Object::Reference(stream_id));
4047 }
4048 other => out.push(other),
4049 }
4050}
4051
4052fn strip_widgets_and_acroform(doc: &mut Document) {
4058 remove_acroform(doc);
4059}
4060
4061fn write_page_content(
4063 doc: &mut Document,
4064 page_id: ObjectId,
4065 overlay: &PageOverlay,
4066 font_ids: &[ObjectId; 3],
4067 embedded_fonts: &[(String, ObjectId)],
4068 page_width: Option<f64>,
4069 page_height: Option<f64>,
4070) -> Result<()> {
4071 let mut resources = make_resources_dict(font_ids, embedded_fonts);
4072
4073 let mut xobjects = Dictionary::new();
4074 for img in &overlay.images {
4075 match embed_image(doc, &img.data, &img.mime_type) {
4076 Ok(result) => {
4077 xobjects.set(img.name.as_str(), Object::Reference(result.object_id));
4078 }
4079 Err(e) => {
4080 eprintln!("failed to embed image {}: {}", img.name, e);
4081 }
4082 }
4083 }
4084 if !xobjects.is_empty() {
4085 resources.set("XObject", Object::Dictionary(xobjects));
4086 }
4087
4088 let stream = Stream::new(
4089 dictionary! { "Length" => Object::Integer(overlay.content_stream.len() as i64) },
4090 overlay.content_stream.clone(),
4091 );
4092 let stream_id = doc.add_object(Object::Stream(stream));
4093
4094 if let Ok(Object::Dictionary(ref mut page_dict)) = doc.get_object_mut(page_id) {
4095 page_dict.set("Contents", Object::Reference(stream_id));
4096 page_dict.set("Resources", Object::Dictionary(resources));
4097 if let (Some(w), Some(h)) = (page_width, page_height) {
4101 page_dict.set(
4102 "MediaBox",
4103 Object::Array(vec![
4104 Object::Real(0.0),
4105 Object::Real(0.0),
4106 Object::Real(w as f32),
4107 Object::Real(h as f32),
4108 ]),
4109 );
4110 }
4111 }
4112 Ok(())
4113}
4114
4115fn overlay_page_content(
4121 doc: &mut Document,
4122 page_id: ObjectId,
4123 overlay: &PageOverlay,
4124 font_ids: &[ObjectId; 3],
4125 embedded_fonts: &[(String, ObjectId)],
4126) -> Result<()> {
4127 let xfa_resources = make_resources_dict(font_ids, embedded_fonts);
4128
4129 let mut xfa_xobjects = Dictionary::new();
4130 for img in &overlay.images {
4131 match embed_image(doc, &img.data, &img.mime_type) {
4132 Ok(result) => {
4133 xfa_xobjects.set(img.name.as_str(), Object::Reference(result.object_id));
4134 }
4135 Err(e) => {
4136 eprintln!("failed to embed image {}: {}", img.name, e);
4137 }
4138 }
4139 }
4140
4141 merge_xfa_resources_into_page(doc, page_id, &xfa_resources, &xfa_xobjects);
4142
4143 if !overlay.content_stream.is_empty() {
4144 append_to_page_content(doc, page_id, &overlay.content_stream);
4145 }
4146
4147 Ok(())
4148}
4149
4150fn merge_xfa_resources_into_page(
4153 doc: &mut Document,
4154 page_id: ObjectId,
4155 xfa_resources: &Dictionary,
4156 xfa_xobjects: &Dictionary,
4157) {
4158 let existing_resources = doc
4159 .get_dictionary(page_id)
4160 .ok()
4161 .and_then(|page_dict| {
4162 page_dict.get(b"Resources").ok().and_then(|obj| match obj {
4163 Object::Reference(id) => doc.get_dictionary(*id).ok().cloned(),
4164 Object::Dictionary(d) => Some(d.clone()),
4165 _ => None,
4166 })
4167 })
4168 .unwrap_or_default();
4169
4170 let mut merged = existing_resources;
4171
4172 if let Ok(xfa_font_dict) = xfa_resources.get(b"Font").and_then(|o| o.as_dict()) {
4175 let existing_font = merged
4176 .get(b"Font")
4177 .ok()
4178 .and_then(|obj| match obj {
4179 Object::Dictionary(d) => Some(d.clone()),
4180 Object::Reference(id) => doc.get_dictionary(*id).ok().cloned(),
4181 _ => None,
4182 })
4183 .unwrap_or_default();
4184
4185 let mut font_merged = existing_font;
4186 for (key, val) in xfa_font_dict.iter() {
4187 if font_merged.get(key).is_err() {
4188 font_merged.set(key.clone(), val.clone());
4189 }
4190 }
4191 merged.set("Font", Object::Dictionary(font_merged));
4192 }
4193
4194 if !xfa_xobjects.is_empty() {
4196 let existing_xobj = merged
4197 .get(b"XObject")
4198 .ok()
4199 .and_then(|obj| match obj {
4200 Object::Dictionary(d) => Some(d.clone()),
4201 Object::Reference(id) => doc.get_dictionary(*id).ok().cloned(),
4202 _ => None,
4203 })
4204 .unwrap_or_default();
4205
4206 let mut xobj_merged = existing_xobj;
4207 for (key, val) in xfa_xobjects.iter() {
4208 xobj_merged.set(key.clone(), val.clone());
4209 }
4210 merged.set("XObject", Object::Dictionary(xobj_merged));
4211 }
4212
4213 if let Ok(Object::Dictionary(ref mut page_dict)) = doc.get_object_mut(page_id) {
4214 page_dict.set("Resources", Object::Dictionary(merged));
4215 }
4216}
4217
4218fn add_new_page(
4220 doc: &mut Document,
4221 w: f64,
4222 h: f64,
4223 overlay: &PageOverlay,
4224 font_ids: &[ObjectId; 3],
4225 embedded_fonts: &[(String, ObjectId)],
4226) -> Result<()> {
4227 let mut resources = make_resources_dict(font_ids, embedded_fonts);
4228
4229 let mut xobjects = Dictionary::new();
4230 for img in &overlay.images {
4231 match embed_image(doc, &img.data, &img.mime_type) {
4232 Ok(result) => {
4233 xobjects.set(img.name.as_str(), Object::Reference(result.object_id));
4234 }
4235 Err(e) => {
4236 eprintln!("failed to embed image {}: {}", img.name, e);
4237 }
4238 }
4239 }
4240 if !xobjects.is_empty() {
4241 resources.set("XObject", Object::Dictionary(xobjects));
4242 }
4243
4244 let stream = Stream::new(
4245 dictionary! { "Length" => Object::Integer(overlay.content_stream.len() as i64) },
4246 overlay.content_stream.clone(),
4247 );
4248 let stream_id = doc.add_object(Object::Stream(stream));
4249
4250 let pages_id = find_pages_root(doc)?;
4252
4253 let page_id = doc.add_object(Object::Dictionary(dictionary! {
4254 "Type" => Object::Name(b"Page".to_vec()),
4255 "Parent" => Object::Reference(pages_id),
4256 "MediaBox" => Object::Array(vec![
4257 Object::Integer(0), Object::Integer(0),
4258 Object::Real(w as f32), Object::Real(h as f32),
4259 ]),
4260 "Contents" => Object::Reference(stream_id),
4261 "Resources" => Object::Dictionary(resources)
4262 }));
4263
4264 if let Ok(Object::Dictionary(ref mut pages_dict)) = doc.get_object_mut(pages_id) {
4266 if let Ok(Object::Array(ref mut kids)) = pages_dict.get_mut(b"Kids") {
4267 kids.push(Object::Reference(page_id));
4268 }
4269 if let Ok(Object::Integer(ref mut count)) = pages_dict.get_mut(b"Count") {
4270 *count += 1;
4271 }
4272 }
4273 Ok(())
4274}
4275
4276fn make_resources_dict(
4277 font_ids: &[ObjectId; 3],
4278 embedded_fonts: &[(String, ObjectId)],
4279) -> Dictionary {
4280 let mut fonts = Dictionary::new();
4281 fonts.set("F1", Object::Reference(font_ids[0]));
4282 fonts.set("F2", Object::Reference(font_ids[1]));
4283 fonts.set("F3", Object::Reference(font_ids[2]));
4284 for (name, obj_id) in embedded_fonts {
4285 fonts.set(name.as_str(), Object::Reference(*obj_id));
4286 }
4287 let mut resources = Dictionary::new();
4288 resources.set("Font", Object::Dictionary(fonts));
4289 resources
4290}
4291
4292fn find_pages_root(doc: &Document) -> Result<ObjectId> {
4293 let root_id = doc
4294 .trailer
4295 .get(b"Root")
4296 .ok()
4297 .and_then(|o: &Object| o.as_reference().ok())
4298 .ok_or_else(|| XfaError::LoadFailed("no /Root in trailer".to_string()))?;
4299 let catalog = doc
4300 .get_dictionary(root_id)
4301 .map_err(|e| XfaError::LoadFailed(format!("catalog: {e}")))?;
4302 catalog
4303 .get(b"Pages")
4304 .ok()
4305 .and_then(|o: &Object| o.as_reference().ok())
4306 .ok_or_else(|| XfaError::LoadFailed("no /Pages in catalog".to_string()))
4307}
4308
4309fn remove_acroform(doc: &mut Document) {
4323 let root_id = match doc.trailer.get(b"Root") {
4324 Ok(Object::Reference(id)) => *id,
4325 _ => return,
4326 };
4327
4328 let acroform_id: Option<ObjectId> = {
4331 if let Ok(Object::Dictionary(ref mut dict)) = doc.get_object_mut(root_id) {
4332 let acroform_ref = dict.get(b"AcroForm").ok().and_then(|o| {
4333 if let Object::Reference(id) = o {
4334 Some(*id)
4335 } else {
4336 None
4337 }
4338 });
4339 dict.remove(b"AcroForm");
4340 dict.remove(b"NeedsRendering");
4341 acroform_ref
4342 } else {
4343 None
4344 }
4345 };
4346
4347 let xfa_stream_ids: Vec<ObjectId> = acroform_id
4350 .and_then(|af_id| doc.get_dictionary(af_id).ok())
4351 .map(|af_dict| match af_dict.get(b"XFA") {
4352 Ok(Object::Array(arr)) => arr
4353 .iter()
4354 .filter_map(|o| {
4355 if let Object::Reference(id) = o {
4356 Some(*id)
4357 } else {
4358 None
4359 }
4360 })
4361 .collect(),
4362 Ok(Object::Reference(id)) => vec![*id],
4363 _ => Vec::new(),
4364 })
4365 .unwrap_or_default();
4366
4367 if let Some(af_id) = acroform_id {
4368 if let Ok(Object::Dictionary(ref mut af_dict)) = doc.get_object_mut(af_id) {
4369 af_dict.remove(b"XFA");
4370 }
4371 }
4372
4373 for stream_id in xfa_stream_ids {
4377 doc.objects.remove(&stream_id);
4378 }
4379 if let Some(af_id) = acroform_id {
4380 doc.objects.remove(&af_id);
4381 }
4382
4383 let page_ids: Vec<ObjectId> = doc.page_iter().collect();
4386 for page_id in page_ids {
4387 strip_widget_annotations(doc, page_id);
4388 }
4389}
4390
4391pub struct FlattenValidation {
4400 pub has_no_xfa: bool,
4402 pub has_no_needs_rendering: bool,
4404 pub has_no_acroform: bool,
4406 pub page_count: usize,
4408 pub warnings: Vec<String>,
4410}
4411
4412pub fn validate_flattened_pdf(pdf_bytes: &[u8]) -> Result<FlattenValidation> {
4420 if pdf_bytes.is_empty() {
4421 return Ok(FlattenValidation {
4422 has_no_xfa: true,
4423 has_no_needs_rendering: true,
4424 has_no_acroform: true,
4425 page_count: 0,
4426 warnings: vec!["empty input — no PDF to validate".into()],
4427 });
4428 }
4429
4430 let doc = match Document::load_mem(pdf_bytes) {
4431 Ok(d) => d,
4432 Err(e) => {
4433 return Ok(FlattenValidation {
4434 has_no_xfa: false,
4435 has_no_needs_rendering: false,
4436 has_no_acroform: false,
4437 page_count: 0,
4438 warnings: vec![format!("could not parse PDF: {e}")],
4439 });
4440 }
4441 };
4442
4443 let mut warnings = Vec::new();
4444 let mut has_no_xfa = true;
4445 let mut has_no_needs_rendering = true;
4446 let mut has_no_acroform = true;
4447
4448 let root_id = doc.trailer.get(b"Root").ok().and_then(|o| {
4450 if let Object::Reference(id) = o {
4451 Some(*id)
4452 } else {
4453 None
4454 }
4455 });
4456
4457 if let Some(rid) = root_id {
4458 if let Ok(catalog) = doc.get_dictionary(rid) {
4459 if catalog.get(b"AcroForm").is_ok() {
4460 has_no_acroform = false;
4461 warnings.push("/AcroForm still present in catalog".into());
4462
4463 let acroform_has_xfa = catalog
4465 .get(b"AcroForm")
4466 .ok()
4467 .and_then(|o| match o {
4468 Object::Reference(id) => doc.get_dictionary(*id).ok(),
4469 Object::Dictionary(d) => Some(d),
4470 _ => None,
4471 })
4472 .map(|d| d.get(b"XFA").is_ok())
4473 .unwrap_or(false);
4474
4475 if acroform_has_xfa {
4476 has_no_xfa = false;
4477 warnings.push("/XFA still present in AcroForm dictionary".into());
4478 }
4479 }
4480
4481 if catalog.get(b"NeedsRendering").is_ok() {
4482 has_no_needs_rendering = false;
4483 warnings.push("/NeedsRendering still present in catalog".into());
4484 }
4485
4486 if catalog.get(b"XFA").is_ok() {
4488 has_no_xfa = false;
4489 warnings.push("/XFA still present directly in catalog".into());
4490 }
4491 }
4492 }
4493
4494 let page_ids: Vec<ObjectId> = doc.page_iter().collect();
4496 let page_count = page_ids.len();
4497 for page_id in page_ids {
4498 for annot_obj in page_annotations(&doc, page_id) {
4499 let is_widget = annot_obj
4500 .as_reference()
4501 .ok()
4502 .and_then(|id| doc.get_dictionary(id).ok())
4503 .and_then(|d| {
4504 d.get(b"Subtype")
4505 .ok()
4506 .map(|st| st == &Object::Name(b"Widget".to_vec()))
4507 })
4508 .unwrap_or(false);
4509 if is_widget {
4510 warnings.push(format!(
4511 "widget annotation found on page (object {:?})",
4512 annot_obj
4513 ));
4514 }
4515 }
4516 }
4517
4518 Ok(FlattenValidation {
4519 has_no_xfa,
4520 has_no_needs_rendering,
4521 has_no_acroform,
4522 page_count,
4523 warnings,
4524 })
4525}
4526
4527pub struct FlattenQualityMetrics {
4535 pub page_count_before: usize,
4537 pub page_count_after: usize,
4539 pub page_count_match: bool,
4541 pub content_stream_bytes_before: usize,
4543 pub content_stream_bytes_after: usize,
4545 pub content_ratio: f64,
4548}
4549
4550pub fn compare_flatten_quality(
4555 original_bytes: &[u8],
4556 flattened_bytes: &[u8],
4557) -> Result<FlattenQualityMetrics> {
4558 fn count_pages_and_stream_bytes(pdf_bytes: &[u8]) -> (usize, usize) {
4559 let doc = match Document::load_mem(pdf_bytes) {
4560 Ok(d) => d,
4561 Err(_) => return (0, 0),
4562 };
4563 let page_count = doc.page_iter().count();
4564 let stream_bytes: usize = doc
4565 .objects
4566 .values()
4567 .filter_map(|obj| {
4568 if let Object::Stream(s) = obj {
4569 s.content.len().into()
4571 } else {
4572 None
4573 }
4574 })
4575 .sum();
4576 (page_count, stream_bytes)
4577 }
4578
4579 let (page_count_before, content_stream_bytes_before) =
4580 count_pages_and_stream_bytes(original_bytes);
4581 let (page_count_after, content_stream_bytes_after) =
4582 count_pages_and_stream_bytes(flattened_bytes);
4583
4584 let content_ratio = if content_stream_bytes_before == 0 {
4585 1.0_f64
4586 } else {
4587 content_stream_bytes_after as f64 / content_stream_bytes_before as f64
4588 };
4589
4590 Ok(FlattenQualityMetrics {
4591 page_count_before,
4592 page_count_after,
4593 page_count_match: page_count_before == page_count_after,
4594 content_stream_bytes_before,
4595 content_stream_bytes_after,
4596 content_ratio,
4597 })
4598}
4599
4600pub struct TextValidation {
4610 pub expected_values: Vec<String>,
4612 pub found_values: Vec<String>,
4614 pub missing_values: Vec<String>,
4616 pub completeness_ratio: f64,
4619}
4620
4621fn extract_field_values_from_datasets(datasets_xml: &str) -> Vec<String> {
4624 let mut values = Vec::new();
4628 let mut remaining = datasets_xml;
4629
4630 while let Some(open_pos) = remaining.find("<field") {
4631 let tag_end = match remaining[open_pos..].find('>') {
4633 Some(p) => open_pos + p + 1,
4634 None => break,
4635 };
4636
4637 if remaining[open_pos..tag_end].ends_with("/>") {
4639 remaining = &remaining[tag_end..];
4640 continue;
4641 }
4642
4643 let close_tag = "</field>";
4645 match remaining[tag_end..].find(close_tag) {
4646 Some(close_pos) => {
4647 let inner = &remaining[tag_end..tag_end + close_pos];
4648 let text = extract_innermost_text(inner);
4651 if !text.is_empty() {
4652 values.push(text);
4653 }
4654 remaining = &remaining[tag_end + close_pos + close_tag.len()..];
4655 }
4656 None => break,
4657 }
4658 }
4659 values
4660}
4661
4662fn extract_innermost_text(inner: &str) -> String {
4665 if let Some(start) = inner.find("<text>") {
4667 let content_start = start + "<text>".len();
4668 if let Some(end) = inner[content_start..].find("</text>") {
4669 let s = inner[content_start..content_start + end].trim().to_string();
4670 if !s.is_empty() {
4671 return s;
4672 }
4673 }
4674 }
4675 let stripped = strip_xml_tags(inner);
4677 stripped.trim().to_string()
4678}
4679
4680fn strip_xml_tags(s: &str) -> String {
4682 let mut out = String::with_capacity(s.len());
4683 let mut in_tag = false;
4684 for ch in s.chars() {
4685 match ch {
4686 '<' => in_tag = true,
4687 '>' => in_tag = false,
4688 _ if !in_tag => out.push(ch),
4689 _ => {}
4690 }
4691 }
4692 out
4693}
4694
4695fn extract_text_from_pdf_bytes(pdf_bytes: &[u8]) -> String {
4702 let doc = match Document::load_mem(pdf_bytes) {
4703 Ok(d) => d,
4704 Err(_) => return String::new(),
4705 };
4706
4707 let mut text = String::new();
4708
4709 for obj in doc.objects.values() {
4710 if let Object::Stream(ref stream) = obj {
4711 let content = match stream.decompressed_content() {
4713 Ok(c) => c,
4714 Err(_) => stream.content.clone(),
4715 };
4716 let fragment = extract_text_from_content_stream(&content);
4717 if !fragment.is_empty() {
4718 text.push(' ');
4719 text.push_str(&fragment);
4720 }
4721 }
4722 }
4723 text
4724}
4725
4726fn extract_text_from_content_stream(content: &[u8]) -> String {
4729 let s = String::from_utf8_lossy(content);
4730 let mut result = String::new();
4731
4732 for (i, ch) in s.char_indices() {
4735 if ch == '(' {
4736 let start = i + 1;
4738 let mut depth: i32 = 1;
4739 let mut end = start;
4740 let bytes = s.as_bytes();
4741 while end < bytes.len() && depth > 0 {
4742 match bytes[end] {
4743 b'(' => depth += 1,
4744 b')' => depth -= 1,
4745 b'\\' => {
4746 end += 1; }
4748 _ => {}
4749 }
4750 end += 1;
4751 }
4752 if depth == 0 {
4753 let literal = &s[start..end - 1];
4754 if literal.chars().all(|c| {
4756 c.is_ascii()
4757 && (c.is_alphanumeric() || c.is_whitespace() || c.is_ascii_punctuation())
4758 }) {
4759 let trimmed = literal.trim();
4760 if !trimmed.is_empty() {
4761 result.push(' ');
4762 result.push_str(trimmed);
4763 }
4764 }
4765 }
4766 }
4767 }
4768 result
4769}
4770
4771pub fn validate_text_completeness(
4784 original_xfa_bytes: &[u8],
4785 flattened_bytes: &[u8],
4786) -> crate::error::Result<TextValidation> {
4787 let packets = match crate::extract::extract_xfa_from_bytes(original_xfa_bytes.to_vec()) {
4789 Ok(p) => p,
4790 Err(_) => {
4791 return Ok(TextValidation {
4793 expected_values: vec![],
4794 found_values: vec![],
4795 missing_values: vec![],
4796 completeness_ratio: 1.0,
4797 });
4798 }
4799 };
4800
4801 let datasets_xml = match packets.datasets() {
4802 Some(ds) => ds.to_string(),
4803 None => {
4804 return Ok(TextValidation {
4805 expected_values: vec![],
4806 found_values: vec![],
4807 missing_values: vec![],
4808 completeness_ratio: 1.0,
4809 });
4810 }
4811 };
4812
4813 let expected_values = extract_field_values_from_datasets(&datasets_xml);
4815
4816 if expected_values.is_empty() {
4817 return Ok(TextValidation {
4818 expected_values: vec![],
4819 found_values: vec![],
4820 missing_values: vec![],
4821 completeness_ratio: 1.0,
4822 });
4823 }
4824
4825 let output_text = extract_text_from_pdf_bytes(flattened_bytes);
4827
4828 let mut found_values = Vec::new();
4830 let mut missing_values = Vec::new();
4831
4832 for value in &expected_values {
4833 if output_text.contains(value.as_str()) {
4834 found_values.push(value.clone());
4835 } else {
4836 missing_values.push(value.clone());
4837 }
4838 }
4839
4840 let completeness_ratio = if expected_values.is_empty() {
4841 1.0
4842 } else {
4843 found_values.len() as f64 / expected_values.len() as f64
4844 };
4845
4846 Ok(TextValidation {
4847 expected_values,
4848 found_values,
4849 missing_values,
4850 completeness_ratio,
4851 })
4852}
4853
4854#[cfg(test)]
4865fn flatten_xfa_to_pdf_simulate_reentrant(pdf_bytes: &[u8]) -> Result<Vec<u8>> {
4866 FLATTEN_DEPTH.with(|d| d.set(1));
4867 let result = flatten_xfa_to_pdf(pdf_bytes);
4868 FLATTEN_DEPTH.with(|d| d.set(0));
4871 result
4872}
4873
4874#[cfg(test)]
4875mod tests {
4876 use super::*;
4877
4878 const HOSTILE_FORMCALC: &[u8] =
4881 include_bytes!("../../../fixtures/formcalc/hostile_deep_call_chain.pdf");
4882
4883 const BENIGN_FORMCALC: &[u8] =
4885 include_bytes!("../../../fixtures/formcalc/fc_01_arithmetic.pdf");
4886
4887 fn flatten_inline(pdf_bytes: &'static [u8]) -> bool {
4890 let packets = extract_xfa_from_bytes(pdf_bytes.to_vec()).expect("fixture carries XFA");
4891 let template_xml = strip_undefined_xml_entities(packets.template().expect("template"));
4892 let datasets_xml = packets.datasets().map(strip_undefined_xml_entities);
4893 let form_xml = packets.get_packet("form").map(|s| s.to_string());
4894 xfa_flatten_inner(
4895 pdf_bytes,
4896 &template_xml,
4897 datasets_xml.as_deref(),
4898 form_xml.as_deref(),
4899 false,
4900 XfaRenderingPolicy::default(),
4901 )
4902 .is_ok()
4903 }
4904
4905 fn on_a_stack_of(bytes: usize, f: impl FnOnce() -> bool + Send + 'static) -> bool {
4908 std::thread::Builder::new()
4909 .stack_size(bytes)
4910 .spawn(f)
4911 .expect("spawn")
4912 .join()
4913 .expect("the pipeline must return, not overflow the stack")
4914 }
4915
4916 #[test]
4923 fn the_inline_pipeline_returns_for_the_hostile_form_on_a_1_mib_stack() {
4924 let roomy = on_a_stack_of(8 << 20, || flatten_inline(HOSTILE_FORMCALC));
4937 let wasm_shape = on_a_stack_of(1 << 20, || flatten_inline(HOSTILE_FORMCALC));
4938 assert_eq!(
4939 wasm_shape, roomy,
4940 "the hostile form answered differently on a 1 MiB stack ({wasm_shape}) \
4941 than on 8 MiB ({roomy}); the budget is supposed to make the caller's \
4942 stack irrelevant"
4943 );
4944 }
4945
4946 #[test]
4949 fn the_inline_pipeline_flattens_an_ordinary_form_on_a_1_mib_stack() {
4950 assert!(
4951 on_a_stack_of(1 << 20, || flatten_inline(BENIGN_FORMCALC)),
4952 "the benign control stopped flattening on a 1 MiB stack"
4953 );
4954 }
4955
4956 fn build_xfa_pdf_with_content(xdp: &str, page_content: Vec<u8>) -> Vec<u8> {
4958 use lopdf::{dictionary, Document, Object, Stream};
4959 let mut doc = Document::with_version("1.4");
4960 let xdp_bytes = xdp.as_bytes().to_vec();
4961 let xfa_stream = Stream::new(
4962 dictionary! { "Length" => Object::Integer(xdp_bytes.len() as i64) },
4963 xdp_bytes,
4964 );
4965 let xfa_id = doc.add_object(Object::Stream(xfa_stream));
4966 let pages_id = doc.new_object_id();
4967 let content_stream = Stream::new(
4968 dictionary! { "Length" => Object::Integer(page_content.len() as i64) },
4969 page_content,
4970 );
4971 let content_id = doc.add_object(Object::Stream(content_stream));
4972 let page_id = doc.add_object(Object::Dictionary(dictionary! {
4973 "Type" => Object::Name(b"Page".to_vec()),
4974 "Parent" => Object::Reference(pages_id),
4975 "MediaBox" => Object::Array(vec![
4976 Object::Integer(0), Object::Integer(0),
4977 Object::Integer(612), Object::Integer(792),
4978 ]),
4979 "Contents" => Object::Reference(content_id)
4980 }));
4981 doc.objects.insert(
4982 pages_id,
4983 Object::Dictionary(dictionary! {
4984 "Type" => Object::Name(b"Pages".to_vec()),
4985 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
4986 "Count" => Object::Integer(1)
4987 }),
4988 );
4989 let acroform_id = doc.add_object(Object::Dictionary(dictionary! {
4990 "XFA" => Object::Reference(xfa_id),
4991 "Fields" => Object::Array(vec![])
4992 }));
4993 let catalog_id = doc.add_object(Object::Dictionary(dictionary! {
4994 "Type" => Object::Name(b"Catalog".to_vec()),
4995 "Pages" => Object::Reference(pages_id),
4996 "AcroForm" => Object::Reference(acroform_id)
4997 }));
4998 doc.trailer.set("Root", Object::Reference(catalog_id));
4999 let mut out = Vec::new();
5000 doc.save_to(&mut out).unwrap();
5001 out
5002 }
5003
5004 fn build_xfa_pdf(xdp: &str) -> Vec<u8> {
5005 build_xfa_pdf_with_content(xdp, Vec::new())
5006 }
5007
5008 #[test]
5015 fn flatten_line_draw_renders_spanning_rule() {
5016 const LINE_XDP: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
5017<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
5018<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
5019 <subform name="form1" layout="paginate">
5020 <pageSet>
5021 <pageArea name="Page1">
5022 <contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
5023 <medium stock="default" short="8.5in" long="11in"/>
5024 </pageArea>
5025 </pageSet>
5026 <subform name="grid" layout="positioned" w="7.5in" h="10in">
5027 <draw name="FFLine1" w="100mm" h="0mm" x="10mm" y="50mm">
5028 <value>
5029 <line hand="right">
5030 <edge cap="butt" thickness="0.5mm"/>
5031 </line>
5032 </value>
5033 <ui><defaultUi/></ui>
5034 </draw>
5035 </subform>
5036 </subform>
5037</template>
5038</xdp:xdp>"#;
5039 let pdf = build_xfa_pdf(LINE_XDP);
5040 let out = flatten_xfa_to_pdf(&pdf).expect("flatten line");
5041 let _ = extract_text_from_pdf_bytes(&out); let doc = Document::load_mem(&out).expect("load flattened line PDF");
5044 let mut all_ops = String::new();
5045 for page_id in doc.page_iter() {
5046 if let Ok(page_dict) = doc.get_dictionary(page_id) {
5047 if let Ok(Object::Reference(sid)) = page_dict.get(b"Contents") {
5048 if let Ok(stream) = doc.get_object(*sid).and_then(|o| o.as_stream().cloned()) {
5049 let bytes = stream.decompressed_content().unwrap_or(stream.content);
5050 all_ops.push_str(&String::from_utf8_lossy(&bytes));
5051 }
5052 }
5053 }
5054 }
5055 let mut max_span = 0.0_f64;
5059 let mut last_mx: Option<f64> = None;
5060 for line in all_ops.lines() {
5061 let parts: Vec<&str> = line.split_whitespace().collect();
5062 if parts.len() == 3 && parts[2] == "m" {
5063 last_mx = parts[0].parse::<f64>().ok();
5064 } else if parts.len() == 3 && parts[2] == "l" {
5065 if let (Some(mx), Ok(ex)) = (last_mx, parts[0].parse::<f64>()) {
5066 max_span = max_span.max((ex - mx).abs());
5067 }
5068 }
5069 }
5070 assert!(
5071 max_span > 100.0,
5072 "grid rule must span its box (~283pt), got max horizontal span {max_span:.1}:\n{all_ops}"
5073 );
5074 assert!(
5075 all_ops.contains(" w\n"),
5076 "line must stroke at the authored edge weight:\n{all_ops}"
5077 );
5078 }
5079
5080 fn build_xfa_doc_with_xfa_array() -> (Document, ObjectId, Vec<ObjectId>) {
5081 use lopdf::{dictionary, Document, Object, Stream};
5082
5083 let mut doc = Document::with_version("1.4");
5084 let pages_id = doc.new_object_id();
5085 let content_id = doc.add_object(Object::Stream(Stream::new(
5086 dictionary! { "Length" => Object::Integer(0) },
5087 Vec::new(),
5088 )));
5089 let page_id = doc.add_object(Object::Dictionary(dictionary! {
5090 "Type" => Object::Name(b"Page".to_vec()),
5091 "Parent" => Object::Reference(pages_id),
5092 "MediaBox" => Object::Array(vec![
5093 Object::Integer(0), Object::Integer(0),
5094 Object::Integer(612), Object::Integer(792),
5095 ]),
5096 "Contents" => Object::Reference(content_id)
5097 }));
5098 doc.objects.insert(
5099 pages_id,
5100 Object::Dictionary(dictionary! {
5101 "Type" => Object::Name(b"Pages".to_vec()),
5102 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
5103 "Count" => Object::Integer(1)
5104 }),
5105 );
5106
5107 let packet_payloads = [
5108 (
5109 b"xdp:xdp".to_vec(),
5110 br#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"></xdp:xdp>"#.to_vec(),
5111 ),
5112 (
5113 b"template".to_vec(),
5114 br#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform/></template>"#
5115 .to_vec(),
5116 ),
5117 (
5118 b"datasets".to_vec(),
5119 br#"<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"></xfa:datasets>"#
5120 .to_vec(),
5121 ),
5122 ];
5123
5124 let mut xfa_array = Vec::new();
5125 let mut xfa_ids = Vec::new();
5126 for (packet_name, payload) in packet_payloads {
5127 let stream_id = doc.add_object(Object::Stream(Stream::new(
5128 dictionary! { "Length" => Object::Integer(payload.len() as i64) },
5129 payload,
5130 )));
5131 xfa_array.push(Object::Name(packet_name));
5132 xfa_array.push(Object::Reference(stream_id));
5133 xfa_ids.push(stream_id);
5134 }
5135
5136 let acroform_id = doc.add_object(Object::Dictionary(dictionary! {
5137 "XFA" => Object::Array(xfa_array),
5138 "Fields" => Object::Array(vec![])
5139 }));
5140 let catalog_id = doc.add_object(Object::Dictionary(dictionary! {
5141 "Type" => Object::Name(b"Catalog".to_vec()),
5142 "Pages" => Object::Reference(pages_id),
5143 "AcroForm" => Object::Reference(acroform_id)
5144 }));
5145 doc.trailer.set("Root", Object::Reference(catalog_id));
5146 (doc, acroform_id, xfa_ids)
5147 }
5148
5149 fn build_xfa_pdf_with_widget_appearance(
5150 page_content: Vec<u8>,
5151 normal_appearance: Object,
5152 widget_extra: Dictionary,
5153 ) -> Vec<u8> {
5154 use lopdf::{dictionary, Document, Object, Stream};
5155
5156 let mut doc = Document::with_version("1.4");
5157 let xdp_bytes = SIMPLE_XDP.as_bytes().to_vec();
5158 let xfa_stream = Stream::new(
5159 dictionary! { "Length" => Object::Integer(xdp_bytes.len() as i64) },
5160 xdp_bytes,
5161 );
5162 let xfa_id = doc.add_object(Object::Stream(xfa_stream));
5163
5164 let pages_id = doc.new_object_id();
5165 let content_id = doc.add_object(Object::Stream(Stream::new(
5166 dictionary! { "Length" => Object::Integer(page_content.len() as i64) },
5167 page_content,
5168 )));
5169
5170 let appearance_id = match normal_appearance {
5171 Object::Reference(id) => id,
5172 other => doc.add_object(other),
5173 };
5174
5175 let widget_id = doc.new_object_id();
5176 let page_id = doc.add_object(Object::Dictionary(dictionary! {
5177 "Type" => Object::Name(b"Page".to_vec()),
5178 "Parent" => Object::Reference(pages_id),
5179 "MediaBox" => Object::Array(vec![
5180 Object::Integer(0), Object::Integer(0),
5181 Object::Integer(612), Object::Integer(792),
5182 ]),
5183 "Contents" => Object::Reference(content_id),
5184 "Annots" => Object::Array(vec![Object::Reference(widget_id)]),
5185 "Resources" => Object::Dictionary(dictionary! {})
5186 }));
5187
5188 let mut widget = dictionary! {
5189 "Type" => Object::Name(b"Annot".to_vec()),
5190 "Subtype" => Object::Name(b"Widget".to_vec()),
5191 "Rect" => Object::Array(vec![
5192 Object::Integer(100), Object::Integer(700),
5193 Object::Integer(220), Object::Integer(730),
5194 ]),
5195 "AP" => Object::Dictionary(dictionary! {
5196 "N" => Object::Reference(appearance_id)
5197 }),
5198 "P" => Object::Reference(page_id)
5199 };
5200 for (key, value) in widget_extra {
5201 widget.set(key, value);
5202 }
5203 doc.objects.insert(widget_id, Object::Dictionary(widget));
5204
5205 doc.objects.insert(
5206 pages_id,
5207 Object::Dictionary(dictionary! {
5208 "Type" => Object::Name(b"Pages".to_vec()),
5209 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
5210 "Count" => Object::Integer(1)
5211 }),
5212 );
5213
5214 let acroform_id = doc.add_object(Object::Dictionary(dictionary! {
5215 "XFA" => Object::Reference(xfa_id),
5216 "Fields" => Object::Array(vec![Object::Reference(widget_id)])
5217 }));
5218 let catalog_id = doc.add_object(Object::Dictionary(dictionary! {
5219 "Type" => Object::Name(b"Catalog".to_vec()),
5220 "Pages" => Object::Reference(pages_id),
5221 "AcroForm" => Object::Reference(acroform_id)
5222 }));
5223 doc.trailer.set("Root", Object::Reference(catalog_id));
5224
5225 let mut out = Vec::new();
5226 doc.save_to(&mut out).unwrap();
5227 out
5228 }
5229
5230 #[allow(dead_code)]
5231 fn find_last_content_stream(doc: &Document, page_id: ObjectId) -> &Stream {
5232 let page_dict = doc.get_dictionary(page_id).expect("page dict");
5233 match page_dict.get(b"Contents").expect("contents") {
5234 Object::Reference(id) => doc
5235 .get_object(*id)
5236 .expect("contents object")
5237 .as_stream()
5238 .expect("contents stream"),
5239 Object::Array(arr) => {
5240 let last = arr.last().expect("last content stream");
5241 let id = last.as_reference().expect("contents ref");
5242 doc.get_object(id)
5243 .expect("contents object")
5244 .as_stream()
5245 .expect("contents stream")
5246 }
5247 other => other.as_stream().expect("contents stream"),
5248 }
5249 }
5250
5251 #[allow(dead_code)]
5252 fn page_xobjects(doc: &Document, page_id: ObjectId) -> Dictionary {
5253 let page_dict = doc.get_dictionary(page_id).expect("page dict");
5254 let resources = page_dict
5255 .get(b"Resources")
5256 .expect("resources")
5257 .as_dict()
5258 .expect("resources dict");
5259 resources
5260 .get(b"XObject")
5261 .expect("xobjects")
5262 .as_dict()
5263 .expect("xobject dict")
5264 .clone()
5265 }
5266
5267 #[test]
5268 fn append_to_page_content_flattens_indirect_contents_arrays() {
5269 let mut doc = Document::with_version("1.4");
5270 let pages_id = doc.new_object_id();
5271 let first_stream_id = doc.add_object(Stream::new(dictionary! {}, b"q\n".to_vec()));
5272 let second_stream_id = doc.add_object(Stream::new(dictionary! {}, b"Q\n".to_vec()));
5273 let contents_array_id = doc.add_object(Object::Array(vec![
5274 Object::Reference(first_stream_id),
5275 Object::Reference(second_stream_id),
5276 ]));
5277 let page_id = doc.add_object(Object::Dictionary(dictionary! {
5278 "Type" => Object::Name(b"Page".to_vec()),
5279 "Parent" => Object::Reference(pages_id),
5280 "MediaBox" => Object::Array(vec![
5281 Object::Integer(0), Object::Integer(0),
5282 Object::Integer(612), Object::Integer(792),
5283 ]),
5284 "Contents" => Object::Reference(contents_array_id),
5285 }));
5286 doc.objects.insert(
5287 pages_id,
5288 Object::Dictionary(dictionary! {
5289 "Type" => Object::Name(b"Pages".to_vec()),
5290 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
5291 "Count" => Object::Integer(1),
5292 }),
5293 );
5294
5295 append_to_page_content(&mut doc, page_id, b"BT\nET\n");
5296
5297 let page_dict = doc.get_dictionary(page_id).expect("page dict");
5298 let contents = page_dict.get(b"Contents").expect("contents");
5299 let items = contents.as_array().expect("flattened contents array");
5300
5301 assert_eq!(items.len(), 3, "existing streams + appended stream");
5302 assert!(
5303 items.iter().all(|obj| obj.as_reference().is_ok()),
5304 "contents array must stay flat and reference only streams"
5305 );
5306 for object in items {
5307 let stream_id = object.as_reference().expect("stream ref");
5308 assert!(
5309 doc.get_object(stream_id)
5310 .expect("stream object")
5311 .as_stream()
5312 .is_ok(),
5313 "nested arrays must not survive in page contents"
5314 );
5315 }
5316 }
5317
5318 const SIMPLE_XDP: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
5319<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
5320<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
5321 <subform name="form1" layout="paginate">
5322 <pageSet>
5323 <pageArea name="Page1">
5324 <contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
5325 <medium stock="default" short="8.5in" long="11in"/>
5326 </pageArea>
5327 </pageSet>
5328 <subform name="section" layout="tb" w="7.5in">
5329 <field name="firstName" w="3.5in" h="0.3in">
5330 <caption><value><text>First Name</text></value></caption>
5331 <ui><textEdit/></ui>
5332 <value><text>John</text></value>
5333 </field>
5334 </subform>
5335 </subform>
5336</template>
5337</xdp:xdp>"#;
5338
5339 const JS_EVENT_XDP: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
5340<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
5341<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
5342 <subform name="form1" layout="paginate">
5343 <pageSet>
5344 <pageArea name="Page1">
5345 <contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
5346 <medium stock="default" short="8.5in" long="11in"/>
5347 </pageArea>
5348 </pageSet>
5349 <subform name="section" layout="tb" w="7.5in">
5350 <event activity="initialize">
5351 <script contentType="application/x-javascript">app.alert('blocked');</script>
5352 </event>
5353 <field name="firstName" w="3.5in" h="0.3in">
5354 <caption><value><text>First Name</text></value></caption>
5355 <ui><textEdit/></ui>
5356 <value><text>John</text></value>
5357 </field>
5358 </subform>
5359 </subform>
5360</template>
5361</xdp:xdp>"#;
5362
5363 fn overflowing_paginate_xdp(base_profile: Option<&str>) -> String {
5364 let mut fields = String::new();
5365 for i in 0..40 {
5366 fields.push_str(&format!(
5367 r#"
5368 <field name="line{i}" w="7.0in" h="0.3in">
5369 <ui><textEdit/></ui>
5370 <value><text>Line {i}</text></value>
5371 </field>"#
5372 ));
5373 }
5374
5375 let base_profile_attr = base_profile
5376 .map(|value| format!(r#" baseProfile="{value}""#))
5377 .unwrap_or_default();
5378
5379 format!(
5380 r#"<?xml version="1.0" encoding="UTF-8"?>
5381<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
5382<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"{base_profile_attr}>
5383 <subform name="form1" layout="paginate">
5384 <pageSet>
5385 <pageArea name="Page1">
5386 <contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
5387 <medium stock="default" short="8.5in" long="11in"/>
5388 </pageArea>
5389 </pageSet>
5390 <subform name="section" layout="tb" w="7.5in">{fields}
5391 </subform>
5392 </subform>
5393</template>
5394</xdp:xdp>"#
5395 )
5396 }
5397
5398 #[test]
5399 fn flatten_simple_form_produces_non_empty_content() {
5400 let pdf_bytes = build_xfa_pdf(SIMPLE_XDP);
5401 let result = flatten_xfa_to_pdf(&pdf_bytes).expect("flatten failed");
5402
5403 let doc = Document::load_mem(&result).expect("load flattened PDF");
5405 let pages: Vec<ObjectId> = doc.page_iter().collect();
5406 assert!(!pages.is_empty(), "flattened PDF has no pages");
5407
5408 let mut found_content = false;
5410 for page_id in &pages {
5411 if let Ok(page_dict) = doc.get_dictionary(*page_id) {
5412 if let Ok(Object::Reference(stream_id)) = page_dict.get(b"Contents") {
5413 if let Ok(obj) = doc.get_object(*stream_id) {
5414 if let Ok(stream) = obj.as_stream() {
5415 if !stream.content.is_empty() {
5416 found_content = true;
5417 }
5418 }
5419 }
5420 }
5421 }
5422 }
5423 assert!(found_content, "all content streams are empty after flatten");
5424 }
5425
5426 #[test]
5427 fn flatten_reports_best_effort_for_xfa_javascript_event() {
5428 let pdf_bytes = build_xfa_pdf(JS_EVENT_XDP);
5429
5430 let (flattened, metadata) =
5431 flatten_xfa_to_pdf_with_metadata(&pdf_bytes).expect("flatten should skip JS");
5432
5433 assert!(!flattened.is_empty());
5434 assert_eq!(metadata.output_quality, OutputQuality::BestEffort);
5435 assert!(metadata.dynamic_scripts.js_present);
5436 assert_eq!(metadata.dynamic_scripts.js_skipped, 1);
5437 }
5438
5439 #[test]
5440 fn flatten_strips_catalog_open_action_javascript() {
5441 let mut pdf_bytes = build_xfa_pdf(SIMPLE_XDP);
5442 {
5443 let mut doc = Document::load_mem(&pdf_bytes).expect("parse test PDF");
5444 let root_id = match doc.trailer.get(b"Root") {
5445 Ok(Object::Reference(id)) => *id,
5446 _ => panic!("no Root in test PDF"),
5447 };
5448 if let Ok(Object::Dictionary(catalog)) = doc.get_object_mut(root_id) {
5449 catalog.set(
5450 "OpenAction",
5451 Object::Dictionary(dictionary! {
5452 "S" => Object::Name(b"JavaScript".to_vec()),
5453 "JS" => Object::String(
5454 b"app.alert('blocked')".to_vec(),
5455 lopdf::StringFormat::Literal,
5456 ),
5457 }),
5458 );
5459 }
5460 let mut out = Vec::new();
5461 doc.save_to(&mut out).expect("save test PDF");
5462 pdf_bytes = out;
5463 }
5464
5465 let flattened = flatten_xfa_to_pdf(&pdf_bytes).expect("flatten failed");
5466 let doc = Document::load_mem(&flattened).expect("load flattened PDF");
5467 let root_id = match doc.trailer.get(b"Root") {
5468 Ok(Object::Reference(id)) => *id,
5469 _ => panic!("no Root in flattened PDF"),
5470 };
5471 let catalog = doc.get_dictionary(root_id).expect("catalog dict");
5472 assert!(
5473 catalog.get(b"OpenAction").is_err(),
5474 "/OpenAction JavaScript must be stripped from flattened output"
5475 );
5476 }
5477
5478 #[test]
5484 fn flatten_paginate_subform_with_nested_pageset_produces_visible_content() {
5485 const LR_TB_XDP: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
5486<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
5487<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
5488 <subform name="form1" layout="paginate" locale="en_US">
5489 <pageSet>
5490 <pageArea name="Page1" id="Page1">
5491 <contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/>
5492 <medium stock="default" short="8.5in" long="11in"/>
5493 </pageArea>
5494 </pageSet>
5495 <subform name="row1" layout="lr-tb" w="7.5in" h="0.4in">
5496 <field name="firstName" w="3.5in" h="0.4in">
5497 <caption><value><text>First</text></value></caption>
5498 <ui><textEdit/></ui>
5499 <value><text>John</text></value>
5500 </field>
5501 <field name="lastName" w="3.5in" h="0.4in">
5502 <caption><value><text>Last</text></value></caption>
5503 <ui><textEdit/></ui>
5504 <value><text>Doe</text></value>
5505 </field>
5506 </subform>
5507 </subform>
5508</template>
5509</xdp:xdp>"#;
5510
5511 let pdf_bytes = build_xfa_pdf(LR_TB_XDP);
5512 let result = flatten_xfa_to_pdf(&pdf_bytes).expect("flatten failed");
5513
5514 let doc = Document::load_mem(&result).expect("load flattened PDF");
5515 let pages: Vec<ObjectId> = doc.page_iter().collect();
5516
5517 assert_eq!(pages.len(), 1, "expected 1 page, got {}", pages.len());
5519
5520 if let Ok(page_dict) = doc.get_dictionary(pages[0]) {
5523 if let Ok(lopdf::Object::Reference(stream_id)) = page_dict.get(b"Contents") {
5524 if let Ok(obj) = doc.get_object(*stream_id) {
5525 if let Ok(stream) = obj.as_stream() {
5526 let content = String::from_utf8_lossy(&stream.content);
5527 assert!(
5528 content.contains("BT\n"),
5529 "no text operators in page 1 content stream (should have BT from field values)"
5530 );
5531 assert!(
5532 content.contains("Tj\n"),
5533 "no text show operators in page 1 content stream"
5534 );
5535 }
5536 }
5537 }
5538 }
5539 }
5540
5541 #[test]
5542 fn static_single_page_pdf_does_not_append_xfa_overflow_pages() {
5543 let xdp = overflowing_paginate_xdp(Some("interactiveForms"));
5544 let pdf_bytes = build_xfa_pdf(&xdp);
5545 let result = flatten_xfa_to_pdf(&pdf_bytes).expect("flatten failed");
5546
5547 let doc = Document::load_mem(&result).expect("load flattened PDF");
5548 let pages: Vec<ObjectId> = doc.page_iter().collect();
5549
5550 assert_eq!(
5551 pages.len(),
5552 1,
5553 "static 1-page PDFs should preserve the original page when XFA layout over-paginates"
5554 );
5555 }
5556
5557 #[test]
5558 fn dynamic_single_page_pdf_can_expand_beyond_original_page_count() {
5559 let xdp = overflowing_paginate_xdp(None);
5564 let pdf_bytes = build_xfa_pdf(&xdp);
5565 let result = flatten_xfa_to_pdf(&pdf_bytes).expect("flatten failed");
5566
5567 let doc = Document::load_mem(&result).expect("load flattened PDF");
5568 let pages: Vec<ObjectId> = doc.page_iter().collect();
5569
5570 assert_eq!(
5571 pages.len(),
5572 2,
5573 "dynamic 1-page PDFs should be allowed to grow when XFA layout paginates"
5574 );
5575 }
5576
5577 #[test]
5578 fn flatten_removes_acroform() {
5579 let pdf_bytes = build_xfa_pdf(SIMPLE_XDP);
5580 let result = flatten_xfa_to_pdf(&pdf_bytes).expect("flatten failed");
5581 let doc = Document::load_mem(&result).expect("load flattened PDF");
5582 let root_id = doc.trailer.get(b"Root").unwrap().as_reference().unwrap();
5583 let catalog = doc.get_dictionary(root_id).unwrap();
5584 assert!(
5585 catalog.get(b"AcroForm").is_err(),
5586 "/AcroForm still present after flatten"
5587 );
5588 }
5589
5590 #[test]
5591 fn flatten_non_xfa_pdf_unchanged() {
5592 let mut doc = Document::with_version("1.4");
5594 let pages_id = doc.new_object_id();
5595 let page_id = doc.add_object(Object::Dictionary(dictionary! {
5596 "Type" => Object::Name(b"Page".to_vec()),
5597 "Parent" => Object::Reference(pages_id),
5598 "MediaBox" => Object::Array(vec![
5599 Object::Integer(0), Object::Integer(0),
5600 Object::Integer(612), Object::Integer(792),
5601 ])
5602 }));
5603 doc.objects.insert(
5604 pages_id,
5605 Object::Dictionary(dictionary! {
5606 "Type" => Object::Name(b"Pages".to_vec()),
5607 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
5608 "Count" => Object::Integer(1)
5609 }),
5610 );
5611 let catalog_id = doc.add_object(Object::Dictionary(dictionary! {
5612 "Type" => Object::Name(b"Catalog".to_vec()),
5613 "Pages" => Object::Reference(pages_id)
5614 }));
5615 doc.trailer.set("Root", Object::Reference(catalog_id));
5616 let mut raw = Vec::new();
5617 doc.save_to(&mut raw).unwrap();
5618
5619 let result = flatten_xfa_to_pdf(&raw).expect("flatten non-XFA failed");
5621 assert!(!result.is_empty());
5622 }
5623
5624 #[test]
5625 fn placeholder_only_page_does_not_trigger_static_strip_path() {
5626 const PLACEHOLDER_STREAM: &str = r#"BT
5627/Helv 24 Tf
562872 720 Td
5629(Please wait...) Tj
56300 -32 Td
5631(If this message is not eventually replaced by the proper contents of the document,) Tj
56320 -32 Td
5633(your PDF viewer may not be able to display this type of document.) Tj
56340 -32 Td
5635(You can upgrade to the latest version of Adobe Reader by visiting reader_download.) Tj
5636ET
5637"#;
5638
5639 let pdf_bytes =
5640 build_xfa_pdf_with_content(SIMPLE_XDP, PLACEHOLDER_STREAM.as_bytes().to_vec());
5641 let result = flatten_xfa_to_pdf(&pdf_bytes).expect("flatten failed");
5642
5643 let doc = Document::load_mem(&result).expect("load flattened PDF");
5644 let page_id = doc.page_iter().next().expect("flattened page");
5645 let page_dict = doc.get_dictionary(page_id).expect("page dict");
5646 let contents_id = page_dict
5647 .get(b"Contents")
5648 .ok()
5649 .and_then(|object| object.as_reference().ok())
5650 .expect("contents ref");
5651 let stream = doc
5652 .get_object(contents_id)
5653 .expect("contents object")
5654 .as_stream()
5655 .expect("contents stream");
5656 let content = String::from_utf8_lossy(&stream.content);
5657
5658 assert!(
5659 content.contains("John"),
5660 "flattened page should contain XFA-rendered field content"
5661 );
5662 assert!(
5663 !content.contains("Please wait"),
5664 "placeholder text should not survive XFA flattening"
5665 );
5666 }
5667
5668 #[test]
5669 fn hybrid_static_pdf_uses_xfa_layout_over_static_content() {
5670 let appearance = Object::Stream(Stream::new(
5674 dictionary! {
5675 "Type" => Object::Name(b"XObject".to_vec()),
5676 "Subtype" => Object::Name(b"Form".to_vec()),
5677 "BBox" => Object::Array(vec![
5678 Object::Integer(0), Object::Integer(0),
5679 Object::Integer(120), Object::Integer(30),
5680 ]),
5681 "Matrix" => Object::Array(vec![
5682 Object::Integer(1), Object::Integer(0),
5683 Object::Integer(0), Object::Integer(1),
5684 Object::Integer(0), Object::Integer(0),
5685 ]),
5686 "Resources" => Object::Dictionary(dictionary! {}),
5687 },
5688 b"0 G\n0.5 0.5 119 29 re\ns\n".to_vec(),
5689 ));
5690 let page_content = b"BT /F1 12 Tf 72 720 Td (Line 1) Tj 0 -14 Td (Line 2) Tj 0 -14 Td (Line 3) Tj 0 -14 Td (Line 4) Tj 0 -14 Td (Line 5) Tj ET\n".to_vec();
5692 let pdf_bytes = build_xfa_pdf_with_widget_appearance(
5693 page_content,
5694 appearance,
5695 dictionary! {
5696 "FT" => Object::Name(b"Tx".to_vec()),
5697 "T" => Object::string_literal("field[0]"),
5698 },
5699 );
5700
5701 let result = flatten_xfa_to_pdf(&pdf_bytes).expect("flatten failed");
5702 let doc = Document::load_mem(&result).expect("load flattened PDF");
5703 let page_id = doc.page_iter().next().expect("page");
5704 let page_dict = doc.get_dictionary(page_id).expect("page dict");
5705
5706 assert!(
5708 page_dict.get(b"Annots").is_err(),
5709 "XFA-flattened page should have no annotations"
5710 );
5711 }
5712
5713 #[test]
5714 fn hybrid_static_pdf_uses_selected_button_appearance_state() {
5715 let yes_stream = Object::Stream(Stream::new(
5716 dictionary! {
5717 "Type" => Object::Name(b"XObject".to_vec()),
5718 "Subtype" => Object::Name(b"Form".to_vec()),
5719 "BBox" => Object::Array(vec![
5720 Object::Integer(0), Object::Integer(0),
5721 Object::Integer(20), Object::Integer(20),
5722 ]),
5723 "Matrix" => Object::Array(vec![
5724 Object::Integer(1), Object::Integer(0),
5725 Object::Integer(0), Object::Integer(1),
5726 Object::Integer(0), Object::Integer(0),
5727 ]),
5728 "Resources" => Object::Dictionary(dictionary! {}),
5729 },
5730 b"BT /F1 8 Tf 1 1 Td (YES) Tj ET\n".to_vec(),
5731 ));
5732 let off_stream = Object::Stream(Stream::new(
5733 dictionary! {
5734 "Type" => Object::Name(b"XObject".to_vec()),
5735 "Subtype" => Object::Name(b"Form".to_vec()),
5736 "BBox" => Object::Array(vec![
5737 Object::Integer(0), Object::Integer(0),
5738 Object::Integer(20), Object::Integer(20),
5739 ]),
5740 "Matrix" => Object::Array(vec![
5741 Object::Integer(1), Object::Integer(0),
5742 Object::Integer(0), Object::Integer(1),
5743 Object::Integer(0), Object::Integer(0),
5744 ]),
5745 "Resources" => Object::Dictionary(dictionary! {}),
5746 },
5747 b"BT /F1 8 Tf 1 1 Td (OFF) Tj ET\n".to_vec(),
5748 ));
5749
5750 let mut doc = Document::with_version("1.4");
5751 let state_id = doc.add_object(Object::Dictionary(dictionary! {
5752 "Yes" => yes_stream,
5753 "Off" => off_stream,
5754 }));
5755 let annot = dictionary! {
5756 "Subtype" => Object::Name(b"Widget".to_vec()),
5757 "Rect" => Object::Array(vec![
5758 Object::Integer(100), Object::Integer(700),
5759 Object::Integer(120), Object::Integer(720),
5760 ]),
5761 "AP" => Object::Dictionary(dictionary! {
5762 "N" => Object::Reference(state_id),
5763 }),
5764 "AS" => Object::Name(b"Yes".to_vec()),
5765 "FT" => Object::Name(b"Btn".to_vec()),
5766 };
5767 let ap_id =
5768 resolve_widget_normal_appearance(&mut doc, &annot).expect("selected normal appearance");
5769 let stream = doc
5770 .get_object(ap_id)
5771 .expect("appearance stream")
5772 .as_stream()
5773 .expect("appearance stream");
5774 let content = String::from_utf8_lossy(&stream.content);
5775
5776 assert!(
5777 content.contains("YES"),
5778 "flatten should choose the selected normal appearance state"
5779 );
5780 }
5781
5782 #[test]
5783 fn widget_as_off_without_off_appearance_returns_none() {
5784 let yes_stream = Object::Stream(Stream::new(
5788 dictionary! {
5789 "Type" => Object::Name(b"XObject".to_vec()),
5790 "Subtype" => Object::Name(b"Form".to_vec()),
5791 "BBox" => Object::Array(vec![
5792 Object::Integer(0), Object::Integer(0),
5793 Object::Integer(10), Object::Integer(10),
5794 ]),
5795 },
5796 b"q 5 5 m 5 5 l S Q\n".to_vec(),
5797 ));
5798
5799 let mut doc = Document::with_version("1.4");
5800 let state_id = doc.add_object(Object::Dictionary(dictionary! {
5802 "0" => yes_stream,
5803 }));
5804 let annot = dictionary! {
5805 "Subtype" => Object::Name(b"Widget".to_vec()),
5806 "Rect" => Object::Array(vec![
5807 Object::Integer(100), Object::Integer(700),
5808 Object::Integer(110), Object::Integer(710),
5809 ]),
5810 "AP" => Object::Dictionary(dictionary! {
5811 "N" => Object::Reference(state_id),
5812 }),
5813 "AS" => Object::Name(b"Off".to_vec()),
5814 "FT" => Object::Name(b"Btn".to_vec()),
5815 };
5816 assert!(
5817 resolve_widget_normal_appearance(&mut doc, &annot).is_none(),
5818 "Off state with no Off appearance should not resolve to the on-state stream"
5819 );
5820 }
5821
5822 #[test]
5823 fn bake_checkbox_radio_ap_marks_skips_off_widgets_without_off_normal_appearance() {
5824 let pdf_bytes = build_xfa_pdf_with_widget_appearance(
5825 Vec::new(),
5826 Object::Dictionary(dictionary! {
5827 "1" => Object::Stream(Stream::new(
5828 dictionary! {
5829 "Type" => Object::Name(b"XObject".to_vec()),
5830 "Subtype" => Object::Name(b"Form".to_vec()),
5831 "BBox" => Object::Array(vec![
5832 Object::Integer(0), Object::Integer(0),
5833 Object::Integer(10), Object::Integer(10),
5834 ]),
5835 "Resources" => Object::Dictionary(dictionary! {}),
5836 },
5837 b"q 1 1 8 8 re W n 2 8 m 8 2 l 8 8 m 2 2 l s Q\n".to_vec(),
5838 )),
5839 }),
5840 dictionary! {
5841 "FT" => Object::Name(b"Btn".to_vec()),
5842 "AS" => Object::Name(b"Off".to_vec()),
5843 "T" => Object::string_literal("checkbox[0]"),
5844 },
5845 );
5846
5847 let mut doc = Document::load_mem(&pdf_bytes).expect("parse test PDF");
5848 let page_id = doc.page_iter().next().expect("page");
5849 let baked = bake_checkbox_radio_ap_marks(&mut doc, page_id);
5850
5851 assert_eq!(baked, 0, "Off-state widget must not stamp the on-mark");
5852 }
5853
5854 #[test]
5855 fn adding_widget_xobject_preserves_indirect_inline_page_xobjects() {
5856 let mut doc = Document::with_version("1.4");
5857 let existing_xobject_id = doc.add_object(Object::Stream(Stream::new(
5858 dictionary! {
5859 "Type" => Object::Name(b"XObject".to_vec()),
5860 "Subtype" => Object::Name(b"Form".to_vec()),
5861 "BBox" => Object::Array(vec![
5862 Object::Integer(0), Object::Integer(0),
5863 Object::Integer(10), Object::Integer(10),
5864 ]),
5865 },
5866 b"q Q\n".to_vec(),
5867 )));
5868 let xobject_dict_id = doc.add_object(Object::Dictionary(dictionary! {
5869 "R11" => Object::Reference(existing_xobject_id),
5870 }));
5871
5872 let pages_id = doc.new_object_id();
5873 let page_id = doc.add_object(Object::Dictionary(dictionary! {
5874 "Type" => Object::Name(b"Page".to_vec()),
5875 "Parent" => Object::Reference(pages_id),
5876 "MediaBox" => Object::Array(vec![
5877 Object::Integer(0), Object::Integer(0),
5878 Object::Integer(612), Object::Integer(792),
5879 ]),
5880 "Resources" => Object::Dictionary(dictionary! {
5881 "XObject" => Object::Reference(xobject_dict_id),
5882 }),
5883 }));
5884 doc.objects.insert(
5885 pages_id,
5886 Object::Dictionary(dictionary! {
5887 "Type" => Object::Name(b"Pages".to_vec()),
5888 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
5889 "Count" => Object::Integer(1)
5890 }),
5891 );
5892
5893 let new_xobject_id = doc.add_object(Object::Stream(Stream::new(
5894 dictionary! {
5895 "Type" => Object::Name(b"XObject".to_vec()),
5896 "Subtype" => Object::Name(b"Form".to_vec()),
5897 "BBox" => Object::Array(vec![
5898 Object::Integer(0), Object::Integer(0),
5899 Object::Integer(10), Object::Integer(10),
5900 ]),
5901 },
5902 b"0 0 10 10 re S\n".to_vec(),
5903 )));
5904
5905 add_xobject_to_page_resources(&mut doc, page_id, "XfaAp0", new_xobject_id);
5906
5907 let xobjects = doc
5908 .get_object(xobject_dict_id)
5909 .expect("xobject dict")
5910 .as_dict()
5911 .expect("xobject dict");
5912 assert!(
5913 xobjects.get(b"R11").is_ok(),
5914 "existing page XObject was lost"
5915 );
5916 assert!(
5917 xobjects.get(b"XfaAp0").is_ok(),
5918 "new flattened widget XObject was not added"
5919 );
5920 }
5921
5922 #[test]
5923 fn encrypted_pdf_without_xfa_returns_ok() {
5924 let mut doc = Document::with_version("1.4");
5926 let pages_id = doc.new_object_id();
5927 let page_id = doc.add_object(Object::Dictionary(dictionary! {
5928 "Type" => Object::Name(b"Page".to_vec()),
5929 "Parent" => Object::Reference(pages_id),
5930 "MediaBox" => Object::Array(vec![
5931 Object::Integer(0), Object::Integer(0),
5932 Object::Integer(612), Object::Integer(792),
5933 ]),
5934 }));
5935 doc.objects.insert(
5936 pages_id,
5937 Object::Dictionary(dictionary! {
5938 "Type" => Object::Name(b"Pages".to_vec()),
5939 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
5940 "Count" => Object::Integer(1),
5941 }),
5942 );
5943 let catalog_id = doc.add_object(Object::Dictionary(dictionary! {
5944 "Type" => Object::Name(b"Catalog".to_vec()),
5945 "Pages" => Object::Reference(pages_id),
5946 }));
5947 doc.trailer.set("Root", Object::Reference(catalog_id));
5948
5949 let encrypt_id = doc.add_object(Object::Dictionary(dictionary! {
5950 "Filter" => Object::Name(b"Standard".to_vec()),
5951 "V" => Object::Integer(2),
5952 "Length" => Object::Integer(128),
5953 }));
5954 doc.trailer.set("Encrypt", Object::Reference(encrypt_id));
5955
5956 let mut buf = Vec::new();
5957 doc.save_to(&mut buf).expect("save test PDF");
5958
5959 let result = flatten_xfa_to_pdf(&buf);
5960 assert!(result.is_ok(), "non-XFA encrypted PDF should return Ok");
5961 }
5962
5963 #[test]
5964 fn encrypted_xfa_pdf_returns_encrypted_error() {
5965 let mut doc = Document::with_version("1.4");
5968 let pages_id = doc.new_object_id();
5969 let page_id = doc.add_object(Object::Dictionary(dictionary! {
5970 "Type" => Object::Name(b"Page".to_vec()),
5971 "Parent" => Object::Reference(pages_id),
5972 "MediaBox" => Object::Array(vec![
5973 Object::Integer(0), Object::Integer(0),
5974 Object::Integer(612), Object::Integer(792),
5975 ]),
5976 }));
5977 doc.objects.insert(
5978 pages_id,
5979 Object::Dictionary(dictionary! {
5980 "Type" => Object::Name(b"Pages".to_vec()),
5981 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
5982 "Count" => Object::Integer(1),
5983 }),
5984 );
5985 let xfa_stream_id = doc.add_object(Object::Stream(lopdf::Stream::new(
5987 dictionary! {},
5988 b"<xdp:xdp></xdp:xdp>".to_vec(),
5989 )));
5990 let acroform_id = doc.add_object(Object::Dictionary(dictionary! {
5991 "XFA" => Object::Reference(xfa_stream_id),
5992 }));
5993 let catalog_id = doc.add_object(Object::Dictionary(dictionary! {
5994 "Type" => Object::Name(b"Catalog".to_vec()),
5995 "Pages" => Object::Reference(pages_id),
5996 "AcroForm" => Object::Reference(acroform_id),
5997 }));
5998 doc.trailer.set("Root", Object::Reference(catalog_id));
5999
6000 let encrypt_id = doc.add_object(Object::Dictionary(dictionary! {
6001 "Filter" => Object::Name(b"Standard".to_vec()),
6002 "V" => Object::Integer(2),
6003 "Length" => Object::Integer(128),
6004 }));
6005 doc.trailer.set("Encrypt", Object::Reference(encrypt_id));
6006
6007 let mut buf = Vec::new();
6008 doc.save_to(&mut buf).expect("save encrypted PDF");
6009
6010 let result = flatten_xfa_to_pdf(&buf);
6011 assert!(result.is_err(), "expected Encrypted error");
6012 let err = result.unwrap_err();
6013 assert!(
6014 matches!(err, XfaError::Encrypted(_)),
6015 "expected XfaError::Encrypted, got: {err:?}"
6016 );
6017 }
6018
6019 #[test]
6020 fn owner_only_encrypted_pdf_is_handled_transparently() {
6021 let mut doc = Document::with_version("2.0");
6024 let pages_id = doc.new_object_id();
6025 let page_id = doc.add_object(Object::Dictionary(dictionary! {
6026 "Type" => Object::Name(b"Page".to_vec()),
6027 "Parent" => Object::Reference(pages_id),
6028 "MediaBox" => Object::Array(vec![
6029 Object::Integer(0), Object::Integer(0),
6030 Object::Integer(612), Object::Integer(792),
6031 ]),
6032 }));
6033 doc.objects.insert(
6034 pages_id,
6035 Object::Dictionary(dictionary! {
6036 "Type" => Object::Name(b"Pages".to_vec()),
6037 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
6038 "Count" => Object::Integer(1),
6039 }),
6040 );
6041 let catalog_id = doc.add_object(Object::Dictionary(dictionary! {
6042 "Type" => Object::Name(b"Catalog".to_vec()),
6043 "Pages" => Object::Reference(pages_id),
6044 }));
6045 doc.trailer.set("Root", Object::Reference(catalog_id));
6046
6047 let state = lopdf::aes256_encryption_state("secret", "", lopdf::Permissions::default())
6049 .expect("create encryption state");
6050 doc.encrypt(&state).expect("encrypt document");
6051
6052 let mut buf = Vec::new();
6053 doc.save_to(&mut buf).expect("save encrypted PDF");
6054
6055 assert!(
6057 !is_pdf_encrypted(&buf),
6058 "lopdf should auto-decrypt owner-only PDFs"
6059 );
6060
6061 let result = flatten_xfa_to_pdf(&buf);
6063 assert!(
6064 result.is_ok(),
6065 "owner-only encrypted PDF should be handled, got: {result:?}"
6066 );
6067 }
6068
6069 fn build_pdf_with_cid_font(w_array: Vec<Object>, dw: Option<i64>) -> Document {
6071 let mut doc = Document::with_version("1.4");
6072
6073 let mut cid_dict = dictionary! {
6075 "Type" => Object::Name(b"Font".to_vec()),
6076 "Subtype" => Object::Name(b"CIDFontType2".to_vec()),
6077 "BaseFont" => Object::Name(b"TestFont".to_vec()),
6078 "W" => Object::Array(w_array)
6079 };
6080 if let Some(dw_val) = dw {
6081 cid_dict.set("DW", Object::Integer(dw_val));
6082 }
6083 let cid_id = doc.add_object(Object::Dictionary(cid_dict));
6084
6085 let type0_dict = dictionary! {
6087 "Type" => Object::Name(b"Font".to_vec()),
6088 "Subtype" => Object::Name(b"Type0".to_vec()),
6089 "BaseFont" => Object::Name(b"TestFont".to_vec()),
6090 "DescendantFonts" => Object::Array(vec![Object::Reference(cid_id)])
6091 };
6092 doc.add_object(Object::Dictionary(type0_dict));
6093 doc
6094 }
6095
6096 #[test]
6099 fn cid_w_array_consecutive() {
6100 let w = vec![
6101 Object::Integer(120),
6102 Object::Array(vec![
6103 Object::Integer(500),
6104 Object::Integer(600),
6105 Object::Integer(700),
6106 ]),
6107 ];
6108 let doc = build_pdf_with_cid_font(w, None);
6109 let _fonts = extract_embedded_fonts(&doc);
6110
6111 for obj in doc.objects.values() {
6114 let dict = match obj.as_dict() {
6115 Ok(d) => d,
6116 Err(_) => continue,
6117 };
6118 let subtype = dict.get(b"Subtype").ok().and_then(|o| o.as_name().ok());
6119 if subtype == Some(b"Type0".as_slice()) {
6120 let result = extract_cid_font_widths(&doc, dict);
6121 let (first, widths) = result.expect("should parse /W array");
6122 assert_eq!(first, 120);
6123 assert_eq!(widths.len(), 3);
6124 assert_eq!(widths[0], 500); assert_eq!(widths[1], 600); assert_eq!(widths[2], 700); return;
6128 }
6129 }
6130 panic!("Type0 font not found in test document");
6131 }
6132
6133 #[test]
6136 fn cid_w_array_range() {
6137 let w = vec![
6138 Object::Integer(200),
6139 Object::Integer(300),
6140 Object::Integer(250),
6141 ];
6142 let doc = build_pdf_with_cid_font(w, None);
6143
6144 for obj in doc.objects.values() {
6145 let dict = match obj.as_dict() {
6146 Ok(d) => d,
6147 Err(_) => continue,
6148 };
6149 let subtype = dict.get(b"Subtype").ok().and_then(|o| o.as_name().ok());
6150 if subtype == Some(b"Type0".as_slice()) {
6151 let (first, widths) =
6152 extract_cid_font_widths(&doc, dict).expect("should parse /W range");
6153 assert_eq!(first, 200);
6154 assert_eq!(widths.len(), 101); assert!(widths.iter().all(|&w| w == 250));
6156 return;
6157 }
6158 }
6159 panic!("Type0 font not found");
6160 }
6161
6162 #[test]
6167 fn cid_w_array_mixed() {
6168 let w = vec![
6169 Object::Integer(120),
6170 Object::Array(vec![
6171 Object::Integer(500),
6172 Object::Integer(600),
6173 Object::Integer(700),
6174 ]),
6175 Object::Integer(200),
6176 Object::Integer(300),
6177 Object::Integer(250),
6178 ];
6179 let doc = build_pdf_with_cid_font(w, Some(1000));
6180
6181 for obj in doc.objects.values() {
6182 let dict = match obj.as_dict() {
6183 Ok(d) => d,
6184 Err(_) => continue,
6185 };
6186 let subtype = dict.get(b"Subtype").ok().and_then(|o| o.as_name().ok());
6187 if subtype == Some(b"Type0".as_slice()) {
6188 let (first, widths) =
6189 extract_cid_font_widths(&doc, dict).expect("should parse mixed /W");
6190 assert_eq!(first, 120);
6191 assert_eq!(widths.len(), 181); assert_eq!(widths[0], 500); assert_eq!(widths[1], 600); assert_eq!(widths[2], 700); assert_eq!(widths[3], 1000); assert_eq!(widths[79], 1000); assert_eq!(widths[80], 250); assert_eq!(widths[180], 250); return;
6203 }
6204 }
6205 panic!("Type0 font not found");
6206 }
6207
6208 #[test]
6210 fn cid_w_array_default_width() {
6211 let w = vec![
6212 Object::Integer(10),
6213 Object::Array(vec![Object::Integer(400)]),
6214 Object::Integer(20),
6215 Object::Array(vec![Object::Integer(600)]),
6216 ];
6217 let doc = build_pdf_with_cid_font(w, None); for obj in doc.objects.values() {
6220 let dict = match obj.as_dict() {
6221 Ok(d) => d,
6222 Err(_) => continue,
6223 };
6224 let subtype = dict.get(b"Subtype").ok().and_then(|o| o.as_name().ok());
6225 if subtype == Some(b"Type0".as_slice()) {
6226 let (first, widths) = extract_cid_font_widths(&doc, dict).expect("should parse /W");
6227 assert_eq!(first, 10);
6228 assert_eq!(widths[0], 400); assert_eq!(widths[5], 1000); assert_eq!(widths[10], 600); return;
6232 }
6233 }
6234 panic!("Type0 font not found");
6235 }
6236
6237 #[test]
6238 fn extract_embedded_fonts_keeps_simple_pdf_fonts_without_fontfile() {
6239 let mut doc = Document::new();
6240 let font_id = doc.add_object(Object::Dictionary(dictionary! {
6241 "Type" => Object::Name(b"Font".to_vec()),
6242 "Subtype" => Object::Name(b"Type1".to_vec()),
6243 "BaseFont" => Object::Name(b"MyriadPro-Regular".to_vec()),
6244 "FirstChar" => Object::Integer(32),
6245 "LastChar" => Object::Integer(34),
6246 "Widths" => Object::Array(vec![
6247 Object::Integer(278),
6248 Object::Integer(333),
6249 Object::Integer(612),
6250 ]),
6251 "Encoding" => Object::Name(b"WinAnsiEncoding".to_vec()),
6252 }));
6253
6254 let fonts = extract_embedded_fonts(&doc);
6255 let font = fonts
6256 .iter()
6257 .find(|font| font.name == "MyriadPro-Regular")
6258 .expect("expected reusable simple font");
6259
6260 assert!(font.data.is_empty(), "no FontFile* should keep data empty");
6261 assert_eq!(font.pdf_widths, Some((32, vec![278, 333, 612])));
6262 assert_eq!(
6263 font.pdf_source_font,
6264 Some(PdfSourceFont { object_id: font_id })
6265 );
6266 }
6267
6268 #[test]
6269 fn store_font_data_reserves_family_alias_for_regular_face() {
6270 let mut fonts = Vec::new();
6271 store_font_data(
6272 &mut fonts,
6273 "ArialMT",
6274 Vec::new(),
6275 Some((32, vec![278, 333, 611])),
6276 None,
6277 Some(PdfSourceFont { object_id: (1, 0) }),
6278 );
6279 store_font_data(
6280 &mut fonts,
6281 "Arial-BoldMT",
6282 Vec::new(),
6283 Some((32, vec![278, 333, 611])),
6284 None,
6285 Some(PdfSourceFont { object_id: (2, 0) }),
6286 );
6287 store_font_data(
6288 &mut fonts,
6289 "Arial-ItalicMT",
6290 Vec::new(),
6291 Some((32, vec![278, 333, 611])),
6292 None,
6293 Some(PdfSourceFont { object_id: (3, 0) }),
6294 );
6295
6296 let aliases: Vec<_> = fonts.iter().map(|font| font.name.as_str()).collect();
6297 assert!(aliases.contains(&"ArialMT"));
6298 assert!(aliases.contains(&"Arial-BoldMT"));
6299 assert!(aliases.contains(&"Arial-ItalicMT"));
6300 assert_eq!(
6301 aliases.iter().filter(|name| **name == "Arial").count(),
6302 1,
6303 "only the regular face should claim the bare family alias"
6304 );
6305 }
6306
6307 #[test]
6308 fn store_font_data_keeps_regular_ps_family_alias() {
6309 let mut fonts = Vec::new();
6310 store_font_data(
6311 &mut fonts,
6312 "MyriadPro-Regular",
6313 Vec::new(),
6314 Some((32, vec![278, 333, 612])),
6315 None,
6316 Some(PdfSourceFont { object_id: (4, 0) }),
6317 );
6318
6319 assert!(
6320 fonts.iter().any(|font| font.name == "Myriad Pro"),
6321 "regular PostScript names should still expose their family alias"
6322 );
6323 }
6324
6325 #[test]
6326 fn page_content_streams_resolves_indirect_contents_arrays() {
6327 let mut doc = Document::new();
6328 let stream_a = doc.add_object(Stream::new(
6329 dictionary! {"Length" => Object::Integer(8)},
6330 b"(A) Tj\n".to_vec(),
6331 ));
6332 let stream_b = doc.add_object(Stream::new(
6333 dictionary! {"Length" => Object::Integer(8)},
6334 b"(B) Tj\n".to_vec(),
6335 ));
6336 let contents_array = doc.add_object(Object::Array(vec![
6337 Object::Reference(stream_a),
6338 Object::Reference(stream_b),
6339 ]));
6340 let page_id = doc.add_object(Object::Dictionary(dictionary! {
6341 "Type" => Object::Name(b"Page".to_vec()),
6342 "Contents" => Object::Reference(contents_array),
6343 }));
6344
6345 let streams = page_content_streams(&doc, page_id);
6346
6347 assert_eq!(
6348 streams.len(),
6349 2,
6350 "indirect /Contents arrays must be traversed"
6351 );
6352 assert!(streams[0].windows(2).any(|w| w == b"Tj"));
6353 assert!(streams[1].windows(2).any(|w| w == b"Tj"));
6354 }
6355
6356 #[test]
6357 fn embed_resolved_fonts_reuses_existing_pdf_font_object() {
6358 let mut doc = Document::new();
6359 let source_font_id = doc.add_object(Object::Dictionary(dictionary! {
6360 "Type" => Object::Name(b"Font".to_vec()),
6361 "Subtype" => Object::Name(b"Type1".to_vec()),
6362 "BaseFont" => Object::Name(b"MyriadPro-Regular".to_vec()),
6363 "Encoding" => Object::Name(b"WinAnsiEncoding".to_vec()),
6364 }));
6365 let before = doc.objects.len();
6366
6367 let mut resolved = HashMap::new();
6368 resolved.insert(
6369 "Myriad Pro_Normal_Normal".to_string(),
6370 ResolvedFont {
6371 name: "Myriad Pro".to_string(),
6372 data: Vec::new(),
6373 face_index: 0,
6374 units_per_em: 1000,
6375 ascender: 800,
6376 descender: -200,
6377 pdf_widths: Some((32, vec![278, 333, 612])),
6378 pdf_encoding: None,
6379 pdf_source_font: Some(PdfSourceFont {
6380 object_id: source_font_id,
6381 }),
6382 },
6383 );
6384
6385 let empty_layout = LayoutDom { pages: vec![] };
6386 let (_font_map, font_objects, metrics_data) =
6387 embed_resolved_fonts(&mut doc, &resolved, &empty_layout);
6388
6389 assert_eq!(
6390 doc.objects.len(),
6391 before,
6392 "should not embed a new font object"
6393 );
6394 assert_eq!(font_objects.len(), 1);
6395 assert_eq!(font_objects[0].1, source_font_id);
6396 assert!(
6397 metrics_data["Myriad Pro_Normal_Normal"].font_data.is_none(),
6398 "reused simple fonts must keep WinAnsi text encoding"
6399 );
6400 }
6401
6402 #[test]
6403 fn strip_undefined_entities_preserves_raw_ampersands_in_processing_instructions() {
6404 let xml = r##"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><?renderCache.textRun 24 A. Adjustment & Location 0 1417 14917 0 0 0 "Myriad Pro" 0 0 18000 ISO-8859-1?><?renderCache.subset "Arial" 0 0 ISO-8859-1 "#$%&'()+,-./" ?><subform name="form1"><field name="A"/></subform></template>"##;
6405
6406 let stripped = strip_undefined_xml_entities(xml);
6407
6408 assert_eq!(
6409 stripped, xml,
6410 "raw ampersands inside processing instructions are valid and must survive sanitization"
6411 );
6412 roxmltree::Document::parse(&stripped)
6413 .expect("processing instructions must remain parseable");
6414 }
6415
6416 #[test]
6417 fn strip_undefined_entities_drops_only_true_named_entity_references() {
6418 let xml = r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form1"><draw name="D"><value><text>alpha &bogus; beta © & gamma</text></value></draw></subform></template>"#;
6419
6420 let stripped = strip_undefined_xml_entities(xml);
6421
6422 assert!(
6423 !stripped.contains("&bogus;"),
6424 "unknown named entities should still be removed for roxmltree compatibility"
6425 );
6426 assert!(stripped.contains("©"));
6427 assert!(stripped.contains("&"));
6428 roxmltree::Document::parse(&stripped).expect("sanitized XML should parse");
6429 }
6430
6431 #[test]
6434 fn form_dom_expands_repeating_subform_instances() {
6435 use xfa_layout_engine::form::FormNodeType;
6436
6437 let template = r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
6439 <subform name="root" layout="tb">
6440 <pageSet><pageArea name="P1">
6441 <contentArea w="200mm" h="280mm"/>
6442 <medium short="210mm" long="297mm"/>
6443 </pageArea></pageSet>
6444 <subform name="body" layout="tb">
6445 <subform name="Items" layout="tb">
6446 <bind match="none"/>
6447 <subform name="Row" layout="tb">
6448 <bind match="none"/>
6449 <occur max="-1"/>
6450 <field name="Label"><ui><textEdit/></ui></field>
6451 </subform>
6452 </subform>
6453 </subform>
6454 </subform>
6455 </template>"#;
6456
6457 let form_xml = r#"<form xmlns="http://www.xfa.org/schema/xfa-form/2.8/">
6459 <subform name="root">
6460 <subform name="body">
6461 <subform name="Items">
6462 <instanceManager name="_Row"/>
6463 <subform name="Row">
6464 <field name="Label"><value><text>Alpha</text></value></field>
6465 </subform>
6466 <subform name="Row">
6467 <field name="Label"><value><text>Beta</text></value></field>
6468 </subform>
6469 <subform name="Row">
6470 <field name="Label"><value><text>Gamma</text></value></field>
6471 </subform>
6472 </subform>
6473 </subform>
6474 </subform>
6475 </form>"#;
6476
6477 let data_dom = xfa_dom_resolver::data_dom::DataDom::new();
6478 let merger = crate::merger::FormMerger::new(&data_dom);
6479 let (mut tree, root_id) = merger.merge(template).unwrap();
6480
6481 fn find_by_name(tree: &FormTree, parent: FormNodeId, name: &str) -> Option<FormNodeId> {
6484 for &c in &tree.get(parent).children {
6485 if tree.get(c).name == name {
6486 return Some(c);
6487 }
6488 if let Some(found) = find_by_name(tree, c, name) {
6489 return Some(found);
6490 }
6491 }
6492 None
6493 }
6494 let items_id =
6495 find_by_name(&tree, root_id, "Items").expect("Items subform not found in tree");
6496 let rows_before = tree
6497 .get(items_id)
6498 .children
6499 .iter()
6500 .filter(|&&c| tree.get(c).name == "Row")
6501 .count();
6502 assert_eq!(
6503 rows_before, 1,
6504 "template merge should produce 1 Row (bind=none)"
6505 );
6506
6507 let _ = apply_form_dom_presence(
6509 &mut tree,
6510 root_id,
6511 form_xml,
6512 XfaRenderingPolicy::SavedStateFaithful,
6513 false,
6514 );
6515
6516 let rows_after: Vec<FormNodeId> = tree
6518 .get(items_id)
6519 .children
6520 .iter()
6521 .filter(|&&c| tree.get(c).name == "Row")
6522 .copied()
6523 .collect();
6524 assert_eq!(
6525 rows_after.len(),
6526 3,
6527 "form DOM should expand to 3 Row instances"
6528 );
6529
6530 let values: Vec<String> = rows_after
6531 .iter()
6532 .map(|&row_id| {
6533 let label_id = tree.get(row_id).children[0];
6534 match &tree.get(label_id).node_type {
6535 FormNodeType::Field { value } => value.clone(),
6536 _ => String::new(),
6537 }
6538 })
6539 .collect();
6540 assert_eq!(values, vec!["Alpha", "Beta", "Gamma"]);
6541 }
6542
6543 #[test]
6551 fn form_dom_expands_uniform_page_area_template() {
6552 use xfa_layout_engine::form::FormNodeType;
6553
6554 let template = r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
6555 <subform name="root" layout="tb">
6556 <pageSet>
6557 <pageArea name="Page1">
6558 <contentArea w="200mm" h="280mm"/>
6559 <medium short="210mm" long="297mm"/>
6560 </pageArea>
6561 </pageSet>
6562 </subform>
6563 </template>"#;
6564
6565 let form_xml = r#"<form xmlns="http://www.xfa.org/schema/xfa-form/2.8/">
6566 <subform name="root">
6567 <pageSet>
6568 <pageArea name="Page1"/>
6569 <pageArea name="Page1"/>
6570 <pageArea name="Page1"/>
6571 <pageArea name="Page1"/>
6572 <pageArea name="Page1"/>
6573 </pageSet>
6574 </subform>
6575 </form>"#;
6576
6577 let data_dom = xfa_dom_resolver::data_dom::DataDom::new();
6578 let merger = crate::merger::FormMerger::new(&data_dom);
6579 let (mut tree, root_id) = merger.merge(template).unwrap();
6580
6581 let _ = apply_form_dom_presence(
6582 &mut tree,
6583 root_id,
6584 form_xml,
6585 XfaRenderingPolicy::SavedStateFaithful,
6586 false,
6587 );
6588
6589 fn collect_page_areas(tree: &FormTree, id: FormNodeId, out: &mut Vec<FormNodeId>) {
6590 if matches!(tree.get(id).node_type, FormNodeType::PageArea { .. }) {
6591 out.push(id);
6592 }
6593 for &c in &tree.get(id).children {
6594 collect_page_areas(tree, c, out);
6595 }
6596 }
6597
6598 let mut page_areas = Vec::new();
6599 collect_page_areas(&tree, root_id, &mut page_areas);
6600 assert_eq!(
6601 page_areas.len(),
6602 5,
6603 "uniform pageArea expansion: 5 form-DOM instances must clone the template"
6604 );
6605 for &pa_id in &page_areas {
6606 assert!(
6607 tree.meta(pa_id).runtime_instantiated_page,
6608 "every expanded pageArea must be flagged as runtime-instantiated"
6609 );
6610 }
6611 }
6612
6613 #[test]
6618 fn form_dom_skips_multi_template_page_area_expansion() {
6619 use xfa_layout_engine::form::FormNodeType;
6620
6621 let template = r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
6622 <subform name="root" layout="tb">
6623 <pageSet>
6624 <pageArea name="Page1">
6625 <contentArea w="200mm" h="280mm"/>
6626 <medium short="210mm" long="297mm"/>
6627 </pageArea>
6628 <pageArea name="OverFlowPage">
6629 <contentArea w="200mm" h="280mm"/>
6630 <medium short="210mm" long="297mm"/>
6631 </pageArea>
6632 </pageSet>
6633 </subform>
6634 </template>"#;
6635
6636 let form_xml = r#"<form xmlns="http://www.xfa.org/schema/xfa-form/2.8/">
6637 <subform name="root">
6638 <pageSet>
6639 <pageArea name="Page1"/>
6640 <pageArea name="OverFlowPage"/>
6641 <pageArea name="OverFlowPage"/>
6642 <pageArea name="OverFlowPage"/>
6643 <pageArea name="OverFlowPage"/>
6644 </pageSet>
6645 </subform>
6646 </form>"#;
6647
6648 let data_dom = xfa_dom_resolver::data_dom::DataDom::new();
6649 let merger = crate::merger::FormMerger::new(&data_dom);
6650 let (mut tree, root_id) = merger.merge(template).unwrap();
6651
6652 let _ = apply_form_dom_presence(
6653 &mut tree,
6654 root_id,
6655 form_xml,
6656 XfaRenderingPolicy::SavedStateFaithful,
6657 false,
6658 );
6659
6660 fn collect_page_areas(tree: &FormTree, id: FormNodeId, out: &mut Vec<FormNodeId>) {
6661 if matches!(tree.get(id).node_type, FormNodeType::PageArea { .. }) {
6662 out.push(id);
6663 }
6664 for &c in &tree.get(id).children {
6665 collect_page_areas(tree, c, out);
6666 }
6667 }
6668
6669 let mut page_areas = Vec::new();
6670 collect_page_areas(&tree, root_id, &mut page_areas);
6671 assert_eq!(
6672 page_areas.len(),
6673 2,
6674 "multi-template pageSet must not clone pageAreas (kept original 2)"
6675 );
6676 for &pa_id in &page_areas {
6677 assert!(
6678 !tree.meta(pa_id).runtime_instantiated_page,
6679 "non-expansion case must not set runtime_instantiated_page flag"
6680 );
6681 }
6682 }
6683
6684 #[test]
6698 fn formdom_admit_databound_override_admits_only_data_bound() {
6699 use xfa_layout_engine::form::{FormNodeType, Presence};
6700
6701 let template = r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
6703 <subform name="root" layout="tb">
6704 <subform name="body" layout="tb">
6705 <subform name="Bound" layout="tb">
6706 <field name="A"><ui><textEdit/></ui></field>
6707 </subform>
6708 <subform name="Unbound" layout="tb">
6709 <field name="B"><ui><textEdit/></ui></field>
6710 </subform>
6711 <subform name="Present" layout="tb">
6712 <field name="C"><ui><textEdit/></ui></field>
6713 </subform>
6714 </subform>
6715 </subform>
6716 </template>"#;
6717
6718 let form_xml = r#"<form xmlns="http://www.xfa.org/schema/xfa-form/2.8/">
6722 <subform name="root">
6723 <subform name="body">
6724 <subform name="Present">
6725 <field name="C"><value><text>x</text></value></field>
6726 </subform>
6727 </subform>
6728 </subform>
6729 </form>"#;
6730
6731 fn find(tree: &FormTree, parent: FormNodeId, name: &str) -> Option<FormNodeId> {
6732 for &c in &tree.get(parent).children {
6733 if tree.get(c).name == name {
6734 return Some(c);
6735 }
6736 if let Some(f) = find(tree, c, name) {
6737 return Some(f);
6738 }
6739 }
6740 None
6741 }
6742
6743 let scenario = |policy: XfaRenderingPolicy, override_on: bool| {
6747 let data_dom = xfa_dom_resolver::data_dom::DataDom::new();
6748 let merger = crate::merger::FormMerger::new(&data_dom);
6749 let (mut tree, root_id) = merger.merge(template).unwrap();
6750
6751 let bound = find(&tree, root_id, "Bound").expect("Bound subform in tree");
6752 let unbound = find(&tree, root_id, "Unbound").expect("Unbound subform in tree");
6753 assert!(matches!(tree.get(bound).node_type, FormNodeType::Subform));
6754 assert!(matches!(tree.get(unbound).node_type, FormNodeType::Subform));
6755
6756 for &id in &[bound, unbound] {
6757 let m = tree.meta_mut(id);
6758 m.presence = Presence::Visible;
6759 m.is_zero_instance_prototype = false;
6760 m.data_bind_none = false;
6761 }
6762 tree.meta_mut(bound).bound_data_node = Some(0); tree.meta_mut(unbound).bound_data_node = None; let (admitted, _, _) =
6766 apply_form_dom_presence(&mut tree, root_id, form_xml, policy, override_on);
6767 (
6768 admitted,
6769 tree.meta(bound).presence,
6770 tree.meta(unbound).presence,
6771 )
6772 };
6773
6774 let (adm, bound_p, unbound_p) = scenario(XfaRenderingPolicy::SavedStateFaithful, false);
6778 assert_eq!(
6779 adm, 0,
6780 "override off admits nothing under SavedStateFaithful"
6781 );
6782 assert_eq!(
6783 bound_p,
6784 Presence::Hidden,
6785 "data-bound suppressed when override off"
6786 );
6787 assert_eq!(
6788 unbound_p,
6789 Presence::Hidden,
6790 "non-data suppressed when override off"
6791 );
6792
6793 let (adm, bound_p, unbound_p) = scenario(XfaRenderingPolicy::SavedStateFaithful, true);
6797 assert_eq!(adm, 1, "override on admits exactly the data-bound subform");
6798 assert_eq!(
6799 bound_p,
6800 Presence::Visible,
6801 "data-bound admitted when override on"
6802 );
6803 assert_eq!(
6804 unbound_p,
6805 Presence::Hidden,
6806 "non-data subform must stay suppressed with override on (over-pagination guard)"
6807 );
6808
6809 let (adm, bound_p, unbound_p) = scenario(XfaRenderingPolicy::FreshMergeExperimental, false);
6812 assert_eq!(
6813 adm, 1,
6814 "fresh-merge admits the data-bound subform without override"
6815 );
6816 assert_eq!(
6817 bound_p,
6818 Presence::Visible,
6819 "data-bound admitted under fresh-merge"
6820 );
6821 assert_eq!(
6822 unbound_p,
6823 Presence::Hidden,
6824 "non-data suppressed under fresh-merge"
6825 );
6826 }
6827
6828 #[test]
6834 fn flatten_xfa_to_pdf_recursion_guard_returns_error() {
6835 let pdf_bytes = build_xfa_pdf(SIMPLE_XDP);
6836 let result = flatten_xfa_to_pdf_simulate_reentrant(&pdf_bytes);
6837 assert!(
6838 result.is_err(),
6839 "expected recursion guard to return Err, got Ok"
6840 );
6841 let err_msg = result.unwrap_err().to_string();
6842 assert!(
6843 err_msg.contains("recursively"),
6844 "expected error message to mention recursion, got: {err_msg}"
6845 );
6846 }
6847
6848 #[test]
6851 fn flatten_xfa_to_pdf_depth_counter_resets_after_call() {
6852 let pdf_bytes = build_xfa_pdf(SIMPLE_XDP);
6853 let _ = flatten_xfa_to_pdf(&pdf_bytes);
6855 let pdf_bytes2 = build_xfa_pdf(SIMPLE_XDP);
6857 let result = flatten_xfa_to_pdf(&pdf_bytes2);
6858 assert!(
6859 result.is_ok(),
6860 "second flatten call should succeed, got: {result:?}"
6861 );
6862 }
6863
6864 #[test]
6868 fn flatten_xfa_to_pdf_does_not_panic_on_empty_input() {
6869 let result = flatten_xfa_to_pdf(&[]);
6870 let _ = result;
6874 }
6875
6876 #[test]
6884 fn flatten_pipeline_completes_on_minimal_xfa_pdf() {
6885 let pdf_bytes = build_xfa_pdf(SIMPLE_XDP);
6886 let result = flatten_xfa_to_pdf(&pdf_bytes);
6890 let _ = result; }
6892
6893 #[test]
6894 fn flatten_with_layout_dump_preserves_pdf_bytes() {
6895 let pdf_bytes = build_xfa_pdf(SIMPLE_XDP);
6896 let flattened = flatten_xfa_to_pdf(&pdf_bytes).expect("plain flatten should succeed");
6897 let (flattened_with_dump, layout_dump) =
6898 flatten_xfa_to_pdf_with_layout_dump(&pdf_bytes).expect("dump flatten should succeed");
6899
6900 assert_eq!(flattened_with_dump, flattened);
6901 assert!(!layout_dump.pages.is_empty());
6902 assert_eq!(layout_dump.pages[0].page_num, 1);
6903 assert!(layout_dump.pages[0].used_height <= layout_dump.pages[0].page_height);
6904 }
6905
6906 #[test]
6912 fn flatten_removes_needs_rendering() {
6913 let mut pdf_bytes = build_xfa_pdf(SIMPLE_XDP);
6915 {
6917 let mut doc = Document::load_mem(&pdf_bytes).expect("parse for NeedsRendering test");
6918 let root_id = match doc.trailer.get(b"Root") {
6919 Ok(Object::Reference(id)) => *id,
6920 _ => panic!("no Root in trailer"),
6921 };
6922 if let Ok(Object::Dictionary(ref mut dict)) = doc.get_object_mut(root_id) {
6923 dict.set("NeedsRendering", Object::Boolean(true));
6924 }
6925 let mut out = Vec::new();
6926 doc.save_to(&mut out)
6927 .expect("re-save for NeedsRendering test");
6928 pdf_bytes = out;
6929 }
6930
6931 let flattened = flatten_xfa_to_pdf(&pdf_bytes).expect("flatten failed");
6933 let doc = Document::load_mem(&flattened).expect("parse flattened PDF");
6934 let root_id = match doc.trailer.get(b"Root") {
6935 Ok(Object::Reference(id)) => *id,
6936 _ => panic!("no Root in flattened trailer"),
6937 };
6938 let catalog = doc.get_dictionary(root_id).expect("catalog dict");
6939 assert!(
6940 catalog.get(b"NeedsRendering").is_err(),
6941 "/NeedsRendering must be absent after flatten"
6942 );
6943 }
6944
6945 #[test]
6948 fn flatten_removes_xfa_entry() {
6949 let pdf_bytes = build_xfa_pdf(SIMPLE_XDP);
6950 let flattened = flatten_xfa_to_pdf(&pdf_bytes).expect("flatten failed");
6951
6952 let flattened_str = String::from_utf8_lossy(&flattened);
6955 assert!(
6956 !flattened_str.contains("/XFA"),
6957 "/XFA must be absent from flattened output, but was found"
6958 );
6959 }
6960
6961 #[test]
6962 fn remove_acroform_purges_xfa_packet_objects() {
6963 let (mut doc, acroform_id, xfa_ids) = build_xfa_doc_with_xfa_array();
6964
6965 remove_acroform(&mut doc);
6966
6967 assert!(
6968 !doc.objects.contains_key(&acroform_id),
6969 "AcroForm object should be removed from doc.objects"
6970 );
6971 for xfa_id in &xfa_ids {
6972 assert!(
6973 !doc.objects.contains_key(xfa_id),
6974 "XFA packet object {xfa_id:?} should be removed from doc.objects"
6975 );
6976 }
6977
6978 let mut out = Vec::new();
6979 doc.save_to(&mut out).expect("save cleaned PDF");
6980 let out_str = String::from_utf8_lossy(&out);
6981 assert!(
6982 !out_str.contains("xdp:xdp"),
6983 "serialized output should not contain orphaned XFA packet payloads"
6984 );
6985 assert!(
6986 !out_str.contains("<template"),
6987 "serialized output should not contain orphaned template payloads"
6988 );
6989 }
6990
6991 #[test]
6993 fn flatten_removes_empty_annots_arrays() {
6994 let mut pdf_bytes = build_xfa_pdf(SIMPLE_XDP);
6996 {
6997 let mut doc = Document::load_mem(&pdf_bytes).expect("parse for annots test");
6998 let page_id = doc.page_iter().next().expect("at least one page");
6999 if let Ok(Object::Dictionary(ref mut dict)) = doc.get_object_mut(page_id) {
7000 dict.set("Annots", Object::Array(vec![]));
7001 }
7002 let mut out = Vec::new();
7003 doc.save_to(&mut out).expect("re-save for annots test");
7004 pdf_bytes = out;
7005 }
7006
7007 let flattened = flatten_xfa_to_pdf(&pdf_bytes).expect("flatten failed");
7008 let doc = Document::load_mem(&flattened).expect("parse flattened PDF");
7009 for page_id in doc.page_iter() {
7010 let page = doc.get_dictionary(page_id).expect("page dict");
7011 if let Ok(Object::Array(arr)) = page.get(b"Annots") {
7012 assert!(
7014 !arr.is_empty(),
7015 "page {:?}: /Annots must either be absent or non-empty after flatten",
7016 page_id
7017 );
7018 }
7019 }
7020 }
7021
7022 #[test]
7023 fn remove_acroform_strips_widgets_from_indirect_annots_arrays() {
7024 let appearance = Object::Stream(Stream::new(
7025 dictionary! {
7026 "Type" => Object::Name(b"XObject".to_vec()),
7027 "Subtype" => Object::Name(b"Form".to_vec()),
7028 "BBox" => Object::Array(vec![
7029 Object::Integer(0), Object::Integer(0),
7030 Object::Integer(20), Object::Integer(20),
7031 ]),
7032 "Resources" => Object::Dictionary(dictionary! {}),
7033 },
7034 b"BT /F1 8 Tf 1 1 Td (X) Tj ET\n".to_vec(),
7035 ));
7036 let pdf_bytes = build_xfa_pdf_with_widget_appearance(
7037 Vec::new(),
7038 appearance,
7039 dictionary! {
7040 "FT" => Object::Name(b"Tx".to_vec()),
7041 "T" => Object::string_literal("field[0]"),
7042 },
7043 );
7044
7045 let mut doc = Document::load_mem(&pdf_bytes).expect("parse test PDF");
7046 let page_id = doc.page_iter().next().expect("page");
7047 let annots = page_annotations(&doc, page_id);
7048 let annots_id = doc.add_object(Object::Array(annots));
7049 if let Ok(Object::Dictionary(ref mut page_dict)) = doc.get_object_mut(page_id) {
7050 page_dict.set("Annots", Object::Reference(annots_id));
7051 }
7052
7053 remove_acroform(&mut doc);
7054
7055 let page = doc.get_dictionary(page_id).expect("page dict");
7056 assert!(
7057 page.get(b"Annots").is_err(),
7058 "widget-only indirect /Annots must be removed"
7059 );
7060 }
7061
7062 #[test]
7063 fn acroform_without_xfa_falls_back_to_static_cleanup() {
7064 let appearance = Object::Stream(Stream::new(
7065 dictionary! {
7066 "Type" => Object::Name(b"XObject".to_vec()),
7067 "Subtype" => Object::Name(b"Form".to_vec()),
7068 "BBox" => Object::Array(vec![
7069 Object::Integer(0), Object::Integer(0),
7070 Object::Integer(20), Object::Integer(20),
7071 ]),
7072 "Resources" => Object::Dictionary(dictionary! {}),
7073 },
7074 b"BT /F1 8 Tf 1 1 Td (X) Tj ET\n".to_vec(),
7075 ));
7076 let pdf_bytes = build_xfa_pdf_with_widget_appearance(
7077 Vec::new(),
7078 appearance,
7079 dictionary! {
7080 "FT" => Object::Name(b"Tx".to_vec()),
7081 "T" => Object::string_literal("field[0]"),
7082 },
7083 );
7084
7085 let mut doc = Document::load_mem(&pdf_bytes).expect("parse source PDF");
7086 let root_id = match doc.trailer.get(b"Root") {
7087 Ok(Object::Reference(id)) => *id,
7088 _ => panic!("no Root"),
7089 };
7090 let acroform_id = doc
7091 .get_dictionary(root_id)
7092 .expect("catalog")
7093 .get(b"AcroForm")
7094 .expect("AcroForm")
7095 .as_reference()
7096 .expect("AcroForm ref");
7097 if let Ok(Object::Dictionary(ref mut acroform)) = doc.get_object_mut(acroform_id) {
7098 acroform.remove(b"XFA");
7099 }
7100 let mut acroform_only = Vec::new();
7101 doc.save_to(&mut acroform_only)
7102 .expect("save AcroForm-only PDF");
7103
7104 let flattened = flatten_xfa_to_pdf(&acroform_only).expect("flatten failed");
7105 let flattened_doc = Document::load_mem(&flattened).expect("parse flattened PDF");
7106 let root_id = match flattened_doc.trailer.get(b"Root") {
7107 Ok(Object::Reference(id)) => *id,
7108 _ => panic!("no Root in flattened PDF"),
7109 };
7110 let catalog = flattened_doc
7111 .get_dictionary(root_id)
7112 .expect("flattened catalog");
7113 assert!(
7114 catalog.get(b"AcroForm").is_err(),
7115 "AcroForm-only PDFs should still be cleaned by flatten"
7116 );
7117
7118 let page_id = flattened_doc.page_iter().next().expect("flattened page");
7119 assert!(
7120 page_annotations(&flattened_doc, page_id).is_empty(),
7121 "flattened AcroForm-only PDFs should not retain widget annotations"
7122 );
7123 }
7124
7125 #[test]
7131 fn validate_flattened_pdf_clean_pdf_passes() {
7132 let mut doc = Document::with_version("1.4");
7134 let pages_id = doc.new_object_id();
7135 let page_id = doc.add_object(Object::Dictionary(dictionary! {
7136 "Type" => Object::Name(b"Page".to_vec()),
7137 "Parent" => Object::Reference(pages_id),
7138 "MediaBox" => Object::Array(vec![
7139 Object::Integer(0), Object::Integer(0),
7140 Object::Integer(612), Object::Integer(792),
7141 ])
7142 }));
7143 doc.objects.insert(
7144 pages_id,
7145 Object::Dictionary(dictionary! {
7146 "Type" => Object::Name(b"Pages".to_vec()),
7147 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
7148 "Count" => Object::Integer(1)
7149 }),
7150 );
7151 let catalog_id = doc.add_object(Object::Dictionary(dictionary! {
7152 "Type" => Object::Name(b"Catalog".to_vec()),
7153 "Pages" => Object::Reference(pages_id)
7154 }));
7155 doc.trailer.set("Root", Object::Reference(catalog_id));
7156 let mut pdf_bytes = Vec::new();
7157 doc.save_to(&mut pdf_bytes).expect("save clean PDF");
7158
7159 let validation = validate_flattened_pdf(&pdf_bytes).expect("validate failed");
7160 assert!(
7161 validation.has_no_acroform,
7162 "clean PDF should have no AcroForm"
7163 );
7164 assert!(validation.has_no_xfa, "clean PDF should have no XFA");
7165 assert!(
7166 validation.has_no_needs_rendering,
7167 "clean PDF should have no NeedsRendering"
7168 );
7169 assert_eq!(validation.page_count, 1, "clean PDF should report 1 page");
7170 assert!(
7171 validation.warnings.is_empty(),
7172 "clean PDF should produce no warnings, got: {:?}",
7173 validation.warnings
7174 );
7175 }
7176
7177 #[test]
7179 fn validate_flattened_pdf_does_not_panic_on_empty_input() {
7180 let result = validate_flattened_pdf(&[]);
7181 assert!(
7183 result.is_ok(),
7184 "expected Ok from empty input, got: {:?}",
7185 result.err()
7186 );
7187 let v = result.unwrap();
7188 assert_eq!(v.page_count, 0, "empty input has 0 pages");
7189 assert!(
7190 !v.warnings.is_empty(),
7191 "empty input should produce at least one warning"
7192 );
7193 }
7194
7195 #[test]
7201 fn compare_flatten_quality_page_count_comparison() {
7202 let original = build_xfa_pdf(SIMPLE_XDP);
7203 let flattened = flatten_xfa_to_pdf(&original).expect("flatten failed");
7204 let metrics =
7205 compare_flatten_quality(&original, &flattened).expect("compare_flatten_quality failed");
7206 assert!(
7208 metrics.page_count_before >= 1,
7209 "original must have >= 1 page"
7210 );
7211 assert!(
7212 metrics.page_count_after >= 1,
7213 "flattened must have >= 1 page"
7214 );
7215 assert_eq!(
7217 metrics.page_count_match,
7218 metrics.page_count_before == metrics.page_count_after,
7219 "page_count_match must equal page_count_before == page_count_after"
7220 );
7221 }
7222
7223 #[test]
7225 fn compare_flatten_quality_content_ratio_computed() {
7226 let original = build_xfa_pdf(SIMPLE_XDP);
7227 let flattened = flatten_xfa_to_pdf(&original).expect("flatten failed");
7228 let metrics =
7229 compare_flatten_quality(&original, &flattened).expect("compare_flatten_quality failed");
7230 assert!(
7232 metrics.content_ratio.is_finite() && metrics.content_ratio >= 0.0,
7233 "content_ratio must be finite and >= 0, got: {}",
7234 metrics.content_ratio
7235 );
7236 let expected = if metrics.content_stream_bytes_before == 0 {
7238 1.0_f64
7239 } else {
7240 metrics.content_stream_bytes_after as f64 / metrics.content_stream_bytes_before as f64
7241 };
7242 assert!(
7243 (metrics.content_ratio - expected).abs() < 1e-9,
7244 "content_ratio mismatch: expected {expected}, got {}",
7245 metrics.content_ratio
7246 );
7247 }
7248
7249 #[test]
7256 fn validate_text_completeness_no_datasets_returns_perfect_ratio() {
7257 let xdp = r#"<?xml version="1.0"?>
7259<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
7260 <template>
7261 <subform name="root">
7262 <field name="greeting"><ui><textEdit/></ui></field>
7263 </subform>
7264 </template>
7265</xdp:xdp>"#;
7266 let original = build_xfa_pdf(xdp);
7267 let mut doc = Document::with_version("1.4");
7269 let pages_id = doc.new_object_id();
7270 let page_id = doc.add_object(Object::Dictionary(dictionary! {
7271 "Type" => Object::Name(b"Page".to_vec()),
7272 "Parent" => Object::Reference(pages_id),
7273 "MediaBox" => Object::Array(vec![
7274 Object::Integer(0), Object::Integer(0),
7275 Object::Integer(612), Object::Integer(792),
7276 ])
7277 }));
7278 doc.objects.insert(
7279 pages_id,
7280 Object::Dictionary(dictionary! {
7281 "Type" => Object::Name(b"Pages".to_vec()),
7282 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
7283 "Count" => Object::Integer(1)
7284 }),
7285 );
7286 let catalog_id = doc.add_object(Object::Dictionary(dictionary! {
7287 "Type" => Object::Name(b"Catalog".to_vec()),
7288 "Pages" => Object::Reference(pages_id)
7289 }));
7290 doc.trailer.set("Root", Object::Reference(catalog_id));
7291 let mut flattened = Vec::new();
7292 doc.save_to(&mut flattened).unwrap();
7293
7294 let result = validate_text_completeness(&original, &flattened)
7295 .expect("validate_text_completeness should not fail");
7296 assert!(
7297 result.expected_values.is_empty(),
7298 "no datasets packet means no expected values"
7299 );
7300 assert_eq!(
7301 result.completeness_ratio, 1.0,
7302 "empty expected set should yield ratio 1.0"
7303 );
7304 }
7305
7306 #[test]
7308 fn validate_text_completeness_empty_inputs_do_not_panic() {
7309 let result = validate_text_completeness(&[], &[]);
7310 assert!(result.is_ok(), "should return Ok on empty inputs");
7311 let v = result.unwrap();
7312 assert_eq!(v.completeness_ratio, 1.0);
7313 assert!(v.expected_values.is_empty());
7314 assert!(v.missing_values.is_empty());
7315 }
7316
7317 #[test]
7327 fn flatten_empty_bytes_does_not_panic_and_does_not_error() {
7328 let _ = flatten_xfa_to_pdf(b"");
7332 }
7333
7334 #[test]
7337 fn flatten_non_xfa_bytes_returns_input_unchanged() {
7338 let input = b"%PDF-1.4\n%%EOF\n";
7342 if let Ok(out) = flatten_xfa_to_pdf(input) {
7344 assert_eq!(out, input, "non-XFA input should pass through unchanged");
7345 }
7346 }
7347}