use std::sync::Arc; use pdfcraft_cos::{Document, Object, SaveOptions, write_incremental}; use super::*; use crate::data::{ DatasetsWrite, FieldData, FieldDatum, build_data, iso_to_pattern, parse_datasets, pattern_to_iso, read_values, som_to_path, write_datasets, }; use crate::data::{add_data_instances, remove_data_instance, write_data_value}; use crate::fixtures::{TINY_GIF, fields_template, scripted_template, shell, static_shell, template, template_with_data, tiny_png}; use crate::layout::Item; use crate::layout::WidgetKind; use crate::model::*; use crate::script::{NodeKind, Overrides, apply_overrides, fields_by_som, form_tree, overrides, rerender, set_overrides}; fn widgets(page: &Page) -> Vec<&Widget> { page.items.iter().filter_map(|i| if let Item::Widget(w) = i { Some(w.as_ref()) } else { None }).collect() } fn texts(page: &Page) -> Vec { page.items.iter().filter_map(|i| if let Item::Text(s) = i { Some(s.text.clone()) } else { None }).collect() } #[test] fn measurements_convert_to_points() { assert_eq!(measure("72pt"), Some(72.0)); assert_eq!(measure("1in"), Some(72.0)); assert!((measure("25.4mm").unwrap() - 72.0).abs() < 1e-9); assert!((measure("2.54cm").unwrap() - 72.0).abs() < 1e-9); assert_eq!(measure(" 12 "), Some(12.0)); assert_eq!(measure("3em"), None); assert_eq!(measure("abc"), None); assert_eq!(measure("NaNpt"), None); } #[test] fn the_template_parses_with_fonts_captions_and_rich_text() { let (tpl, warnings) = parse(&template(2)).unwrap(); assert!(warnings.is_empty(), "{warnings:?}"); assert_eq!(tpl.root.layout, Layout::Tb); let ps = tpl.root.page_set.as_ref().unwrap(); assert_eq!(ps.areas.len(), 2); assert_eq!((ps.areas[0].width, ps.areas[0].height), (612.0, 792.0)); assert_eq!(ps.areas[0].occur_max, Some(1)); assert_eq!(ps.areas[1].occur_max, None); assert_eq!(ps.areas[0].content[0], Rect::new(36.0, 36.0, 540.0, 720.0)); assert_eq!(tpl.page_roles, vec![("pageNo".to_string(), PageRole::Number), ("pageCount".to_string(), PageRole::Count)]); let Node::Subform(head) = &tpl.root.children[0] else { panic!() }; assert_eq!(head.layout, Layout::LrTb); let Node::Draw(title) = &head.children[0] else { panic!() }; let Value::Rich(r) = &title.value else { panic!("rich text") }; assert_eq!(r.paragraphs.len(), 2, "{r:?}"); assert_eq!(r.paragraphs[0].runs[0].bold, Some(true)); assert_eq!(r.plain(), "Sample Form\nSecond line"); let Node::Field(f) = &head.children[2] else { panic!() }; assert_eq!(f.common.name.as_deref(), Some("familyName")); assert_eq!(f.common.font.typeface.as_deref(), Some("Courier New")); assert_eq!(f.max_chars, Some(30)); assert_eq!(f.tooltip.as_deref(), Some("Your family name")); let cap = f.caption.as_ref().unwrap(); assert_eq!((cap.placement, cap.reserve), (Placement::Top, Some(14.4))); // The widget border: three hidden edges and a visible bottom one. assert_eq!(f.ui_border.as_ref().unwrap().visible_edges(), [false, false, true, false]); let Node::Field(check) = &head.children[3] else { panic!() }; assert_eq!((check.ui.clone(), check.items.clone()), (Ui::CheckButton, vec!["1".to_string(), "0".to_string()])); let Node::Field(date) = &head.children[4] else { panic!() }; assert_eq!((date.ui.clone(), date.picture.as_deref()), (Ui::DateTimeEdit, Some("date{YYYY-MM-DD}"))); let Node::ExclGroup(g) = &head.children[6] else { panic!() }; assert_eq!(g.fields.len(), 2); let Node::Subform(table) = &tpl.root.children[1] else { panic!() }; assert_eq!(table.column_widths, vec![144.0, 216.0, 180.0]); assert_eq!(table.overflow_leader.as_deref(), Some("header")); let Node::Subform(last) = &tpl.root.children[2] else { panic!() }; assert!(last.break_before_page); } #[test] fn the_xdp_template_wins_over_the_config_packets_template_element() { // The config packet has a
{rows}
" ), ); let form = layout_xml(&many).unwrap(); assert!(form.warnings.iter().any(|w| w.contains("60 rows") && w.contains("first 50")), "{:?}", form.warnings); } #[test] fn an_anchor_type_places_the_named_point_of_the_box_at_its_x_and_y() { // The page label of IRCC IMM 5532E: x is the centre of the box, so its right edge sits // 10.795mm right of x, inside the page, not 21.59mm. let xml = r##" "##; let form = layout_xml(xml).unwrap(); let page = &form.pages[0]; let span = |t: &str| { page.items .iter() .find_map(|i| { if let Item::Text(s) = i && s.text == t { Some(s.clone()) } else { None } }) .unwrap() }; let label = span("PAGE 1 OF 8"); let right = (197.485 + 21.59 / 2.0) * 72.0 / 25.4; assert!( (label.x + label.face.width(&label.text) - right).abs() < 0.5, "label ends at {}, box at {right}", label.x + label.face.width(&label.text) ); let foot = span("Footer"); assert!((foot.x - 7.0 * 72.0).abs() < 0.01, "footer at x {}", foot.x); assert!(foot.baseline > 10.25 * 72.0 && foot.baseline < 10.5 * 72.0, "footer baseline {}", foot.baseline); let field = widgets(page).into_iter().find(|w| w.name == "ref").unwrap(); assert!((field.rect.x - 6.0 * 72.0).abs() < 0.01 && (field.rect.y - 72.0).abs() < 0.01, "{:?}", field.rect); } #[test] fn stripping_takes_the_xfa_out_and_leaves_an_ordinary_document() { use crate::pdf::strip; let mut doc = Document::open(Arc::new(shell(&fields_template("
JP
")))).unwrap(); render_into(&mut doc).unwrap(); assert!(crate::read_packets(&doc).unwrap().is_some()); // Reader usage rights on the catalog go too (changing the form voids them). let root = doc.root().unwrap(); let mut ur = pdfcraft_cos::Dict::new(); ur.set(b"UR3".to_vec(), Object::Dict(pdfcraft_cos::Dict::new())); doc.update_dict(root, |d| d.set(b"Perms".to_vec(), Object::Dict(ur))).unwrap(); let r = strip(&mut doc).unwrap(); assert_eq!(r.packets, vec!["template".to_string(), "datasets".to_string()]); assert_eq!(r.unbound_data, Some(0)); assert_eq!((r.script_buttons, r.image_fields), (0, 1), "{r:?}"); assert!(r.usage_rights); assert!(r.field_keys >= 7, "{r:?}"); assert!( r.notes().iter().any(|n| n.contains("template, datasets")) && r.notes().iter().any(|n| n.contains("extended features")), "{:?}", r.notes() ); let bytes = write_incremental(&doc, &SaveOptions::default()).unwrap(); let doc = Document::open(Arc::new(bytes)).unwrap(); assert!(crate::read_packets(&doc).unwrap().is_none(), "no XFA entry"); assert!(!is_dynamic(&doc)); assert!(crate::existing_layout(&doc).is_none(), "the layout mark is gone"); let root = doc.get(doc.root().unwrap()); let catalog = root.as_dict().unwrap(); assert!(!catalog.contains(b"NeedsRendering") && !catalog.contains(b"Perms")); let acro = doc.resolve(catalog.get(b"AcroForm").unwrap()); let acro = acro.as_dict().unwrap(); assert!(!acro.contains(b"XFA") && !acro.contains(b"PCXfaLayout") && !acro.contains(b"PCXfaOverrides")); // The fields stay, with their values, and without the private keys. let fields = pdfcraft_forms_free_fields(&doc); assert_eq!(fields.len(), 7); let country = &fields.iter().find(|(n, _)| n == "country").unwrap().1; assert_eq!(country.get(b"V").and_then(|v| v.as_string()).map(|s| s.to_text()).as_deref(), Some("JP")); for (name, d) in &fields { for k in [b"PCSom".as_slice(), b"PCXfaClick", b"PCXfaImage", b"PCItems"] { assert!(!d.contains(k), "{name} still has {}", String::from_utf8_lossy(k)); } } // The pages are untouched. let pages = doc.resolve(catalog.get(b"Pages").unwrap()); assert_eq!(pages.as_dict().unwrap().int(b"Count"), Some(1)); // Twice is nothing: a document without XFA is left as it is. let mut doc = doc; let r = strip(&mut doc).unwrap(); assert_eq!(r, crate::Stripped::default()); assert!(!doc.is_modified()); // Data no field holds is counted; a form whose click buttons ran scripts too; the shape // check needs no packet decoded and tells a never-laid-out dynamic form from one to strip. let mut doc = Document::open(Arc::new(shell(&scripted_template().replace("", "
3kept nowherex
")))).unwrap(); let before = crate::xfa_shape(&doc).unwrap(); assert_eq!(before, crate::XfaShape { needs_rendering: true, has_fields: false, laid_out: false }); assert!(!before.strippable(), "nothing laid out yet: stripping would leave the placeholder page"); render_into(&mut doc).unwrap(); assert!(crate::xfa_shape(&doc).unwrap().strippable()); let r = strip(&mut doc).unwrap(); assert_eq!(r.unbound_data, Some(2), "{r:?}"); assert_eq!(r.script_buttons, 4, "addRow, removeRow, hello, legacy: {r:?}"); assert!(crate::xfa_shape(&doc).is_none()); // A field tree whose dictionaries share one Kids array (every kid listing every other) is // walked once per dictionary, not once per listing. let mut doc = Document::open(Arc::new(shell(&fields_template("")))).unwrap(); render_into(&mut doc).unwrap(); let root = doc.root().unwrap(); let acro_ref = doc.get(root).as_dict().unwrap().get(b"AcroForm").and_then(Object::as_ref); let fields: Vec = (0..2000).map(|_| Object::Ref(doc.add(Object::Dict(pdfcraft_cos::Dict::new())))).collect(); let kids = doc.add(Object::Array(fields.clone())); for f in &fields { if let Some(r) = f.as_ref() { doc.update_dict(r, |d| { d.set(b"Kids".to_vec(), Object::Ref(kids)); d.set(b"PCSom".to_vec(), Object::String(pdfcraft_cos::PdfString::text("x"))); }) .unwrap(); } } let update = |d: &mut pdfcraft_cos::Dict| d.set(b"Fields".to_vec(), Object::Array(fields.clone())); match acro_ref { Some(r) => doc.update_dict(r, update).unwrap(), None => doc .update_dict(root, |c| { let mut a = c.get(b"AcroForm").and_then(|a| a.as_dict().cloned()).unwrap_or_default(); update(&mut a); c.set(b"AcroForm".to_vec(), Object::Dict(a)); }) .unwrap(), } let started = std::time::Instant::now(); let r = strip(&mut doc).unwrap(); assert_eq!(r.field_keys, 2000); assert!(started.elapsed() < std::time::Duration::from_secs(5), "{:?}", started.elapsed()); } #[test] fn som_indices_count_earlier_same_named_siblings() { let draw = |name: Option<&str>| { Node::Draw(Box::new(Draw { common: Common { name: name.map(str::to_string), ..Default::default() }, ..Default::default() })) }; let children = [draw(Some("a")), draw(Some("b")), draw(Some("a")), draw(None), draw(Some("a")), draw(Some("b"))]; let sibs = SomIndices::of(&children); assert_eq!((0..=children.len()).map(|i| sibs.at(i)).collect::>(), [0, 0, 1, 0, 2, 1, 0]); }