1use pdf_interpret::color::Color;
14use xfa_layout_engine::form::{FieldKind, FormNodeStyle};
15use xfa_layout_engine::layout::{LayoutContent, LayoutNode, LayoutPage};
16use xfa_layout_engine::text::{FontFamily, FontMetrics};
17use xfa_layout_engine::types::TextAlign;
18
19use crate::render_bridge::XfaRenderConfig;
20
21#[derive(Debug, Clone)]
24pub enum XfaPaintCommand {
25 FillRect {
27 x: f64,
29 y: f64,
31 w: f64,
33 h: f64,
35 color: Color,
37 },
38 StrokeRect {
40 x: f64,
42 y: f64,
44 w: f64,
46 h: f64,
48 color: Color,
50 width: f64,
52 },
53 DrawText {
55 x: f64,
57 y: f64,
59 text: String,
61 font_family: FontFamily,
63 font_size: f64,
65 color: Color,
67 },
68 DrawMultilineText {
70 x: f64,
72 y: f64,
74 lines: Vec<String>,
76 font_family: FontFamily,
78 font_size: f64,
80 line_height: f64,
82 color: Color,
84 text_align: TextAlign,
86 container_width: f64,
88 text_padding: f64,
90 },
91 DrawImage {
93 x: f64,
95 y: f64,
97 w: f64,
99 h: f64,
101 image_data: Vec<u8>,
103 mime_type: String,
105 },
106 DrawCheckbox {
108 x: f64,
110 y: f64,
112 w: f64,
114 h: f64,
116 checked: bool,
118 border_color: [f64; 3],
120 check_color: [f64; 3],
122 border_width: f64,
124 },
125}
126
127fn apply_node_style(config: &XfaRenderConfig, style: &FormNodeStyle) -> XfaRenderConfig {
131 let mut cfg = config.clone();
132
133 if let Some((r, g, b)) = style.bg_color {
135 if !(r >= 250 && g >= 250 && b >= 250) {
136 cfg.background_color = Some([r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0]);
137 }
138 }
139
140 cfg.draw_borders = false;
143 if let Some(bw) = style.border_width_pt {
144 if bw > 0.0 {
145 cfg.border_width = bw;
146 cfg.draw_borders = true;
147 if let Some((r, g, b)) = style.border_color {
148 cfg.border_color = [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0];
149 }
150 }
151 }
152
153 if let Some((r, g, b)) = style.text_color {
155 cfg.text_color = [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0];
156 }
157
158 if let Some(mark) = &style.check_button_mark {
159 cfg.check_button_mark = Some(mark.clone());
160 }
161
162 cfg
163}
164
165pub fn layout_to_commands(page: &LayoutPage, config: &XfaRenderConfig) -> Vec<XfaPaintCommand> {
167 let mut commands = Vec::new();
168 let page_height = page.height;
169 for node in &page.nodes {
170 emit_node_commands(node, 0.0, 0.0, page_height, config, &mut commands);
171 }
172 commands
173}
174
175fn emit_node_commands(
176 node: &LayoutNode,
177 parent_x: f64,
178 parent_y: f64,
179 page_height: f64,
180 config: &XfaRenderConfig,
181 commands: &mut Vec<XfaPaintCommand>,
182) {
183 let abs_x = node.rect.x + parent_x;
184 let abs_y = node.rect.y + parent_y;
185 let w = node.rect.width;
186 let h = node.rect.height;
187 let pdf_y = page_height - abs_y - h;
189
190 let node_config = apply_node_style(config, &node.style);
192
193 if !matches!(node.content, LayoutContent::Field { .. }) {
197 if let Some(bg) = &node_config.background_color {
199 commands.push(XfaPaintCommand::FillRect {
200 x: abs_x,
201 y: pdf_y,
202 w,
203 h,
204 color: Color::from_device_rgb(bg[0] as f32, bg[1] as f32, bg[2] as f32),
205 });
206 }
207 if let Some(bw) = node.style.border_width_pt {
209 if bw > 0.0 && w > 0.0 && h > 0.0 {
210 let bc = node
211 .style
212 .border_color
213 .map_or([0.0, 0.0, 0.0], |(r, g, b)| {
214 [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0]
215 });
216 commands.push(XfaPaintCommand::StrokeRect {
217 x: abs_x,
218 y: pdf_y,
219 w,
220 h,
221 color: Color::from_device_rgb(bc[0] as f32, bc[1] as f32, bc[2] as f32),
222 width: bw,
223 });
224 }
225 }
226 }
227
228 match &node.content {
229 LayoutContent::Field {
230 value,
231 field_kind,
232 font_size,
233 font_family,
234 } => match field_kind {
235 FieldKind::Checkbox | FieldKind::Radio => {
236 let checked = !value.is_empty()
237 && !value.eq_ignore_ascii_case("0")
238 && !value.eq_ignore_ascii_case("off")
239 && !value.eq_ignore_ascii_case("false");
240 commands.push(XfaPaintCommand::DrawCheckbox {
241 x: abs_x,
242 y: pdf_y,
243 w,
244 h,
245 checked,
246 border_color: node_config.border_color,
247 check_color: node_config.text_color,
248 border_width: node_config.border_width.max(0.5),
249 });
250 }
251 _ => {
252 emit_field_commands(
253 abs_x,
254 pdf_y,
255 w,
256 h,
257 value,
258 *font_size,
259 *font_family,
260 &node_config,
261 commands,
262 );
263 }
264 },
265 LayoutContent::Text(text) => {
266 if !text.is_empty() {
267 let text_color = make_color(&node_config.text_color);
268 commands.push(XfaPaintCommand::DrawText {
269 x: abs_x + node_config.text_padding,
270 y: pdf_y + node_config.text_padding,
271 text: text.clone(),
272 font_family: FontFamily::SansSerif,
273 font_size: node_config.default_font_size,
274 color: text_color,
275 });
276 }
277 }
278 LayoutContent::WrappedText {
279 lines,
280 font_size,
281 text_align,
282 font_family,
283 ..
284 } => {
285 let fs = *font_size;
286 let line_height = fs * 1.2;
287 if !lines.is_empty() {
288 let text_color = make_color(&node_config.text_color);
289 let first_line_y = page_height - abs_y - fs;
293 commands.push(XfaPaintCommand::DrawMultilineText {
294 x: abs_x,
295 y: first_line_y,
296 lines: lines.clone(),
297 font_family: *font_family,
298 font_size: fs,
299 line_height,
300 color: text_color,
301 text_align: *text_align,
302 container_width: w,
303 text_padding: node_config.text_padding,
304 });
305 }
306 }
307 LayoutContent::Image { .. } => {}
308 LayoutContent::Draw(_) => {}
309 LayoutContent::None => {}
310 }
311
312 for child in &node.children {
315 emit_node_commands(child, abs_x, abs_y, page_height, config, commands);
316 }
317}
318
319#[allow(clippy::too_many_arguments)]
320fn emit_field_commands(
321 x: f64,
322 pdf_y: f64,
323 w: f64,
324 h: f64,
325 value: &str,
326 font_size: f64,
327 font_family: FontFamily,
328 config: &XfaRenderConfig,
329 commands: &mut Vec<XfaPaintCommand>,
330) {
331 if let Some(bg) = &config.background_color {
333 commands.push(XfaPaintCommand::FillRect {
334 x,
335 y: pdf_y,
336 w,
337 h,
338 color: Color::from_device_rgb(bg[0] as f32, bg[1] as f32, bg[2] as f32),
339 });
340 }
341 if config.draw_borders && config.border_width > 0.0 {
343 commands.push(XfaPaintCommand::StrokeRect {
344 x,
345 y: pdf_y,
346 w,
347 h,
348 color: make_color(&config.border_color),
349 width: config.border_width,
350 });
351 }
352 if !value.is_empty() {
354 let fs = if font_size > 0.0 {
355 font_size
356 } else {
357 config.default_font_size
358 };
359 let p = config.text_padding;
360 let content_w = (w - p * 2.0).max(0.0);
361 let metrics = FontMetrics {
362 size: fs,
363 typeface: font_family,
364 ..Default::default()
365 };
366 let text_w = metrics.measure_width(value);
367 let text_color = make_color(&config.text_color);
368
369 if text_w <= content_w || content_w <= 0.0 {
370 commands.push(XfaPaintCommand::DrawText {
372 x: x + p,
373 y: pdf_y + p,
374 text: value.to_string(),
375 font_family,
376 font_size: fs,
377 color: text_color,
378 });
379 } else {
380 let lines = wrap_text(value, content_w, &metrics);
382 let line_height = fs * 1.2;
383 commands.push(XfaPaintCommand::DrawMultilineText {
384 x,
385 y: pdf_y + h - p - fs,
386 lines,
387 font_family,
388 font_size: fs,
389 line_height,
390 color: text_color,
391 text_align: TextAlign::Left,
392 container_width: w,
393 text_padding: p,
394 });
395 }
396 }
397}
398
399fn make_color(rgb: &[f64; 3]) -> Color {
400 Color::from_device_rgb(rgb[0] as f32, rgb[1] as f32, rgb[2] as f32)
401}
402
403fn wrap_text(text: &str, max_width: f64, metrics: &FontMetrics) -> Vec<String> {
405 let mut lines = Vec::new();
406 let mut current = String::new();
407
408 for word in text.split_whitespace() {
409 if current.is_empty() {
410 current = word.to_string();
411 } else {
412 let candidate = format!("{} {}", current, word);
413 if metrics.measure_width(&candidate) <= max_width {
414 current = candidate;
415 } else {
416 lines.push(current);
417 current = word.to_string();
418 }
419 }
420 }
421 if !current.is_empty() {
422 lines.push(current);
423 }
424 if lines.is_empty() && !text.is_empty() {
425 lines.push(text.to_string());
426 }
427 lines
428}
429
430fn font_family_to_ref(ff: FontFamily) -> &'static str {
432 match ff {
433 FontFamily::Serif => "/F1",
434 FontFamily::SansSerif => "/F2",
435 FontFamily::Monospace => "/F3",
436 }
437}
438
439pub fn execute_commands(commands: &[XfaPaintCommand]) -> Vec<u8> {
445 let mut ops = Vec::new();
446 ops.extend_from_slice(b"q\n");
447
448 let mut image_index = 0usize;
449
450 for cmd in commands {
451 match cmd {
452 XfaPaintCommand::FillRect { x, y, w, h, color } => {
453 let rgba = color.to_rgba();
454 let [r, g, b, _] = rgba.to_rgba8();
455 ops.extend(
456 format!(
457 "{:.3} {:.3} {:.3} rg\n{:.2} {:.2} {:.2} {:.2} re\nf\n",
458 r as f32 / 255.0,
459 g as f32 / 255.0,
460 b as f32 / 255.0,
461 x,
462 y,
463 w,
464 h
465 )
466 .bytes(),
467 );
468 }
469 XfaPaintCommand::StrokeRect {
470 x,
471 y,
472 w,
473 h,
474 color,
475 width,
476 } => {
477 let rgba = color.to_rgba();
478 let [r, g, b, _] = rgba.to_rgba8();
479 ops.extend(format!("{:.2} w\n", width).bytes());
480 ops.extend(
481 format!(
482 "{:.3} {:.3} {:.3} RG\n{:.2} {:.2} {:.2} {:.2} re\nS\n",
483 r as f32 / 255.0,
484 g as f32 / 255.0,
485 b as f32 / 255.0,
486 x,
487 y,
488 w,
489 h
490 )
491 .bytes(),
492 );
493 }
494 XfaPaintCommand::DrawText {
495 x,
496 y,
497 text,
498 font_family,
499 font_size,
500 color,
501 } => {
502 let rgba = color.to_rgba();
503 let [r, g, b, _] = rgba.to_rgba8();
504 let font_ref = font_family_to_ref(*font_family);
505 ops.extend(
506 format!(
507 "BT\n{:.3} {:.3} {:.3} rg\n{} {:.1} Tf\n{:.2} {:.2} Td\n({}) Tj\nET\n",
508 r as f32 / 255.0,
509 g as f32 / 255.0,
510 b as f32 / 255.0,
511 font_ref,
512 font_size,
513 x,
514 y,
515 pdf_escape(text)
516 )
517 .bytes(),
518 );
519 }
520 XfaPaintCommand::DrawMultilineText {
521 x,
522 y,
523 lines,
524 font_family,
525 font_size,
526 line_height,
527 color,
528 text_align,
529 container_width,
530 text_padding,
531 } => {
532 let rgba = color.to_rgba();
533 let [r, g, b, _] = rgba.to_rgba8();
534 let font_ref = font_family_to_ref(*font_family);
535 let p = *text_padding;
536 let content_w = (container_width - p * 2.0).max(0.0);
537 let metrics = FontMetrics {
538 size: *font_size,
539 typeface: *font_family,
540 ..Default::default()
541 };
542 ops.extend(
543 format!(
544 "BT\n{:.3} {:.3} {:.3} rg\n{} {:.1} Tf\n",
545 r as f32 / 255.0,
546 g as f32 / 255.0,
547 b as f32 / 255.0,
548 font_ref,
549 font_size
550 )
551 .bytes(),
552 );
553 let mut prev_tx = x + p;
554 for (i, line) in lines.iter().enumerate() {
555 let line_w = metrics.measure_width(line);
556 let text_x = match text_align {
557 TextAlign::Center => x + p + ((content_w - line_w) / 2.0).max(0.0),
558 TextAlign::Right => x + p + (content_w - line_w).max(0.0),
559 _ => x + p,
560 };
561 if i == 0 {
562 ops.extend(format!("{:.2} {:.2} Td\n", text_x, y).bytes());
563 } else {
564 let dx = text_x - prev_tx;
565 ops.extend(format!("{:.2} {:.2} Td\n", dx, -line_height).bytes());
566 }
567 prev_tx = text_x;
568 ops.extend(format!("({}) Tj\n", pdf_escape(line)).bytes());
569 }
570 ops.extend_from_slice(b"ET\n");
571 }
572 XfaPaintCommand::DrawImage {
573 x,
574 y,
575 w,
576 h,
577 image_data: _,
578 mime_type: _,
579 } => {
580 ops.extend(
581 format!(
582 "q\n{:.2} 0 0 {:.2} {:.2} {:.2} cm\n/Im{} Do\nQ\n",
583 w, h, x, y, image_index
584 )
585 .bytes(),
586 );
587 image_index += 1;
588 }
589 XfaPaintCommand::DrawCheckbox {
590 x,
591 y,
592 w,
593 h,
594 checked,
595 border_color,
596 check_color,
597 border_width,
598 } => {
599 ops.extend(
601 format!(
602 "q\n{:.2} w\n{:.3} {:.3} {:.3} RG\n{:.2} {:.2} {:.2} {:.2} re\nS\n",
603 border_width, border_color[0], border_color[1], border_color[2], x, y, w, h
604 )
605 .bytes(),
606 );
607 if *checked {
608 let m = w.min(*h) * 0.15;
610 ops.extend(
611 format!(
612 "{:.2} w\n{:.3} {:.3} {:.3} RG\n\
613 {:.2} {:.2} m {:.2} {:.2} l S\n\
614 {:.2} {:.2} m {:.2} {:.2} l S\n",
615 border_width.max(1.0),
616 check_color[0],
617 check_color[1],
618 check_color[2],
619 x + m,
620 y + m,
621 x + w - m,
622 y + h - m,
623 x + m,
624 y + h - m,
625 x + w - m,
626 y + m,
627 )
628 .bytes(),
629 );
630 }
631 ops.extend_from_slice(b"Q\n");
632 }
633 }
634 }
635
636 ops.extend_from_slice(b"Q\n");
637 ops
638}
639
640fn pdf_escape(s: &str) -> String {
647 let mut r = String::with_capacity(s.len());
648 for c in s.chars() {
649 match c {
650 '(' => r.push_str("\\("),
651 ')' => r.push_str("\\)"),
652 '\\' => r.push_str("\\\\"),
653 '\x20'..='\x7e' => r.push(c),
655 _ => {
656 if let Some(b) = unicode_to_winansi(c) {
657 use std::fmt::Write;
659 let _ = write!(r, "\\{:03o}", b);
660 } else {
661 r.push('?');
662 }
663 }
664 }
665 }
666 r
667}
668
669fn unicode_to_winansi(c: char) -> Option<u8> {
675 let cp = c as u32;
677 if (0xA0..=0xFF).contains(&cp) {
678 return Some(cp as u8);
679 }
680 match c {
682 '\u{20AC}' => Some(0x80), '\u{201A}' => Some(0x82), '\u{0192}' => Some(0x83), '\u{201E}' => Some(0x84), '\u{2026}' => Some(0x85), '\u{2020}' => Some(0x86), '\u{2021}' => Some(0x87), '\u{02C6}' => Some(0x88), '\u{2030}' => Some(0x89), '\u{0160}' => Some(0x8A), '\u{2039}' => Some(0x8B), '\u{0152}' => Some(0x8C), '\u{017D}' => Some(0x8E), '\u{2018}' => Some(0x91), '\u{2019}' => Some(0x92), '\u{201C}' => Some(0x93), '\u{201D}' => Some(0x94), '\u{2022}' => Some(0x95), '\u{2013}' => Some(0x96), '\u{2014}' => Some(0x97), '\u{02DC}' => Some(0x98), '\u{2122}' => Some(0x99), '\u{0161}' => Some(0x9A), '\u{203A}' => Some(0x9B), '\u{0153}' => Some(0x9C), '\u{017E}' => Some(0x9E), '\u{0178}' => Some(0x9F), _ => None,
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716 use crate::render_bridge::XfaRenderConfig;
717 use xfa_layout_engine::layout::{LayoutNode, LayoutPage};
718 use xfa_layout_engine::types::Rect;
719
720 fn test_config() -> XfaRenderConfig {
721 XfaRenderConfig {
722 default_font: "Helvetica".into(),
723 default_font_size: 10.0,
724 draw_borders: true,
725 border_width: 0.5,
726 border_color: [0.0, 0.0, 0.0],
727 text_color: [0.0, 0.0, 0.0],
728 background_color: Some([1.0, 1.0, 1.0]),
729 text_padding: 2.0,
730 font_map: std::sync::Arc::new(std::collections::HashMap::new()),
731 font_metrics_data: std::sync::Arc::new(std::collections::HashMap::new()),
732 check_button_mark: None,
733 field_values_only: false,
734 }
735 }
736
737 fn field_node(name: &str, x: f64, y: f64, w: f64, h: f64, value: &str) -> LayoutNode {
738 LayoutNode {
739 form_node: xfa_layout_engine::form::FormNodeId(0),
740 rect: Rect {
741 x,
742 y,
743 width: w,
744 height: h,
745 },
746 name: name.into(),
747 content: LayoutContent::Field {
748 value: value.into(),
749 field_kind: xfa_layout_engine::form::FieldKind::Text,
750 font_size: 0.0,
751 font_family: xfa_layout_engine::text::FontFamily::Serif,
752 },
753 children: vec![],
754 style: Default::default(),
755 display_items: vec![],
756 save_items: vec![],
757 }
758 }
759
760 fn field_node_with_border(
761 name: &str,
762 x: f64,
763 y: f64,
764 w: f64,
765 h: f64,
766 value: &str,
767 ) -> LayoutNode {
768 LayoutNode {
769 form_node: xfa_layout_engine::form::FormNodeId(0),
770 rect: Rect {
771 x,
772 y,
773 width: w,
774 height: h,
775 },
776 name: name.into(),
777 content: LayoutContent::Field {
778 value: value.into(),
779 field_kind: xfa_layout_engine::form::FieldKind::Text,
780 font_size: 0.0,
781 font_family: xfa_layout_engine::text::FontFamily::Serif,
782 },
783 children: vec![],
784 style: FormNodeStyle {
785 border_width_pt: Some(0.5),
786 border_color: Some((0, 0, 0)),
787 ..Default::default()
788 },
789 display_items: vec![],
790 save_items: vec![],
791 }
792 }
793
794 #[test]
795 fn field_with_value_and_border_emits_fill_stroke_text() {
796 let page = LayoutPage {
797 width: 612.0,
798 height: 792.0,
799 nodes: vec![field_node_with_border(
800 "name", 10.0, 10.0, 200.0, 20.0, "Hello",
801 )],
802 runtime_instantiated: false,
803 };
804 let cmds = layout_to_commands(&page, &test_config());
805 assert_eq!(cmds.len(), 3); assert!(matches!(cmds[0], XfaPaintCommand::FillRect { .. }));
807 assert!(matches!(cmds[1], XfaPaintCommand::StrokeRect { .. }));
808 assert!(matches!(cmds[2], XfaPaintCommand::DrawText { .. }));
809 }
810
811 #[test]
812 fn field_without_border_style_no_stroke() {
813 let page = LayoutPage {
814 width: 612.0,
815 height: 792.0,
816 nodes: vec![field_node("name", 10.0, 10.0, 200.0, 20.0, "Hello")],
817 runtime_instantiated: false,
818 };
819 let cmds = layout_to_commands(&page, &test_config());
820 assert_eq!(cmds.len(), 2); assert!(matches!(cmds[0], XfaPaintCommand::FillRect { .. }));
823 assert!(matches!(cmds[1], XfaPaintCommand::DrawText { .. }));
824 }
825
826 #[test]
827 fn empty_field_no_text_command() {
828 let page = LayoutPage {
829 width: 612.0,
830 height: 792.0,
831 nodes: vec![field_node("name", 10.0, 10.0, 200.0, 20.0, "")],
832 runtime_instantiated: false,
833 };
834 let cmds = layout_to_commands(&page, &test_config());
835 assert_eq!(cmds.len(), 1); }
837
838 #[test]
839 fn transparent_background_no_fill() {
840 let mut config = test_config();
841 config.background_color = None;
842 let page = LayoutPage {
843 width: 612.0,
844 height: 792.0,
845 nodes: vec![field_node("name", 10.0, 10.0, 200.0, 20.0, "Hi")],
846 runtime_instantiated: false,
847 };
848 let cmds = layout_to_commands(&page, &config);
849 assert_eq!(cmds.len(), 1); assert!(matches!(cmds[0], XfaPaintCommand::DrawText { .. }));
851 }
852
853 #[test]
854 fn multiline_text_emits_multiline_command() {
855 let page = LayoutPage {
856 width: 612.0,
857 height: 792.0,
858 nodes: vec![LayoutNode {
859 form_node: xfa_layout_engine::form::FormNodeId(0),
860 rect: Rect {
861 x: 10.0,
862 y: 10.0,
863 width: 200.0,
864 height: 60.0,
865 },
866 name: "memo".into(),
867 content: LayoutContent::WrappedText {
868 lines: vec!["Line 1".into(), "Line 2".into()],
869 first_line_of_para: vec![true, false],
870 font_size: 10.0,
871 text_align: xfa_layout_engine::types::TextAlign::Left,
872 font_family: xfa_layout_engine::text::FontFamily::SansSerif,
873 space_above_pt: None,
874 space_below_pt: None,
875 from_field: false,
876 },
877 children: vec![],
878 style: Default::default(),
879 display_items: vec![],
880 save_items: vec![],
881 }],
882 runtime_instantiated: false,
883 };
884 let cmds = layout_to_commands(&page, &test_config());
885 assert_eq!(cmds.len(), 2);
887 assert!(matches!(cmds[0], XfaPaintCommand::FillRect { .. }));
888 assert!(matches!(cmds[1], XfaPaintCommand::DrawMultilineText { .. }));
889 }
890
891 #[test]
892 fn multiple_nodes_coordinate_mapping() {
893 let page = LayoutPage {
894 width: 612.0,
895 height: 792.0,
896 nodes: vec![
897 field_node("a", 10.0, 10.0, 100.0, 20.0, "A"),
898 field_node("b", 10.0, 40.0, 100.0, 20.0, "B"),
899 ],
900 runtime_instantiated: false,
901 };
902 let cmds = layout_to_commands(&page, &test_config());
903 assert_eq!(cmds.len(), 4);
905 if let XfaPaintCommand::FillRect { y: y1, .. } = &cmds[0] {
907 if let XfaPaintCommand::FillRect { y: y2, .. } = &cmds[2] {
908 assert!(
909 y1 > y2,
910 "first field (y=10) should have higher PDF y than second (y=40)"
911 );
912 }
913 }
914 }
915
916 #[test]
917 fn checkbox_emits_checkbox_command() {
918 let page = LayoutPage {
919 width: 612.0,
920 height: 792.0,
921 nodes: vec![LayoutNode {
922 form_node: xfa_layout_engine::form::FormNodeId(0),
923 rect: Rect {
924 x: 10.0,
925 y: 10.0,
926 width: 15.0,
927 height: 15.0,
928 },
929 name: "check1".into(),
930 content: LayoutContent::Field {
931 value: "1".into(),
932 field_kind: FieldKind::Checkbox,
933 font_size: 10.0,
934 font_family: FontFamily::SansSerif,
935 },
936 children: vec![],
937 style: Default::default(),
938 display_items: vec![],
939 save_items: vec![],
940 }],
941 runtime_instantiated: false,
942 };
943 let cmds = layout_to_commands(&page, &test_config());
944 assert_eq!(cmds.len(), 1);
945 assert!(matches!(
946 cmds[0],
947 XfaPaintCommand::DrawCheckbox { checked: true, .. }
948 ));
949 }
950
951 #[test]
952 fn pdf_escape_winansi_encoding() {
953 assert_eq!(pdf_escape("Hello"), "Hello");
955 assert_eq!(pdf_escape("a(b)c\\d"), "a\\(b\\)c\\\\d");
957 assert_eq!(pdf_escape("\u{2013}"), "\\226");
959 assert_eq!(pdf_escape("\u{2022}"), "\\225");
961 assert_eq!(pdf_escape("\u{00A9}"), "\\251");
963 assert_eq!(pdf_escape("\u{4E16}"), "?"); }
966
967 #[test]
968 fn child_coordinates_accumulate() {
969 let page = LayoutPage {
970 width: 612.0,
971 height: 792.0,
972 nodes: vec![LayoutNode {
973 form_node: xfa_layout_engine::form::FormNodeId(0),
974 rect: Rect {
975 x: 50.0,
976 y: 100.0,
977 width: 200.0,
978 height: 200.0,
979 },
980 name: "parent".into(),
981 content: LayoutContent::None,
982 children: vec![LayoutNode {
983 form_node: xfa_layout_engine::form::FormNodeId(1),
984 rect: Rect {
985 x: 10.0,
986 y: 10.0,
987 width: 100.0,
988 height: 20.0,
989 },
990 name: "child".into(),
991 content: LayoutContent::Field {
992 value: "Test".into(),
993 field_kind: FieldKind::Text,
994 font_size: 10.0,
995 font_family: FontFamily::SansSerif,
996 },
997 children: vec![],
998 style: Default::default(),
999 display_items: vec![],
1000 save_items: vec![],
1001 }],
1002 style: Default::default(),
1003 display_items: vec![],
1004 save_items: vec![],
1005 }],
1006 runtime_instantiated: false,
1007 };
1008 let config = XfaRenderConfig::default();
1009 let cmds = layout_to_commands(&page, &config);
1010 let text_cmd = cmds
1012 .iter()
1013 .find(|c| matches!(c, XfaPaintCommand::DrawText { .. }));
1014 assert!(text_cmd.is_some());
1015 if let Some(XfaPaintCommand::DrawText { x, .. }) = text_cmd {
1016 assert!(
1018 (*x - 60.0).abs() < 0.1,
1019 "child x should be parent(50) + child(10) = 60, got {}",
1020 x
1021 );
1022 }
1023 }
1024}