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