1use crate::error::Result;
20use crate::font_bridge::font_variant_key;
21use std::collections::HashMap;
22use std::sync::Arc;
23use xfa_layout_engine::form::{DrawContent, FieldKind, FormNodeStyle, RichTextSpan};
24use xfa_layout_engine::layout::{LayoutContent, LayoutDom, LayoutNode, LayoutPage};
25use xfa_layout_engine::text::{FontFamily, FontMetrics};
26use xfa_layout_engine::types::{TextAlign, VerticalAlign};
27
28#[derive(Debug, Clone)]
30pub struct XfaRenderConfig {
31 pub default_font: String,
33 pub default_font_size: f64,
35 pub draw_borders: bool,
37 pub border_width: f64,
39 pub border_color: [f64; 3],
41 pub text_color: [f64; 3],
43 pub background_color: Option<[f64; 3]>,
45 pub text_padding: f64,
47 pub font_map: Arc<HashMap<String, String>>,
55 pub font_metrics_data: Arc<HashMap<String, FontMetricsData>>,
60 pub check_button_mark: Option<String>,
62 pub field_values_only: bool,
67}
68
69#[derive(Debug, Clone)]
71pub struct FontMetricsData {
72 pub widths: Vec<u16>,
74 pub upem: u16,
76 pub ascender: i16,
78 pub descender: i16,
80 pub font_data: Option<Vec<u8>>,
82 pub face_index: u32,
84 pub simple_unicode_to_code: Option<HashMap<u16, u8>>,
90}
91
92#[derive(Debug, Clone)]
94pub struct ImageInfo {
95 pub name: String,
97 pub data: Vec<u8>,
99 pub mime_type: String,
101}
102
103#[derive(Debug, Clone)]
105pub struct PageOverlay {
106 pub content_stream: Vec<u8>,
108 pub images: Vec<ImageInfo>,
110}
111
112impl Default for XfaRenderConfig {
113 fn default() -> Self {
114 Self {
115 default_font: "Helvetica".to_string(),
116 default_font_size: 10.0,
117 draw_borders: true,
118 border_width: 1.0, border_color: [0.0, 0.0, 0.0],
122 text_color: [0.0, 0.0, 0.0],
123 background_color: None,
124 text_padding: xfa_layout_engine::types::DEFAULT_TEXT_PADDING,
125 font_map: Arc::new(HashMap::new()),
126 font_metrics_data: Arc::new(HashMap::new()),
127 check_button_mark: None,
128 field_values_only: false,
129 }
130 }
131}
132
133pub struct CoordinateMapper {
135 page_height: f64,
136 page_width: f64,
137}
138
139impl CoordinateMapper {
140 pub fn new(page_height: f64, page_width: f64) -> Self {
142 Self {
143 page_height,
144 page_width,
145 }
146 }
147
148 pub fn xfa_to_pdf_y(&self, xfa_y: f64, element_height: f64) -> f64 {
150 self.page_height - xfa_y - element_height
151 }
152
153 pub fn page_width(&self) -> f64 {
155 self.page_width
156 }
157}
158
159fn apply_node_style(config: &XfaRenderConfig, style: &FormNodeStyle) -> XfaRenderConfig {
162 let mut cfg = config.clone();
163
164 if let Some((r, g, b)) = style.bg_color {
165 cfg.background_color = Some([r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0]);
166 }
167
168 cfg.draw_borders = false;
169 if let Some(bw) = effective_border_width(style) {
175 cfg.border_width = bw;
176 cfg.draw_borders = true;
177 if let Some((r, g, b)) = style.border_color {
178 cfg.border_color = [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0];
179 }
180 }
181
182 if let Some((r, g, b)) = style.text_color {
183 cfg.text_color = [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0];
184 }
185
186 if let Some(mark) = &style.check_button_mark {
187 cfg.check_button_mark = Some(mark.clone());
188 }
189
190 cfg
191}
192
193fn effective_border_width(style: &FormNodeStyle) -> Option<f64> {
194 if let Some(bw) = style.border_width_pt.filter(|bw| *bw > 0.0) {
195 return Some(bw);
196 }
197
198 style
199 .border_widths
200 .as_ref()
201 .map(|widths| {
202 widths
203 .iter()
204 .zip(style.border_edges.iter())
205 .filter_map(|(width, visible)| (*visible && *width > 0.0).then_some(*width))
206 .fold(0.0, f64::max)
207 })
208 .filter(|bw| *bw > 0.0)
209}
210
211pub fn generate_page_overlay(page: &LayoutPage, config: &XfaRenderConfig) -> Result<PageOverlay> {
213 let mapper = CoordinateMapper::new(page.height, page.width);
214 let mut ops = Vec::new();
215 let mut images: Vec<ImageInfo> = Vec::new();
216 ops.extend_from_slice(b"q\n");
217 render_nodes(
218 &page.nodes,
219 0.0,
220 0.0,
221 &mapper,
222 config,
223 &mut ops,
224 &mut images,
225 );
226 ops.extend_from_slice(b"Q\n");
227 Ok(PageOverlay {
228 content_stream: ops,
229 images,
230 })
231}
232
233pub fn generate_all_overlays(
235 layout: &LayoutDom,
236 config: &XfaRenderConfig,
237) -> Result<Vec<PageOverlay>> {
238 layout
239 .pages
240 .iter()
241 .map(|page| generate_page_overlay(page, config))
242 .collect()
243}
244
245#[derive(Debug, Clone)]
255pub enum RenderNode {
256 Page {
258 width: f64,
260 height: f64,
262 children: Vec<RenderNode>,
264 },
265 Text {
267 x: f64,
269 y: f64,
271 content: String,
273 font: String,
275 size: f64,
277 },
278 Rect {
280 x: f64,
282 y: f64,
284 width: f64,
286 height: f64,
288 fill: Option<[u8; 3]>,
290 stroke: Option<[u8; 3]>,
292 },
293 Image {
295 x: f64,
297 y: f64,
299 width: f64,
301 height: f64,
303 data_len: usize,
305 },
306 Widget {
308 x: f64,
310 y: f64,
312 width: f64,
314 height: f64,
316 field_name: String,
318 value: String,
320 },
321 Group {
323 children: Vec<RenderNode>,
325 },
326}
327
328impl RenderNode {
329 fn fmt_indented(&self, buf: &mut String, depth: usize) {
331 let indent = " ".repeat(depth);
332 match self {
333 RenderNode::Page {
334 width,
335 height,
336 children,
337 } => {
338 buf.push_str(&format!("{indent}Page({width:.1}x{height:.1})\n"));
339 for c in children {
340 c.fmt_indented(buf, depth + 1);
341 }
342 }
343 RenderNode::Text {
344 x,
345 y,
346 content,
347 font,
348 size,
349 } => {
350 let preview: String = content.chars().take(40).collect();
351 let ellipsis = if content.len() > 40 { "…" } else { "" };
352 buf.push_str(&format!(
353 "{indent}Text({x:.1},{y:.1}) font={font} size={size:.1} \"{preview}{ellipsis}\"\n"
354 ));
355 }
356 RenderNode::Rect {
357 x,
358 y,
359 width,
360 height,
361 fill,
362 stroke,
363 } => {
364 let fill_str = fill
365 .map(|[r, g, b]| format!("fill=#{r:02X}{g:02X}{b:02X}"))
366 .unwrap_or_default();
367 let stroke_str = stroke
368 .map(|[r, g, b]| format!("stroke=#{r:02X}{g:02X}{b:02X}"))
369 .unwrap_or_default();
370 buf.push_str(&format!(
371 "{indent}Rect({x:.1},{y:.1} {width:.1}x{height:.1}) {fill_str} {stroke_str}\n"
372 ));
373 }
374 RenderNode::Image {
375 x,
376 y,
377 width,
378 height,
379 data_len,
380 } => {
381 buf.push_str(&format!(
382 "{indent}Image({x:.1},{y:.1} {width:.1}x{height:.1}) bytes={data_len}\n"
383 ));
384 }
385 RenderNode::Widget {
386 x,
387 y,
388 width,
389 height,
390 field_name,
391 value,
392 } => {
393 let preview: String = value.chars().take(30).collect();
394 buf.push_str(&format!(
395 "{indent}Widget({x:.1},{y:.1} {width:.1}x{height:.1}) name={field_name} value=\"{preview}\"\n"
396 ));
397 }
398 RenderNode::Group { children } => {
399 buf.push_str(&format!("{indent}Group\n"));
400 for c in children {
401 c.fmt_indented(buf, depth + 1);
402 }
403 }
404 }
405 }
406}
407
408#[derive(Debug, Clone)]
413pub struct RenderTree {
414 pub pages: Vec<RenderNode>,
416}
417
418impl RenderTree {
419 pub fn to_debug_string(&self) -> String {
421 let mut buf = String::new();
422 for page in &self.pages {
423 page.fmt_indented(&mut buf, 0);
424 }
425 buf
426 }
427}
428
429pub fn layout_dom_to_render_tree(layout: &LayoutDom, config: &XfaRenderConfig) -> RenderTree {
434 let pages = layout
435 .pages
436 .iter()
437 .map(|page| {
438 let mapper = CoordinateMapper::new(page.height, page.width);
439 let children = render_tree_nodes(&page.nodes, 0.0, 0.0, &mapper, config);
440 RenderNode::Page {
441 width: page.width,
442 height: page.height,
443 children,
444 }
445 })
446 .collect();
447 RenderTree { pages }
448}
449
450fn render_tree_nodes(
451 nodes: &[LayoutNode],
452 parent_x: f64,
453 parent_y: f64,
454 mapper: &CoordinateMapper,
455 config: &XfaRenderConfig,
456) -> Vec<RenderNode> {
457 let mut result = Vec::new();
458 for node in nodes {
459 let abs_x = node.rect.x + parent_x;
460 let abs_y = node.rect.y + parent_y;
461 let w = node.rect.width;
462 let h = node.rect.height;
463 let pdf_y = mapper.xfa_to_pdf_y(abs_y, h);
464 let node_cfg = apply_node_style(config, &node.style);
465
466 let fill = node_cfg.background_color.map(|c| {
468 [
469 (c[0] * 255.0) as u8,
470 (c[1] * 255.0) as u8,
471 (c[2] * 255.0) as u8,
472 ]
473 });
474 let stroke = if node_cfg.draw_borders && node_cfg.border_width > 0.0 {
475 let bc = node_cfg.border_color;
476 Some([
477 (bc[0] * 255.0) as u8,
478 (bc[1] * 255.0) as u8,
479 (bc[2] * 255.0) as u8,
480 ])
481 } else {
482 None
483 };
484 if fill.is_some() || stroke.is_some() {
485 result.push(RenderNode::Rect {
486 x: abs_x,
487 y: pdf_y,
488 width: w,
489 height: h,
490 fill,
491 stroke,
492 });
493 }
494
495 let leaf = match &node.content {
496 LayoutContent::Field {
497 value,
498 field_kind,
499 font_size,
500 font_family,
501 } => {
502 use xfa_layout_engine::form::FieldKind;
503 match field_kind {
504 FieldKind::Checkbox | FieldKind::Radio => Some(RenderNode::Widget {
505 x: abs_x,
506 y: pdf_y,
507 width: w,
508 height: h,
509 field_name: node.name.clone(),
510 value: value.clone(),
511 }),
512 _ => {
513 let font_ref = config
514 .font_map
515 .get(&font_bridge_key_for_tree(*font_family))
516 .cloned()
517 .unwrap_or_else(|| node_cfg.default_font.clone());
518 if !value.is_empty() {
519 Some(RenderNode::Text {
520 x: abs_x,
521 y: pdf_y,
522 content: value.clone(),
523 font: font_ref,
524 size: if *font_size > 0.0 {
525 *font_size
526 } else {
527 node_cfg.default_font_size
528 },
529 })
530 } else {
531 Some(RenderNode::Widget {
532 x: abs_x,
533 y: pdf_y,
534 width: w,
535 height: h,
536 field_name: node.name.clone(),
537 value: value.clone(),
538 })
539 }
540 }
541 }
542 }
543 LayoutContent::Text(text) => {
544 if text.is_empty() {
545 None
546 } else {
547 Some(RenderNode::Text {
548 x: abs_x,
549 y: pdf_y,
550 content: text.clone(),
551 font: node_cfg.default_font.clone(),
552 size: node_cfg.default_font_size,
553 })
554 }
555 }
556 LayoutContent::WrappedText {
557 lines, font_size, ..
558 } => {
559 if lines.is_empty() {
560 None
561 } else {
562 Some(RenderNode::Text {
563 x: abs_x,
564 y: pdf_y,
565 content: lines.join(" "),
566 font: node_cfg.default_font.clone(),
567 size: *font_size,
568 })
569 }
570 }
571 LayoutContent::Image { data, .. } => Some(RenderNode::Image {
572 x: abs_x,
573 y: pdf_y,
574 width: w,
575 height: h,
576 data_len: data.len(),
577 }),
578 LayoutContent::Draw(_) | LayoutContent::None => None,
579 };
580 if let Some(leaf_node) = leaf {
581 result.push(leaf_node);
582 }
583
584 if !node.children.is_empty() {
586 let child_x = abs_x + node.style.inset_left_pt.unwrap_or(0.0);
587 let child_y = abs_y + node.style.inset_top_pt.unwrap_or(0.0);
588 let child_nodes = render_tree_nodes(&node.children, child_x, child_y, mapper, config);
589 if !child_nodes.is_empty() {
590 result.push(RenderNode::Group {
591 children: child_nodes,
592 });
593 }
594 }
595 }
596 result
597}
598
599fn font_bridge_key_for_tree(family: xfa_layout_engine::text::FontFamily) -> String {
601 use xfa_layout_engine::text::FontFamily;
602 match family {
603 FontFamily::SansSerif => "sans-serif".to_string(),
604 FontFamily::Monospace => "monospace".to_string(),
605 FontFamily::Serif => "serif".to_string(),
606 }
607}
608
609pub fn generate_field_values_overlays(
613 layout: &LayoutDom,
614 config: &XfaRenderConfig,
615) -> Result<Vec<PageOverlay>> {
616 let mut fv_config = config.clone();
617 fv_config.field_values_only = true;
618 layout
619 .pages
620 .iter()
621 .map(|page| generate_page_overlay(page, &fv_config))
622 .collect()
623}
624
625fn render_nodes(
626 nodes: &[LayoutNode],
627 parent_x: f64,
628 parent_y: f64,
629 mapper: &CoordinateMapper,
630 config: &XfaRenderConfig,
631 ops: &mut Vec<u8>,
632 images: &mut Vec<ImageInfo>,
633) {
634 for node in nodes {
635 let abs_x = node.rect.x + parent_x;
636 let abs_y = node.rect.y + parent_y;
637 let w = node.rect.width;
638 let h = node.rect.height;
639 let pdf_y = mapper.xfa_to_pdf_y(abs_y, h);
640
641 let node_config = apply_node_style(config, &node.style);
642
643 let inset_l = node.style.inset_left_pt.unwrap_or(0.0);
647 let inset_t = node.style.inset_top_pt.unwrap_or(0.0);
648 let inset_r = node.style.inset_right_pt.unwrap_or(0.0);
649 let inset_b = node.style.inset_bottom_pt.unwrap_or(0.0);
650 let inner_w = (w - inset_l - inset_r).max(0.0);
651 let inner_h = (h - inset_t - inset_b).max(0.0);
652 let (caption_font_size, caption_font_family) =
653 caption_font_for_content(&node.content, &node.style, &node_config);
654 let is_button = matches!(
655 &node.content,
656 LayoutContent::Field {
657 field_kind: FieldKind::Button,
658 ..
659 }
660 );
661 let caption_reserve = if is_button {
662 0.0
663 } else {
664 effective_caption_reserve(
665 &node.style,
666 caption_font_size,
667 caption_font_family,
668 &node_config,
669 )
670 };
671
672 let (cap_dx, cap_dy, val_w, val_h) =
674 caption_value_offset(&node.style, caption_reserve, inner_w, inner_h);
675 let val_x = abs_x + inset_l + cap_dx;
676 let val_y_offset = inset_t + cap_dy;
677 let val_pdf_y = mapper.xfa_to_pdf_y(abs_y + val_y_offset, val_h);
678
679 if !matches!(node.content, LayoutContent::Field { .. }) && !config.field_values_only {
680 let border_radius = node.style.border_radius_pt.unwrap_or(0.0);
681 let border_style = node.style.border_style.as_deref();
682 let (bx, by, bw, bh) = if node.style.caption_text.is_some() {
685 (val_x, val_pdf_y, val_w, val_h)
686 } else {
687 let inner_pdf_y = mapper.xfa_to_pdf_y(abs_y + inset_t, inner_h);
688 (abs_x + inset_l, inner_pdf_y, inner_w, inner_h)
689 };
690 if let Some(bg) = &node_config.background_color {
691 write_ops(
692 ops,
693 format_args!("{:.3} {:.3} {:.3} rg\n", bg[0], bg[1], bg[2]),
694 );
695 emit_rect_path(ops, bx, by, bw, bh, border_radius);
696 ops.extend_from_slice(b"f\n");
697 }
698 if node_config.draw_borders && node_config.border_width > 0.0 && bw > 0.0 && bh > 0.0 {
699 let bwid = node_config.border_width;
700 let bc = node_config.border_color;
701 write_ops(
702 ops,
703 format_args!("{:.2} w\n{:.3} {:.3} {:.3} RG\n", bwid, bc[0], bc[1], bc[2]),
704 );
705 let per_edge = node.style.border_colors.map(|cs| {
706 cs.map(|(r, g, b)| [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0])
707 });
708 let per_edge_widths = node.style.border_widths.as_ref();
709 apply_border_dash(ops, border_style);
710 let edges = node.style.border_edges;
711 if per_edge.is_some() || per_edge_widths.is_some() {
712 emit_individual_edges(
713 ops,
714 bx,
715 by,
716 bw,
717 bh,
718 &edges,
719 per_edge.as_ref(),
720 per_edge_widths,
721 bwid,
722 );
723 } else if edges[0] && edges[1] && edges[2] && edges[3] {
724 emit_rect_path(ops, bx, by, bw, bh, border_radius);
725 ops.extend_from_slice(b"S\n");
726 } else {
727 emit_individual_edges(ops, bx, by, bw, bh, &edges, None, None, bwid);
728 }
729 reset_border_dash(ops, border_style);
730 }
731 }
732
733 ops.extend_from_slice(b"q\n");
736 let skip_line_clip = draw_line_span_enabled()
742 && (w <= 0.01 || h <= 0.01)
743 && matches!(
744 &node.content,
745 LayoutContent::Draw(DrawContent::Line { x1, y1, x2, y2 })
746 if *x1 == 0.0 && *y1 == 0.0 && *x2 == 0.0 && *y2 == 0.0
747 );
748 if !skip_line_clip {
749 write_ops(
750 ops,
751 format_args!("{:.2} {:.2} {:.2} {:.2} re W n\n", abs_x, pdf_y, w, h),
752 );
753 }
754
755 let is_bold = node.style.font_weight.as_deref() == Some("bold");
756
757 let is_field = matches!(&node.content, LayoutContent::Field { .. });
761 let field_caption_needs_post_body_render =
762 is_field && matches!(node.style.caption_placement.as_deref(), Some("top"));
763 if node.style.caption_text.is_some()
764 && !is_button
765 && (!is_field || !field_caption_needs_post_body_render)
766 && !config.field_values_only
767 {
768 render_caption(
769 abs_x + inset_l,
770 mapper.xfa_to_pdf_y(abs_y + inset_t, inner_h),
771 inner_w,
772 inner_h,
773 caption_reserve,
774 caption_font_size,
775 caption_font_family,
776 &node.style,
777 &node_config,
778 ops,
779 );
780 }
781
782 match &node.content {
783 LayoutContent::Field {
784 value,
785 field_kind,
786 font_size,
787 font_family,
788 } => {
789 match field_kind {
790 FieldKind::Checkbox => render_checkbox(
791 val_x,
792 val_pdf_y,
793 val_w,
794 val_h,
795 value,
796 &node.style,
797 &node_config,
798 ops,
799 ),
800 FieldKind::Radio => render_radio(
801 val_x,
802 val_pdf_y,
803 val_w,
804 val_h,
805 value,
806 &node.style,
807 &node_config,
808 ops,
809 ),
810 FieldKind::Dropdown => render_dropdown(
811 val_x,
812 val_pdf_y,
813 val_w,
814 val_h,
815 value,
816 *font_size,
817 *font_family,
818 &node.style,
819 &node_config,
820 ops,
821 &node.display_items,
822 &node.save_items,
823 ),
824 FieldKind::Button => {
825 let label = if value.is_empty() {
826 node.style.caption_text.as_deref().unwrap_or("")
827 } else {
828 value
829 };
830 if !label.is_empty() {
831 render_button(
832 val_x,
833 val_pdf_y,
834 val_w,
835 val_h,
836 label,
837 *font_size,
838 *font_family,
839 &node.style,
840 &node_config,
841 ops,
842 )
843 }
844 }
845 FieldKind::PasswordEdit => {
846 let masked_value: String = value.chars().map(|_| '•').collect();
847 render_field(
848 val_x,
849 val_pdf_y,
850 val_w,
851 val_h,
852 &masked_value,
853 *font_size,
854 *font_family,
855 &node.style,
856 &node_config,
857 ops,
858 )
859 }
860 FieldKind::Barcode => render_barcode(
861 val_x,
862 val_pdf_y,
863 val_w,
864 val_h,
865 value,
866 &node.style,
867 &node_config,
868 ops,
869 ),
870 FieldKind::Signature => render_signature(
871 val_x,
872 val_pdf_y,
873 val_w,
874 val_h,
875 value,
876 &node.style,
877 &node_config,
878 ops,
879 ),
880 _ => {
881 let display_val = if node.style.format_pattern.is_some() {
882 crate::appearance_bridge::format_value(
883 value,
884 node.style.format_pattern.as_deref(),
885 )
886 } else if matches!(field_kind, FieldKind::NumericEdit) {
887 crate::appearance_bridge::format_numeric_default(value)
888 } else {
889 value.to_string()
890 };
891 render_field(
892 val_x,
893 val_pdf_y,
894 val_w,
895 val_h,
896 &display_val,
897 *font_size,
898 *font_family,
899 &node.style,
900 &node_config,
901 ops,
902 )
903 }
904 }
905
906 if node.style.caption_text.is_some()
907 && !is_button
908 && field_caption_needs_post_body_render
909 && !config.field_values_only
910 {
911 render_caption(
917 abs_x + inset_l,
918 mapper.xfa_to_pdf_y(abs_y + inset_t, inner_h),
919 inner_w,
920 inner_h,
921 caption_reserve,
922 caption_font_size,
923 caption_font_family,
924 &node.style,
925 &node_config,
926 ops,
927 );
928 }
929 }
930 LayoutContent::Text(_) if config.field_values_only => {}
931 LayoutContent::Text(text) => {
932 let inner_pdf_y = mapper.xfa_to_pdf_y(abs_y + inset_t, inner_h);
933 render_text(
934 abs_x + inset_l,
935 inner_pdf_y,
936 inner_w,
937 inner_h,
938 text,
939 &node.style,
940 &node_config,
941 ops,
942 )
943 }
944 LayoutContent::WrappedText {
945 from_field: false, ..
946 } if config.field_values_only => {}
947 LayoutContent::WrappedText {
948 lines,
949 first_line_of_para,
950 font_size,
951 text_align,
952 font_family,
953 ..
954 } => {
955 let use_rich = node.style.rich_text_spans.as_ref().is_some_and(|spans| {
961 spans.len() > 1
962 || spans.iter().any(|s| {
963 s.font_size.is_some()
964 || s.font_family.is_some()
965 || s.font_weight.is_some()
966 || s.font_style.is_some()
967 || s.text_color.is_some()
968 || s.underline
969 })
970 });
971 if use_rich {
972 if let Some(ref spans) = node.style.rich_text_spans {
973 render_rich_multiline(
974 val_x,
975 val_w,
976 val_h,
977 lines,
978 first_line_of_para,
979 spans,
980 *font_size,
981 *text_align,
982 *font_family,
983 mapper,
984 abs_y + val_y_offset,
985 &node.style,
986 &node_config,
987 ops,
988 );
989 }
990 } else {
991 render_multiline(
992 val_x,
993 val_pdf_y,
994 val_w,
995 val_h,
996 lines,
997 first_line_of_para,
998 *font_size,
999 *text_align,
1000 *font_family,
1001 is_bold,
1002 mapper,
1003 abs_y + val_y_offset,
1004 &node.style,
1005 &node_config,
1006 ops,
1007 );
1008 }
1009 }
1010 LayoutContent::Image { .. } if config.field_values_only => {}
1011 LayoutContent::Image { data, mime_type } => {
1012 let img_name = format!("XImg{}", images.len());
1013 ops.extend(crate::image_bridge::render_image_ops(
1014 &img_name, abs_x, pdf_y, w, h,
1015 ));
1016 images.push(ImageInfo {
1017 name: img_name,
1018 data: data.clone(),
1019 mime_type: mime_type.clone(),
1020 });
1021 }
1022 LayoutContent::Draw(_) if config.field_values_only => {}
1023 LayoutContent::Draw(draw_content) => {
1024 render_draw(
1025 draw_content,
1026 abs_x,
1027 pdf_y,
1028 w,
1029 h,
1030 &node.style,
1031 &node_config,
1032 ops,
1033 );
1034 }
1035 LayoutContent::None => {}
1036 }
1037
1038 ops.extend_from_slice(b"Q\n");
1040
1041 if !node.children.is_empty() {
1042 let child_origin_x = abs_x + node.style.inset_left_pt.unwrap_or(0.0);
1047 let child_origin_y = abs_y + node.style.inset_top_pt.unwrap_or(0.0);
1048 render_nodes(
1049 &node.children,
1050 child_origin_x,
1051 child_origin_y,
1052 mapper,
1053 config,
1054 ops,
1055 images,
1056 );
1057 }
1058 }
1059}
1060
1061fn emit_rect_path(ops: &mut Vec<u8>, x: f64, y: f64, w: f64, h: f64, radius: f64) {
1063 if radius <= 0.0 {
1064 write_ops(
1065 ops,
1066 format_args!("{:.2} {:.2} {:.2} {:.2} re\n", x, y, w, h),
1067 );
1068 } else {
1069 let r = radius.min(w / 2.0).min(h / 2.0);
1070 let k = r * 0.5522847498;
1071 write_ops(
1072 ops,
1073 format_args!(
1074 "{:.2} {:.2} m\n\
1075 {:.2} {:.2} l\n\
1076 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
1077 {:.2} {:.2} l\n\
1078 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
1079 {:.2} {:.2} l\n\
1080 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
1081 {:.2} {:.2} l\n\
1082 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
1083 h\n",
1084 x,
1085 y + r,
1086 x,
1087 y + h - r,
1088 x,
1089 y + h - r + k,
1090 x + r - k,
1091 y + h,
1092 x + r,
1093 y + h,
1094 x + w - r,
1095 y + h,
1096 x + w - r + k,
1097 y + h,
1098 x + w,
1099 y + h - r + k,
1100 x + w,
1101 y + h - r,
1102 x + w,
1103 y + r,
1104 x + w,
1105 y + r - k,
1106 x + w - r + k,
1107 y,
1108 x + w - r,
1109 y,
1110 x + r,
1111 y,
1112 x + r - k,
1113 y,
1114 x,
1115 y + r - k,
1116 x,
1117 y + r,
1118 ),
1119 );
1120 }
1121}
1122
1123#[allow(clippy::too_many_arguments)]
1125fn emit_individual_edges(
1126 ops: &mut Vec<u8>,
1127 x: f64,
1128 y: f64,
1129 w: f64,
1130 h: f64,
1131 edges: &[bool; 4],
1132 colors: Option<&[[f64; 3]; 4]>,
1133 widths: Option<&[f64; 4]>,
1134 default_width: f64,
1135) {
1136 if edges[0] {
1137 let ww = widths.map(|w| w[0]).unwrap_or(default_width);
1138 if let Some(c) = colors.map(|c| &c[0]) {
1139 write_ops(
1140 ops,
1141 format_args!("{:.2} w\n{:.3} {:.3} {:.3} RG\n", ww, c[0], c[1], c[2]),
1142 );
1143 } else {
1144 write_ops(ops, format_args!("{:.2} w\n", ww));
1145 }
1146 write_ops(
1147 ops,
1148 format_args!("{:.2} {:.2} m {:.2} {:.2} l S\n", x, y + h, x + w, y + h),
1149 );
1150 }
1151 if edges[1] {
1152 let ww = widths.map(|w| w[1]).unwrap_or(default_width);
1153 if let Some(c) = colors.map(|c| &c[1]) {
1154 write_ops(
1155 ops,
1156 format_args!("{:.2} w\n{:.3} {:.3} {:.3} RG\n", ww, c[0], c[1], c[2]),
1157 );
1158 } else {
1159 write_ops(ops, format_args!("{:.2} w\n", ww));
1160 }
1161 write_ops(
1162 ops,
1163 format_args!("{:.2} {:.2} m {:.2} {:.2} l S\n", x + w, y, x + w, y + h),
1164 );
1165 }
1166 if edges[2] {
1167 let ww = widths.map(|w| w[2]).unwrap_or(default_width);
1168 if let Some(c) = colors.map(|c| &c[2]) {
1169 write_ops(
1170 ops,
1171 format_args!("{:.2} w\n{:.3} {:.3} {:.3} RG\n", ww, c[0], c[1], c[2]),
1172 );
1173 } else {
1174 write_ops(ops, format_args!("{:.2} w\n", ww));
1175 }
1176 write_ops(
1177 ops,
1178 format_args!("{:.2} {:.2} m {:.2} {:.2} l S\n", x, y, x + w, y),
1179 );
1180 }
1181 if edges[3] {
1182 let ww = widths.map(|w| w[3]).unwrap_or(default_width);
1183 if let Some(c) = colors.map(|c| &c[3]) {
1184 write_ops(
1185 ops,
1186 format_args!("{:.2} w\n{:.3} {:.3} {:.3} RG\n", ww, c[0], c[1], c[2]),
1187 );
1188 } else {
1189 write_ops(ops, format_args!("{:.2} w\n", ww));
1190 }
1191 write_ops(
1192 ops,
1193 format_args!("{:.2} {:.2} m {:.2} {:.2} l S\n", x, y, x, y + h),
1194 );
1195 }
1196}
1197
1198fn apply_border_dash(ops: &mut Vec<u8>, style: Option<&str>) {
1199 match style {
1200 Some("dashed") => write_ops(ops, format_args!("[3 2] 0 d\n")),
1201 Some("dotted") => write_ops(ops, format_args!("[1 1] 0 d\n")),
1202 _ => {}
1203 }
1204}
1205
1206fn reset_border_dash(ops: &mut Vec<u8>, style: Option<&str>) {
1207 if matches!(style, Some("dashed") | Some("dotted")) {
1208 write_ops(ops, format_args!("[] 0 d\n"));
1209 }
1210}
1211
1212fn emit_3d_border(
1217 ops: &mut Vec<u8>,
1218 x: f64,
1219 y: f64,
1220 w: f64,
1221 h: f64,
1222 line_w: f64,
1223 style: Option<&str>,
1224) {
1225 let dark = [0.502, 0.502, 0.502]; let light = [0.831, 0.831, 0.831]; let (tl, br) = match style {
1228 Some("lowered") => (dark, light),
1229 _ => (light, dark), };
1231 write_ops(ops, format_args!("{:.2} w\n", line_w));
1232 write_ops(
1234 ops,
1235 format_args!(
1236 "{:.3} {:.3} {:.3} RG\n{:.2} {:.2} m {:.2} {:.2} l S\n",
1237 tl[0],
1238 tl[1],
1239 tl[2],
1240 x,
1241 y + h,
1242 x + w,
1243 y + h,
1244 ),
1245 );
1246 write_ops(
1248 ops,
1249 format_args!("{:.2} {:.2} m {:.2} {:.2} l S\n", x, y + h, x, y,),
1250 );
1251 write_ops(
1253 ops,
1254 format_args!(
1255 "{:.3} {:.3} {:.3} RG\n{:.2} {:.2} m {:.2} {:.2} l S\n",
1256 br[0],
1257 br[1],
1258 br[2],
1259 x,
1260 y,
1261 x + w,
1262 y,
1263 ),
1264 );
1265 write_ops(
1267 ops,
1268 format_args!("{:.2} {:.2} m {:.2} {:.2} l S\n", x + w, y, x + w, y + h,),
1269 );
1270}
1271
1272fn resolve_font_ref<'a>(
1277 font_map: &'a HashMap<String, String>,
1278 node_style: &FormNodeStyle,
1279 font_family: FontFamily,
1280) -> &'a str {
1281 if let Some(typeface) = &node_style.font_family {
1282 let vkey = font_variant_key(
1284 typeface,
1285 node_style.font_weight.as_deref(),
1286 node_style.font_style.as_deref(),
1287 );
1288 if let Some(mapped) = font_map.get(&vkey) {
1289 return mapped;
1290 }
1291 if let Some(mapped) = font_map.get(typeface) {
1293 return mapped;
1294 }
1295 }
1296 match font_family {
1297 FontFamily::Serif => "/F1",
1298 FontFamily::SansSerif => "/F2",
1299 FontFamily::Monospace => "/F3",
1300 }
1301}
1302
1303fn emit_text_style_ops(node_style: &FormNodeStyle, ops: &mut Vec<u8>) {
1306 if let Some(h_scale) = node_style.font_horizontal_scale {
1307 if (h_scale - 1.0).abs() > 0.001 {
1308 write_ops(ops, format_args!("{:.1} Tz\n", h_scale * 100.0));
1309 }
1310 }
1311 if let Some(spacing) = node_style.letter_spacing_pt {
1312 if spacing.abs() > 0.001 {
1313 write_ops(ops, format_args!("{:.3} Tc\n", spacing));
1314 }
1315 }
1316}
1317
1318fn is_bold_style(node_style: &FormNodeStyle) -> bool {
1320 node_style.font_weight.as_deref() == Some("bold")
1321}
1322
1323fn style_uses_real_bold_variant(
1326 font_map: &HashMap<String, String>,
1327 node_style: &FormNodeStyle,
1328) -> bool {
1329 if !is_bold_style(node_style) {
1330 return false;
1331 }
1332 let Some(typeface) = node_style.font_family.as_deref() else {
1333 return false;
1334 };
1335 let vkey = font_variant_key(
1336 typeface,
1337 node_style.font_weight.as_deref(),
1338 node_style.font_style.as_deref(),
1339 );
1340 font_map.contains_key(&vkey)
1341}
1342
1343fn emit_synthetic_bold_ops(
1347 font_map: &HashMap<String, String>,
1348 node_style: &FormNodeStyle,
1349 font_size: f64,
1350 text_color: &[f64; 3],
1351 ops: &mut Vec<u8>,
1352) {
1353 if is_bold_style(node_style) && !style_uses_real_bold_variant(font_map, node_style) {
1354 let stroke_w = font_size * 0.03;
1355 write_ops(
1356 ops,
1357 format_args!(
1358 "2 Tr\n{:.4} w\n{:.3} {:.3} {:.3} RG\n",
1359 stroke_w, text_color[0], text_color[1], text_color[2],
1360 ),
1361 );
1362 }
1363}
1364
1365fn reset_synthetic_bold_ops(
1367 font_map: &HashMap<String, String>,
1368 node_style: &FormNodeStyle,
1369 ops: &mut Vec<u8>,
1370) {
1371 if is_bold_style(node_style) && !style_uses_real_bold_variant(font_map, node_style) {
1372 write_ops(ops, format_args!("0 Tr\n"));
1373 }
1374}
1375
1376fn reset_text_style_ops(node_style: &FormNodeStyle, ops: &mut Vec<u8>) {
1378 if node_style
1379 .font_horizontal_scale
1380 .is_some_and(|s| (s - 1.0).abs() > 0.001)
1381 {
1382 write_ops(ops, format_args!("100 Tz\n"));
1383 }
1384 if node_style
1385 .letter_spacing_pt
1386 .is_some_and(|s| s.abs() > 0.001)
1387 {
1388 write_ops(ops, format_args!("0 Tc\n"));
1389 }
1390}
1391
1392fn ascender_pt(font_metrics: &FontMetrics, font_size: f64) -> f64 {
1394 if let (Some(asc), Some(upem)) = (font_metrics.resolved_ascender, font_metrics.resolved_upem) {
1395 if upem > 0 {
1396 return asc as f64 / upem as f64 * font_size;
1397 }
1398 }
1399 font_size
1400}
1401
1402fn build_font_metrics(
1407 font_size: f64,
1408 font_family: FontFamily,
1409 node_style: &FormNodeStyle,
1410 config: &XfaRenderConfig,
1411) -> FontMetrics {
1412 let mut metrics = FontMetrics {
1413 size: font_size,
1414 typeface: font_family,
1415 ..Default::default()
1416 };
1417 if let Some(typeface) = &node_style.font_family {
1418 let vkey = font_variant_key(
1419 typeface,
1420 node_style.font_weight.as_deref(),
1421 node_style.font_style.as_deref(),
1422 );
1423 let data = config
1424 .font_metrics_data
1425 .get(&vkey)
1426 .or_else(|| config.font_metrics_data.get(typeface));
1427 if let Some(data) = data {
1428 metrics.resolved_widths = Some(data.widths.clone());
1429 metrics.resolved_upem = Some(data.upem);
1430 metrics.resolved_ascender = Some(data.ascender);
1431 metrics.resolved_descender = Some(data.descender);
1432 }
1433 }
1434 metrics
1435}
1436
1437fn caption_own_font_enabled() -> bool {
1441 !matches!(
1442 std::env::var("XFA_CAPTION_OWN_FONT").as_deref(),
1443 Ok("0") | Ok("off") | Ok("false") | Ok("OFF") | Ok("FALSE")
1444 )
1445}
1446
1447fn caption_font_for_content(
1448 content: &LayoutContent,
1449 node_style: &FormNodeStyle,
1450 config: &XfaRenderConfig,
1451) -> (f64, FontFamily) {
1452 let (field_size, field_family) = match content {
1453 LayoutContent::Field {
1454 font_size,
1455 font_family,
1456 ..
1457 }
1458 | LayoutContent::WrappedText {
1459 font_size,
1460 font_family,
1461 ..
1462 } => (*font_size, *font_family),
1463 _ => (
1464 node_style.font_size.unwrap_or(config.default_font_size),
1465 FontFamily::SansSerif,
1466 ),
1467 };
1468 if caption_own_font_enabled() {
1471 if let Some(cap_face) = &node_style.caption_font_family {
1472 return (
1473 node_style.caption_font_size.unwrap_or(field_size),
1474 classify_font_family(cap_face),
1475 );
1476 }
1477 }
1478 (field_size, field_family)
1479}
1480
1481fn effective_caption_reserve(
1482 style: &FormNodeStyle,
1483 font_size: f64,
1484 font_family: FontFamily,
1485 config: &XfaRenderConfig,
1486) -> f64 {
1487 let caption_text = match style.caption_text.as_deref() {
1488 Some(text) if !text.is_empty() => text,
1489 _ => return 0.0,
1490 };
1491 if let Some(reserve) = style.caption_reserve {
1492 return reserve.max(0.0);
1493 }
1494 let fs = if font_size > 0.0 {
1495 font_size
1496 } else {
1497 config.default_font_size
1498 };
1499 let metrics = build_font_metrics(fs, font_family, style, config);
1500 match style.caption_placement.as_deref().unwrap_or("left") {
1501 "left" | "right" => metrics.measure_width(caption_text),
1502 "top" | "bottom" => metrics.line_height_pt(),
1503 _ => 0.0,
1504 }
1505}
1506
1507fn caption_value_offset(
1512 style: &FormNodeStyle,
1513 reserve: f64,
1514 w: f64,
1515 h: f64,
1516) -> (f64, f64, f64, f64) {
1517 if reserve <= 0.0 || style.caption_text.is_none() {
1518 return (0.0, 0.0, w, h);
1519 }
1520 match style.caption_placement.as_deref().unwrap_or("left") {
1521 "left" => (reserve, 0.0, (w - reserve).max(0.0), h),
1522 "right" => (0.0, 0.0, (w - reserve).max(0.0), h),
1523 "top" => (0.0, reserve, w, (h - reserve).max(0.0)),
1524 "bottom" => (0.0, 0.0, w, (h - reserve).max(0.0)),
1525 _ => (0.0, 0.0, w, h),
1526 }
1527}
1528
1529#[allow(clippy::too_many_arguments)]
1538fn render_caption(
1539 x: f64,
1540 pdf_y: f64,
1541 w: f64,
1542 h: f64,
1543 caption_reserve: f64,
1544 font_size: f64,
1545 font_family: FontFamily,
1546 node_style: &FormNodeStyle,
1547 config: &XfaRenderConfig,
1548 ops: &mut Vec<u8>,
1549) {
1550 let caption_text = match &node_style.caption_text {
1551 Some(t) if !t.is_empty() => t,
1552 _ => return,
1553 };
1554 let caption_placement = node_style.caption_placement.as_deref().unwrap_or("left");
1555 let fs = if font_size > 0.0 {
1556 font_size
1557 } else {
1558 config.default_font_size
1559 };
1560 let cap_owned;
1564 let cap_style: &FormNodeStyle = match &node_style.caption_font_family {
1565 Some(cf) if caption_own_font_enabled() => {
1566 cap_owned = FormNodeStyle {
1567 font_family: Some(cf.clone()),
1568 ..node_style.clone()
1569 };
1570 &cap_owned
1571 }
1572 _ => node_style,
1573 };
1574 let metrics = build_font_metrics(fs, font_family, cap_style, config);
1575 let font_ref = resolve_font_ref(&config.font_map, cap_style, font_family);
1576 let idh_metrics = lookup_font_metrics(cap_style, config);
1577
1578 let (cap_x, cap_y, cap_w, cap_h) = match caption_placement {
1580 "left" => (x, pdf_y, caption_reserve, h),
1581 "right" => (x + w - caption_reserve, pdf_y, caption_reserve, h),
1582 "top" => (x, pdf_y + h - caption_reserve, w, caption_reserve),
1583 "bottom" => (x, pdf_y, w, caption_reserve),
1584 _ => (x, pdf_y, caption_reserve, h),
1585 };
1586
1587 let is_multiline = caption_text.contains('\n') || metrics.measure_width(caption_text) > cap_w;
1590
1591 if is_multiline {
1592 let line_height = node_style
1593 .line_height_pt
1594 .unwrap_or_else(|| metrics.line_height_pt());
1595 let pad_left = node_style.margin_left_pt.unwrap_or(0.0);
1596 let pad_right = node_style.margin_right_pt.unwrap_or(0.0);
1597 let text_indent = node_style.text_indent_pt.unwrap_or(0.0);
1598 let usable_w = (cap_w - pad_left - pad_right).max(1.0);
1599
1600 let layout = xfa_layout_engine::text::wrap_text(
1602 caption_text,
1603 usable_w,
1604 &metrics,
1605 text_indent,
1606 node_style.line_height_pt,
1607 );
1608
1609 if layout.lines.is_empty() {
1610 return;
1611 }
1612
1613 let asc_pt = ascender_pt(&metrics, fs);
1614 let space_above = node_style.space_above_pt.unwrap_or(0.0);
1615 let total_text_h = layout.lines.len() as f64 * line_height;
1616
1617 let first_line_pdf_y = match node_style.v_align {
1619 Some(VerticalAlign::Middle) => {
1620 cap_y + cap_h - asc_pt - space_above - (cap_h - space_above - total_text_h) / 2.0
1621 + (cap_h - space_above - total_text_h) / 2.0
1622 }
1623 Some(VerticalAlign::Bottom) => cap_y + total_text_h - asc_pt,
1624 _ => cap_y + cap_h - asc_pt - space_above,
1625 };
1626
1627 let tc = node_style
1628 .text_color
1629 .map(|(r, g, b)| [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0])
1630 .unwrap_or(config.text_color);
1631
1632 write_ops(
1633 ops,
1634 format_args!(
1635 "BT\n{:.3} {:.3} {:.3} rg\n{} {:.1} Tf\n",
1636 tc[0], tc[1], tc[2], font_ref, fs
1637 ),
1638 );
1639 emit_text_style_ops(node_style, ops);
1640
1641 let text_x_base = cap_x + pad_left;
1642 let mut prev_x = text_x_base;
1643 for (i, line) in layout.lines.iter().enumerate() {
1644 let is_para_start = layout.first_line_of_para.get(i).copied().unwrap_or(false);
1645 let indent = if is_para_start { text_indent } else { 0.0 };
1646 let text_x = text_x_base + indent;
1647 let line_y = first_line_pdf_y - (i as f64 * line_height);
1648 if i == 0 {
1649 write_ops(ops, format_args!("{:.2} {:.2} Td\n", text_x, line_y));
1650 } else {
1651 let dx = text_x - prev_x;
1652 write_ops(ops, format_args!("{:.2} {:.2} Td\n", dx, -line_height));
1653 }
1654 prev_x = text_x;
1655 let encoded = pdf_encode_text(line, idh_metrics);
1656 write_ops(ops, format_args!("{} Tj\n", encoded));
1657 }
1658
1659 reset_text_style_ops(node_style, ops);
1660 ops.extend_from_slice(b"ET\n");
1661 } else {
1662 let asc_pt = ascender_pt(&metrics, fs);
1664 let text_y = cap_y + cap_h - asc_pt;
1665
1666 let encoded = pdf_encode_text(caption_text, idh_metrics);
1667 write_ops(
1668 ops,
1669 format_args!(
1670 "BT\n{:.3} {:.3} {:.3} rg\n{} {:.1} Tf\n",
1671 config.text_color[0], config.text_color[1], config.text_color[2], font_ref, fs,
1672 ),
1673 );
1674 emit_text_style_ops(node_style, ops);
1675 write_ops(
1676 ops,
1677 format_args!("{:.2} {:.2} Td\n{} Tj\n", cap_x, text_y, encoded),
1678 );
1679 reset_text_style_ops(node_style, ops);
1680 ops.extend_from_slice(b"ET\n");
1681 }
1682}
1683
1684#[allow(clippy::too_many_arguments)]
1685fn render_field(
1686 x: f64,
1687 pdf_y: f64,
1688 w: f64,
1689 h: f64,
1690 value: &str,
1691 font_size: f64,
1692 font_family: FontFamily,
1693 node_style: &FormNodeStyle,
1694 config: &XfaRenderConfig,
1695 ops: &mut Vec<u8>,
1696) {
1697 let border_radius = node_style.border_radius_pt.unwrap_or(0.0);
1698 let border_style = node_style.border_style.as_deref();
1699
1700 if !config.field_values_only {
1701 if let Some(bg) = config.background_color {
1705 write_ops(
1706 ops,
1707 format_args!("{:.3} {:.3} {:.3} rg\n", bg[0], bg[1], bg[2]),
1708 );
1709 emit_rect_path(ops, x, pdf_y, w, h, border_radius);
1710 ops.extend_from_slice(b"f\n");
1711 }
1712 if config.draw_borders && config.border_width > 0.0 {
1713 if matches!(border_style, Some("lowered") | Some("raised")) {
1714 emit_3d_border(ops, x, pdf_y, w, h, config.border_width, border_style);
1715 } else {
1716 write_ops(
1717 ops,
1718 format_args!(
1719 "{:.2} w\n{:.3} {:.3} {:.3} RG\n",
1720 config.border_width,
1721 config.border_color[0],
1722 config.border_color[1],
1723 config.border_color[2],
1724 ),
1725 );
1726 let per_edge = node_style.border_colors.map(|cs| {
1727 cs.map(|(r, g, b)| [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0])
1728 });
1729 let per_edge_widths = node_style.border_widths.as_ref();
1730 apply_border_dash(ops, border_style);
1731 let edges = node_style.border_edges;
1732 if per_edge.is_some() || per_edge_widths.is_some() {
1733 emit_individual_edges(
1734 ops,
1735 x,
1736 pdf_y,
1737 w,
1738 h,
1739 &edges,
1740 per_edge.as_ref(),
1741 per_edge_widths,
1742 config.border_width,
1743 );
1744 } else if edges[0] && edges[1] && edges[2] && edges[3] {
1745 emit_rect_path(ops, x, pdf_y, w, h, border_radius);
1746 ops.extend_from_slice(b"S\n");
1747 } else {
1748 emit_individual_edges(
1749 ops,
1750 x,
1751 pdf_y,
1752 w,
1753 h,
1754 &edges,
1755 None,
1756 None,
1757 config.border_width,
1758 );
1759 }
1760 reset_border_dash(ops, border_style);
1761 }
1762 }
1763 } if !value.is_empty() {
1765 let fs = if font_size > 0.0 {
1766 font_size
1767 } else {
1768 config.default_font_size
1769 };
1770 let pad_left = node_style.margin_left_pt.unwrap_or(0.0);
1773 let pad_right = node_style.margin_right_pt.unwrap_or(0.0);
1774 let space_above = node_style.space_above_pt.unwrap_or(0.0);
1775 let content_w = (w - pad_left - pad_right).max(0.0);
1776 let metrics = build_font_metrics(fs, font_family, node_style, config);
1777 let font_ref = resolve_font_ref(&config.font_map, node_style, font_family);
1778 let text_w = metrics.measure_width(value);
1779
1780 let idh_metrics = lookup_font_metrics(node_style, config);
1781
1782 if text_w <= content_w || content_w <= 0.0 {
1783 let line_h = metrics.line_height_pt();
1784 let asc_pt = ascender_pt(&metrics, fs);
1785 let text_y = match node_style.v_align {
1786 Some(VerticalAlign::Middle) => {
1787 pdf_y + space_above + (h - space_above - line_h) / 2.0
1788 }
1789 Some(VerticalAlign::Bottom) => pdf_y + space_above,
1790 _ => pdf_y + h - space_above - asc_pt,
1791 };
1792 let encoded = pdf_encode_text(value, idh_metrics);
1793 write_ops(
1794 ops,
1795 format_args!(
1796 "BT\n{:.3} {:.3} {:.3} rg\n{} {:.1} Tf\n",
1797 config.text_color[0], config.text_color[1], config.text_color[2], font_ref, fs,
1798 ),
1799 );
1800 emit_synthetic_bold_ops(&config.font_map, node_style, fs, &config.text_color, ops);
1801 emit_text_style_ops(node_style, ops);
1802 write_ops(
1803 ops,
1804 format_args!("{:.2} {:.2} Td\n{} Tj\n", x + pad_left, text_y, encoded),
1805 );
1806 reset_text_style_ops(node_style, ops);
1807 reset_synthetic_bold_ops(&config.font_map, node_style, ops);
1808 ops.extend_from_slice(b"ET\n");
1809 } else {
1810 let lines = wrap_text(value, content_w, &metrics);
1811 let line_height = metrics.line_height_pt();
1812 let asc_pt = ascender_pt(&metrics, fs);
1813 let total_content_h = lines.len() as f64 * line_height;
1814 let text_start_y = match node_style.v_align {
1815 Some(VerticalAlign::Middle) => {
1816 pdf_y + space_above + (h - space_above - total_content_h) / 2.0
1817 }
1818 Some(VerticalAlign::Bottom) => pdf_y + space_above,
1819 _ => pdf_y + h - space_above - total_content_h,
1820 };
1821 write_ops(
1822 ops,
1823 format_args!(
1824 "BT\n{:.3} {:.3} {:.3} rg\n{} {:.1} Tf\n",
1825 config.text_color[0], config.text_color[1], config.text_color[2], font_ref, fs,
1826 ),
1827 );
1828 emit_synthetic_bold_ops(&config.font_map, node_style, fs, &config.text_color, ops);
1829 emit_text_style_ops(node_style, ops);
1830 write_ops(
1831 ops,
1832 format_args!(
1833 "{:.2} {:.2} Td\n",
1834 x + pad_left,
1835 text_start_y + total_content_h - asc_pt,
1836 ),
1837 );
1838 for (i, line) in lines.iter().enumerate() {
1839 if i > 0 {
1840 write_ops(ops, format_args!("0 {:.2} Td\n", -line_height));
1841 }
1842 let line_top = h - space_above - asc_pt - (i as f64 * line_height);
1843 if line_top < 0.0 {
1844 break;
1845 }
1846 let encoded = pdf_encode_text(line, idh_metrics);
1847 write_ops(ops, format_args!("{} Tj\n", encoded));
1848 }
1849 reset_text_style_ops(node_style, ops);
1850 reset_synthetic_bold_ops(&config.font_map, node_style, ops);
1851 ops.extend_from_slice(b"ET\n");
1852 }
1853 }
1854}
1855
1856#[allow(clippy::too_many_arguments)]
1860fn draw_check_mark(
1861 mark: &str,
1862 x: f64,
1863 y: f64,
1864 w: f64,
1865 h: f64,
1866 m: f64,
1867 color: [f64; 3],
1868 ops: &mut Vec<u8>,
1869) {
1870 write_ops(
1871 ops,
1872 format_args!(
1873 "{:.3} {:.3} {:.3} RG\n{:.3} {:.3} {:.3} rg\n",
1874 color[0], color[1], color[2], color[0], color[1], color[2]
1875 ),
1876 );
1877 let cx = x + w / 2.0;
1878 let cy = y + h / 2.0;
1879 match mark {
1880 "check" => {
1881 let lw = (w.min(h) * 0.08).max(0.5);
1883 write_ops(ops, format_args!("{:.2} w\n", lw));
1884 write_ops(
1885 ops,
1886 format_args!(
1887 "{:.2} {:.2} m\n{:.2} {:.2} l\n{:.2} {:.2} l\nS\n",
1888 x + m,
1889 cy,
1890 cx - m * 0.3,
1891 y + m,
1892 x + w - m,
1893 y + h - m,
1894 ),
1895 );
1896 }
1897 "circle" => {
1898 let r = (w.min(h) / 2.0 - m).max(1.0);
1899 let k = r * 0.5523; write_ops(ops, format_args!(
1901 "{:.2} {:.2} m\n{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\nf\n",
1902 cx + r, cy,
1903 cx + r, cy + k, cx + k, cy + r, cx, cy + r,
1904 cx - k, cy + r, cx - r, cy + k, cx - r, cy,
1905 cx - r, cy - k, cx - k, cy - r, cx, cy - r,
1906 cx + k, cy - r, cx + r, cy - k, cx + r, cy,
1907 ));
1908 }
1909 "diamond" => {
1910 let d = (w.min(h) / 2.0 - m).max(1.0);
1911 write_ops(
1912 ops,
1913 format_args!(
1914 "{:.2} {:.2} m\n{:.2} {:.2} l\n{:.2} {:.2} l\n{:.2} {:.2} l\nf\n",
1915 cx,
1916 cy + d,
1917 cx - d,
1918 cy,
1919 cx,
1920 cy - d,
1921 cx + d,
1922 cy,
1923 ),
1924 );
1925 }
1926 "square" => {
1927 let s = (w.min(h) - 2.0 * m).max(1.0);
1928 write_ops(
1929 ops,
1930 format_args!("{:.2} {:.2} {:.2} {:.2} re\nf\n", x + m, y + m, s, s,),
1931 );
1932 }
1933 "star" => {
1934 let lw = (w.min(h) * 0.08).max(0.5);
1936 write_ops(ops, format_args!("{:.2} w\n", lw));
1937 write_ops(
1938 ops,
1939 format_args!(
1940 "{:.2} {:.2} m\n{:.2} {:.2} l\nS\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\n",
1941 x + m,
1942 y + m,
1943 x + w - m,
1944 y + h - m,
1945 x + w - m,
1946 y + m,
1947 x + m,
1948 y + h - m,
1949 ),
1950 );
1951 write_ops(
1952 ops,
1953 format_args!(
1954 "{:.2} {:.2} m\n{:.2} {:.2} l\nS\n",
1955 cx,
1956 y + m * 0.5,
1957 cx,
1958 y + h - m * 0.5,
1959 ),
1960 );
1961 }
1962 _ => {
1963 let lw = (w.min(h) * 0.08).max(0.5);
1965 write_ops(ops, format_args!("{:.2} w\n", lw));
1966 write_ops(
1967 ops,
1968 format_args!(
1969 "{:.2} {:.2} m\n{:.2} {:.2} l\nS\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\n",
1970 x + m,
1971 y + m,
1972 x + w - m,
1973 y + h - m,
1974 x + w - m,
1975 y + m,
1976 x + m,
1977 y + h - m,
1978 ),
1979 );
1980 }
1981 }
1982}
1983
1984#[allow(clippy::too_many_arguments)]
2005fn render_checkbox(
2006 x: f64,
2007 pdf_y: f64,
2008 w: f64,
2009 h: f64,
2010 value: &str,
2011 node_style: &FormNodeStyle,
2012 config: &XfaRenderConfig,
2013 ops: &mut Vec<u8>,
2014) {
2015 let bw = config.border_width;
2016 write_ops(ops, format_args!("q\n"));
2020 if let Some((r_u8, g_u8, b_u8)) = node_style.bg_color {
2021 let bg = [
2022 r_u8 as f64 / 255.0,
2023 g_u8 as f64 / 255.0,
2024 b_u8 as f64 / 255.0,
2025 ];
2026 write_ops(
2027 ops,
2028 format_args!(
2029 "{:.3} {:.3} {:.3} rg\n{:.2} {:.2} {:.2} {:.2} re\nf\n",
2030 bg[0], bg[1], bg[2], x, pdf_y, w, h
2031 ),
2032 );
2033 }
2034 write_ops(
2035 ops,
2036 format_args!(
2037 "{:.2} w\n{:.3} {:.3} {:.3} RG\n{:.2} {:.2} {:.2} {:.2} re\nS\n",
2038 bw,
2039 config.border_color[0],
2040 config.border_color[1],
2041 config.border_color[2],
2042 x,
2043 pdf_y,
2044 w,
2045 h
2046 ),
2047 );
2048 let checked = is_check_button_checked(value, FieldKind::Checkbox, node_style);
2049 if checked {
2050 let mark = config
2051 .check_button_mark
2052 .as_deref()
2053 .unwrap_or(default_check_button_mark(FieldKind::Checkbox));
2054 let m = w.min(h) * 0.15;
2055 let color = config.text_color;
2056 draw_check_mark(mark, x, pdf_y, w, h, m, color, ops);
2057 }
2058 write_ops(ops, format_args!("Q\n"));
2059}
2060
2061#[allow(clippy::too_many_arguments)]
2077fn render_radio(
2078 x: f64,
2079 pdf_y: f64,
2080 w: f64,
2081 h: f64,
2082 value: &str,
2083 node_style: &FormNodeStyle,
2084 config: &XfaRenderConfig,
2085 ops: &mut Vec<u8>,
2086) {
2087 let bw = config.border_width;
2088 let cx = x + w / 2.0;
2089 let cy = pdf_y + h / 2.0;
2090 let r = w.min(h) / 2.0;
2091
2092 let k = 0.5523; let kx = r * k;
2095 let ky = r * k;
2096 write_ops(ops, format_args!("q\n",));
2099 if let Some((r_u8, g_u8, b_u8)) = node_style.bg_color {
2100 let bg = [
2101 r_u8 as f64 / 255.0,
2102 g_u8 as f64 / 255.0,
2103 b_u8 as f64 / 255.0,
2104 ];
2105 write_ops(
2106 ops,
2107 format_args!(
2108 "{:.3} {:.3} {:.3} rg\n\
2109 {:.2} {:.2} m\n\
2110 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
2111 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
2112 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
2113 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
2114 f\n",
2115 bg[0],
2116 bg[1],
2117 bg[2],
2118 cx + r,
2119 cy,
2120 cx + r,
2121 cy + ky,
2122 cx + kx,
2123 cy + r,
2124 cx,
2125 cy + r,
2126 cx - kx,
2127 cy + r,
2128 cx - r,
2129 cy + ky,
2130 cx - r,
2131 cy,
2132 cx - r,
2133 cy - ky,
2134 cx - kx,
2135 cy - r,
2136 cx,
2137 cy - r,
2138 cx + kx,
2139 cy - r,
2140 cx + r,
2141 cy - ky,
2142 cx + r,
2143 cy,
2144 ),
2145 );
2146 }
2147 write_ops(
2148 ops,
2149 format_args!(
2150 "{:.2} w\n{:.3} {:.3} {:.3} RG\n",
2151 bw, config.border_color[0], config.border_color[1], config.border_color[2],
2152 ),
2153 );
2154 write_ops(
2155 ops,
2156 format_args!(
2157 "{:.2} {:.2} m\n\
2158 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
2159 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
2160 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
2161 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
2162 S\n",
2163 cx + r,
2164 cy,
2165 cx + r,
2166 cy + ky,
2167 cx + kx,
2168 cy + r,
2169 cx,
2170 cy + r,
2171 cx - kx,
2172 cy + r,
2173 cx - r,
2174 cy + ky,
2175 cx - r,
2176 cy,
2177 cx - r,
2178 cy - ky,
2179 cx - kx,
2180 cy - r,
2181 cx,
2182 cy - r,
2183 cx + kx,
2184 cy - r,
2185 cx + r,
2186 cy - ky,
2187 cx + r,
2188 cy,
2189 ),
2190 );
2191
2192 let checked = is_check_button_checked(value, FieldKind::Radio, node_style);
2193 if checked {
2194 let mark = config
2195 .check_button_mark
2196 .as_deref()
2197 .unwrap_or(default_check_button_mark(FieldKind::Radio));
2198 if mark == "circle" {
2199 let ir = r * 0.4;
2202 let ikx = ir * k;
2203 let iky = ir * k;
2204 write_ops(
2205 ops,
2206 format_args!(
2207 "{:.3} {:.3} {:.3} rg\n\
2208 {:.2} {:.2} m\n\
2209 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
2210 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
2211 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
2212 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
2213 f\n",
2214 config.text_color[0],
2215 config.text_color[1],
2216 config.text_color[2],
2217 cx + ir,
2218 cy,
2219 cx + ir,
2220 cy + iky,
2221 cx + ikx,
2222 cy + ir,
2223 cx,
2224 cy + ir,
2225 cx - ikx,
2226 cy + ir,
2227 cx - ir,
2228 cy + iky,
2229 cx - ir,
2230 cy,
2231 cx - ir,
2232 cy - iky,
2233 cx - ikx,
2234 cy - ir,
2235 cx,
2236 cy - ir,
2237 cx + ikx,
2238 cy - ir,
2239 cx + ir,
2240 cy - iky,
2241 cx + ir,
2242 cy,
2243 ),
2244 );
2245 } else {
2246 let m = w.min(h) * 0.15;
2247 draw_check_mark(mark, x, pdf_y, w, h, m, config.text_color, ops);
2248 }
2249 }
2250 write_ops(ops, format_args!("Q\n"));
2251}
2252
2253fn default_check_button_mark(field_kind: FieldKind) -> &'static str {
2254 match field_kind {
2255 FieldKind::Radio => "circle",
2256 _ => "cross",
2257 }
2258}
2259
2260fn is_check_button_checked(value: &str, field_kind: FieldKind, node_style: &FormNodeStyle) -> bool {
2261 let on_value = node_style.check_button_on_value.as_deref().unwrap_or("1");
2264 let off_value = node_style.check_button_off_value.as_deref().unwrap_or("");
2265 let neutral_value = node_style
2266 .check_button_neutral_value
2267 .as_deref()
2268 .unwrap_or("");
2269 if value == on_value {
2270 return true;
2271 }
2272 if value == off_value {
2273 return false;
2274 }
2275 if field_kind == FieldKind::Checkbox && value == neutral_value {
2276 return false;
2277 }
2278
2279 false
2280}
2281
2282#[allow(clippy::too_many_arguments)]
2301fn render_dropdown(
2302 x: f64,
2303 pdf_y: f64,
2304 w: f64,
2305 h: f64,
2306 value: &str,
2307 font_size: f64,
2308 font_family: FontFamily,
2309 node_style: &FormNodeStyle,
2310 config: &XfaRenderConfig,
2311 ops: &mut Vec<u8>,
2312 display_items: &[String],
2313 save_items: &[String],
2314) {
2315 let display_value = if let Some(idx) = save_items.iter().position(|s| s == value) {
2318 display_items.get(idx).map(|s| s.as_str()).unwrap_or(value)
2319 } else {
2320 value
2321 };
2322
2323 if display_value.is_empty() {
2325 return;
2326 }
2327 let border_radius = node_style.border_radius_pt.unwrap_or(0.0);
2328
2329 if let Some(bg) = &config.background_color {
2330 write_ops(
2331 ops,
2332 format_args!("{:.3} {:.3} {:.3} rg\n", bg[0], bg[1], bg[2]),
2333 );
2334 emit_rect_path(ops, x, pdf_y, w, h, border_radius);
2335 ops.extend_from_slice(b"f\n");
2336 }
2337
2338 if config.draw_borders && config.border_width > 0.0 {
2339 write_ops(
2340 ops,
2341 format_args!(
2342 "{:.2} w\n{:.3} {:.3} {:.3} RG\n",
2343 config.border_width,
2344 config.border_color[0],
2345 config.border_color[1],
2346 config.border_color[2],
2347 ),
2348 );
2349 emit_rect_path(ops, x, pdf_y, w, h, border_radius);
2350 ops.extend_from_slice(b"S\n");
2351 }
2352
2353 let arrow_w = h.min(12.0);
2354
2355 if !display_value.is_empty() {
2356 let fs = if font_size > 0.0 {
2357 font_size
2358 } else {
2359 config.default_font_size
2360 };
2361 let _metrics = build_font_metrics(fs, font_family, node_style, config);
2362 let font_ref = resolve_font_ref(&config.font_map, node_style, font_family);
2363 let idh_metrics = lookup_font_metrics(node_style, config);
2364
2365 let v_offset = pdf_y + h / 2.0 - fs / 2.0;
2366 let encoded = pdf_encode_text(display_value, idh_metrics);
2367 write_ops(
2368 ops,
2369 format_args!(
2370 "BT\n{:.3} {:.3} {:.3} rg\n{} {:.1} Tf\n",
2371 config.text_color[0], config.text_color[1], config.text_color[2], font_ref, fs,
2372 ),
2373 );
2374 emit_synthetic_bold_ops(&config.font_map, node_style, fs, &config.text_color, ops);
2375 emit_text_style_ops(node_style, ops);
2376 write_ops(
2377 ops,
2378 format_args!("{:.2} {:.2} Td\n{} Tj\n", x + 2.0, v_offset, encoded),
2379 );
2380 reset_text_style_ops(node_style, ops);
2381 reset_synthetic_bold_ops(&config.font_map, node_style, ops);
2382 ops.extend_from_slice(b"ET\n");
2383 }
2384
2385 let arrow_x = x + w - arrow_w - 1.0;
2386 let arrow_y_center = pdf_y + h / 2.0;
2387 let arrow_size = arrow_w * 0.6;
2388 let arrow_char = "\u{25BC}";
2389 write_ops(
2390 ops,
2391 format_args!(
2392 "BT\n/F2 {:.1} Tf\n{:.2} {:.2} Td\n({}) Tj\nET\n",
2393 arrow_size,
2394 arrow_x,
2395 arrow_y_center - arrow_size / 2.0,
2396 arrow_char
2397 ),
2398 );
2399}
2400
2401#[allow(clippy::too_many_arguments)]
2402fn render_button(
2403 x: f64,
2404 pdf_y: f64,
2405 w: f64,
2406 h: f64,
2407 value: &str,
2408 font_size: f64,
2409 font_family: FontFamily,
2410 node_style: &FormNodeStyle,
2411 config: &XfaRenderConfig,
2412 ops: &mut Vec<u8>,
2413) {
2414 if value.is_empty() {
2416 return;
2417 }
2418 let border_radius = node_style.border_radius_pt.unwrap_or(0.0);
2419 let bw = config.border_width.max(0.0);
2420
2421 let fill_color = if let Some((r, g, b)) = node_style.bg_color {
2424 [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0]
2425 } else {
2426 [
2427 (config.border_color[0] + 0.3).min(1.0),
2428 (config.border_color[1] + 0.3).min(1.0),
2429 (config.border_color[2] + 0.3).min(1.0),
2430 ]
2431 };
2432 let border_color = node_style
2433 .border_color
2434 .map(|(r, g, b)| [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0])
2435 .unwrap_or([
2436 fill_color[0] * 0.6,
2437 fill_color[1] * 0.6,
2438 fill_color[2] * 0.6,
2439 ]);
2440
2441 write_ops(
2442 ops,
2443 format_args!(
2444 "q\n{:.3} {:.3} {:.3} rg\n",
2445 fill_color[0], fill_color[1], fill_color[2]
2446 ),
2447 );
2448 emit_rect_path(ops, x, pdf_y, w, h, border_radius);
2449 ops.extend_from_slice(b"f\n");
2450
2451 write_ops(
2452 ops,
2453 format_args!(
2454 "{:.3} {:.3} {:.3} RG\n{:.2} w\n",
2455 border_color[0], border_color[1], border_color[2], bw
2456 ),
2457 );
2458 emit_rect_path(ops, x, pdf_y, w, h, border_radius);
2459 ops.extend_from_slice(b"S\n");
2460
2461 if !value.is_empty() {
2462 let fs = if font_size > 0.0 {
2463 font_size
2464 } else {
2465 config.default_font_size
2466 };
2467 let metrics = build_font_metrics(fs, font_family, node_style, config);
2468 let font_ref = resolve_font_ref(&config.font_map, node_style, font_family);
2469 let idh_metrics = lookup_font_metrics(node_style, config);
2470 let text_w = metrics.measure_width(value);
2471 let text_x = x + (w - text_w) / 2.0;
2472 let v_offset = pdf_y + h / 2.0 - fs / 2.0;
2473 let encoded = pdf_encode_text(value, idh_metrics);
2474 let tc = node_style
2475 .text_color
2476 .map(|(r, g, b)| [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0])
2477 .unwrap_or(config.text_color);
2478 write_ops(
2479 ops,
2480 format_args!(
2481 "BT\n{:.3} {:.3} {:.3} rg\n{} {:.1} Tf\n",
2482 tc[0], tc[1], tc[2], font_ref, fs,
2483 ),
2484 );
2485 emit_text_style_ops(node_style, ops);
2486 write_ops(
2487 ops,
2488 format_args!("{:.2} {:.2} Td\n{} Tj\n", text_x, v_offset, encoded),
2489 );
2490 reset_text_style_ops(node_style, ops);
2491 ops.extend_from_slice(b"ET\n");
2492 }
2493 ops.extend_from_slice(b"Q\n");
2500}
2501
2502#[allow(clippy::too_many_arguments)]
2503fn render_barcode(
2514 x: f64,
2515 pdf_y: f64,
2516 w: f64,
2517 h: f64,
2518 value: &str,
2519 _node_style: &FormNodeStyle,
2520 config: &XfaRenderConfig,
2521 ops: &mut Vec<u8>,
2522) {
2523 if value.is_empty() || w <= 0.0 || h <= 0.0 {
2524 return;
2525 }
2526 let Some(elementen) = crate::barcode::encode_code39(value) else {
2527 return;
2528 };
2529 let modules = crate::barcode::total_modules(&elementen);
2530 if modules == 0 {
2531 return;
2532 }
2533 let module_breedte = w / f64::from(modules);
2537 if module_breedte < 0.25 {
2538 return;
2539 }
2540
2541 write_ops(
2542 ops,
2543 format_args!(
2544 "{:.3} {:.3} {:.3} rg\n",
2545 config.text_color[0], config.text_color[1], config.text_color[2]
2546 ),
2547 );
2548 let mut cursor = x;
2549 for e in &elementen {
2550 let breedte = module_breedte * f64::from(e.modules);
2551 if e.is_bar {
2552 write_ops(
2553 ops,
2554 format_args!("{cursor:.2} {pdf_y:.2} {breedte:.2} {h:.2} re\n"),
2555 );
2556 }
2557 cursor += breedte;
2558 }
2559 ops.extend_from_slice(b"f\n");
2560}
2561
2562#[allow(clippy::too_many_arguments)]
2563fn render_signature(
2564 x: f64,
2565 pdf_y: f64,
2566 w: f64,
2567 h: f64,
2568 value: &str,
2569 node_style: &FormNodeStyle,
2570 config: &XfaRenderConfig,
2571 ops: &mut Vec<u8>,
2572) {
2573 let border_radius = node_style.border_radius_pt.unwrap_or(0.0);
2574
2575 if value.is_empty() {
2589 if config.draw_borders && config.border_width > 0.0 {
2590 write_ops(
2591 ops,
2592 format_args!(
2593 "{:.2} w\n{:.3} {:.3} {:.3} RG\n",
2594 config.border_width,
2595 config.border_color[0],
2596 config.border_color[1],
2597 config.border_color[2],
2598 ),
2599 );
2600 emit_rect_path(ops, x, pdf_y, w, h, border_radius);
2601 ops.extend_from_slice(b"S\n");
2602 }
2603 return;
2604 }
2605
2606 if let Some(bg) = &config.background_color {
2607 write_ops(
2608 ops,
2609 format_args!("{:.3} {:.3} {:.3} rg\n", bg[0], bg[1], bg[2]),
2610 );
2611 emit_rect_path(ops, x, pdf_y, w, h, border_radius);
2612 ops.extend_from_slice(b"f\n");
2613 }
2614
2615 write_ops(
2616 ops,
2617 format_args!(
2618 "{:.2} w\n{:.3} {:.3} {:.3} RG\n",
2619 config.border_width,
2620 config.border_color[0],
2621 config.border_color[1],
2622 config.border_color[2],
2623 ),
2624 );
2625 write_ops(ops, format_args!("[4 2] 0 d\n"));
2626 emit_rect_path(ops, x, pdf_y, w, h, border_radius);
2627 ops.extend_from_slice(b"S\n");
2628 write_ops(ops, format_args!("[] 0 d\n"));
2629
2630 if !value.is_empty() {
2631 let fs = node_style.font_size.unwrap_or(config.default_font_size);
2632 let text_x = x + node_style.margin_left_pt.unwrap_or(0.0);
2634 let v_offset = pdf_y + h / 2.0 - fs / 2.0;
2635 write_ops(
2636 ops,
2637 format_args!(
2638 "BT\n{:.3} {:.3} {:.3} rg\n/F1 {:.1} Tf\n{:.2} {:.2} Td\n({}) Tj\nET\n",
2639 config.text_color[0],
2640 config.text_color[1],
2641 config.text_color[2],
2642 fs,
2643 text_x,
2644 v_offset,
2645 pdf_escape(value)
2646 ),
2647 );
2648 }
2649}
2650
2651#[allow(clippy::too_many_arguments)]
2679fn render_text(
2680 x: f64,
2681 pdf_y: f64,
2682 _w: f64,
2683 h: f64,
2684 text: &str,
2685 node_style: &FormNodeStyle,
2686 config: &XfaRenderConfig,
2687 ops: &mut Vec<u8>,
2688) {
2689 if text.is_empty() {
2690 return;
2691 }
2692 let fs = node_style.font_size.unwrap_or(config.default_font_size);
2693 let p = node_style.margin_left_pt.unwrap_or(0.0);
2695 let font_family = match node_style.font_family.as_deref() {
2696 Some(f) if f.contains("Courier") || f.contains("Mono") => FontFamily::Monospace,
2697 Some(f)
2698 if f.contains("Helvetica")
2699 || f.contains("Arial")
2700 || f.contains("Sans")
2701 || f.contains("Myriad") =>
2702 {
2703 FontFamily::SansSerif
2704 }
2705 _ => FontFamily::Serif,
2706 };
2707 let font_ref = resolve_font_ref(&config.font_map, node_style, font_family);
2708 let tc = node_style
2709 .text_color
2710 .map(|(r, g, b)| [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0])
2711 .unwrap_or(config.text_color);
2712 let metrics = build_font_metrics(fs, font_family, node_style, config);
2713 let asc_pt = ascender_pt(&metrics, fs);
2714 let line_h = metrics.line_height_pt();
2715 let idh_metrics = lookup_font_metrics(node_style, config);
2716 let encoded = pdf_encode_text(text, idh_metrics);
2717 let desc_pt =
2718 if let (Some(desc), Some(upem)) = (metrics.resolved_descender, metrics.resolved_upem) {
2719 if upem > 0 {
2720 desc as f64 / upem as f64 * fs
2721 } else {
2722 fs * 0.2
2723 }
2724 } else {
2725 fs * 0.2
2726 };
2727 let text_y = match node_style.v_align {
2728 Some(VerticalAlign::Middle) => pdf_y + (h - line_h) / 2.0,
2729 Some(VerticalAlign::Bottom) => pdf_y + desc_pt,
2730 _ => pdf_y + h - p - asc_pt,
2731 };
2732 write_ops(
2733 ops,
2734 format_args!(
2735 "BT\n{:.3} {:.3} {:.3} rg\n{} {:.1} Tf\n",
2736 tc[0], tc[1], tc[2], font_ref, fs,
2737 ),
2738 );
2739 emit_synthetic_bold_ops(&config.font_map, node_style, fs, &tc, ops);
2740 write_ops(
2741 ops,
2742 format_args!("{:.2} {:.2} Td\n{} Tj\n", x + p, text_y, encoded),
2743 );
2744 reset_synthetic_bold_ops(&config.font_map, node_style, ops);
2745 ops.extend_from_slice(b"ET\n");
2746 let text_x = x + p;
2747 let text_y = pdf_y + p;
2748 let line_thickness = (fs * 0.05).max(0.5);
2749 if node_style.underline {
2750 let underline_y = text_y - desc_pt;
2751 let text_w = metrics.measure_width(text);
2752 write_ops(
2753 ops,
2754 format_args!(
2755 "BT\n{:.3} w\n{:.3} {:.3} {:.3} RG\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nET\n",
2756 line_thickness,
2757 tc[0],
2758 tc[1],
2759 tc[2],
2760 text_x,
2761 underline_y,
2762 text_x + text_w,
2763 underline_y,
2764 ),
2765 );
2766 }
2767 if node_style.line_through {
2768 let mid_y = text_y + fs * 0.5 - asc_pt * 0.1;
2769 let text_w = metrics.measure_width(text);
2770 write_ops(
2771 ops,
2772 format_args!(
2773 "BT\n{:.3} w\n{:.3} {:.3} {:.3} RG\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nET\n",
2774 line_thickness,
2775 tc[0],
2776 tc[1],
2777 tc[2],
2778 text_x,
2779 mid_y,
2780 text_x + text_w,
2781 mid_y,
2782 ),
2783 );
2784 }
2785}
2786
2787#[allow(clippy::too_many_arguments)]
2812fn render_multiline(
2813 x: f64,
2814 _pdf_y: f64,
2815 container_width: f64,
2816 container_height: f64,
2817 lines: &[String],
2818 first_line_of_para: &[bool],
2819 font_size: f64,
2820 text_align: TextAlign,
2821 font_family: FontFamily,
2822 _is_bold: bool,
2823 mapper: &CoordinateMapper,
2824 abs_y_xfa: f64,
2825 node_style: &FormNodeStyle,
2826 config: &XfaRenderConfig,
2827 ops: &mut Vec<u8>,
2828) {
2829 if lines.is_empty() {
2830 return;
2831 }
2832 let pad_left = node_style.margin_left_pt.unwrap_or(0.0);
2835 let pad_right = node_style.margin_right_pt.unwrap_or(0.0);
2836 let space_above = node_style.space_above_pt.unwrap_or(0.0);
2837 let text_indent = node_style.text_indent_pt.unwrap_or(0.0);
2838 let font_metrics = build_font_metrics(font_size, font_family, node_style, config);
2839 let line_height = node_style
2840 .line_height_pt
2841 .unwrap_or_else(|| font_metrics.line_height_pt());
2842 let font_ref = resolve_font_ref(&config.font_map, node_style, font_family);
2843 let tc = node_style
2844 .text_color
2845 .map(|(r, g, b)| [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0])
2846 .unwrap_or(config.text_color);
2847 write_ops(
2848 ops,
2849 format_args!(
2850 "BT\n{:.3} {:.3} {:.3} rg\n{} {:.1} Tf\n",
2851 tc[0], tc[1], tc[2], font_ref, font_size
2852 ),
2853 );
2854 emit_synthetic_bold_ops(&config.font_map, node_style, font_size, &tc, ops);
2855 emit_text_style_ops(node_style, ops);
2856 let ascender_pt = if let (Some(asc), Some(upem)) =
2857 (font_metrics.resolved_ascender, font_metrics.resolved_upem)
2858 {
2859 if upem > 0 {
2860 asc as f64 / upem as f64 * font_size
2861 } else {
2862 font_size
2863 }
2864 } else {
2865 font_size
2866 };
2867 let total_text_h = lines.len() as f64 * line_height;
2868 let first_line_y_xfa = match node_style.v_align {
2869 Some(VerticalAlign::Middle) => {
2870 abs_y_xfa
2871 + space_above
2872 + (container_height - space_above - total_text_h) / 2.0
2873 + ascender_pt
2874 }
2875 Some(VerticalAlign::Bottom) => abs_y_xfa + container_height - total_text_h + ascender_pt,
2876 _ => abs_y_xfa + space_above + ascender_pt,
2877 };
2878 let first_line_pdf_y = mapper.xfa_to_pdf_y(first_line_y_xfa, 0.0);
2879 let content_w = (container_width - pad_left - pad_right).max(0.0);
2880 let idh_metrics = lookup_font_metrics(node_style, config);
2881 let mut prev_x = x + pad_left;
2882 for (i, line) in lines.iter().enumerate() {
2883 let is_para_start = first_line_of_para.get(i).copied().unwrap_or(false);
2884 let indent_offset = if is_para_start { text_indent } else { 0.0 };
2885 let line_y = first_line_pdf_y - (i as f64 * line_height);
2886 let line_w = font_metrics.measure_width(line);
2887 let text_x = match text_align {
2888 TextAlign::Center => {
2889 x + pad_left + indent_offset + ((content_w - indent_offset - line_w) / 2.0).max(0.0)
2890 }
2891 TextAlign::Right => x + pad_left + (content_w - line_w).max(0.0),
2892 _ => x + pad_left + indent_offset,
2893 };
2894 if i == 0 {
2895 write_ops(ops, format_args!("{:.2} {:.2} Td\n", text_x, line_y));
2896 } else {
2897 let dx = text_x - prev_x;
2898 write_ops(ops, format_args!("{:.2} {:.2} Td\n", dx, -line_height));
2899 }
2900 prev_x = text_x;
2901 let encoded = pdf_encode_text(line, idh_metrics);
2902 write_ops(ops, format_args!("{} Tj\n", encoded));
2903 }
2904 reset_text_style_ops(node_style, ops);
2905 reset_synthetic_bold_ops(&config.font_map, node_style, ops);
2906 ops.extend_from_slice(b"ET\n");
2907}
2908
2909#[allow(clippy::too_many_arguments)]
2911fn render_rich_multiline(
2912 x: f64,
2913 container_width: f64,
2914 container_height: f64,
2915 lines: &[String],
2916 first_line_of_para: &[bool],
2917 spans: &[RichTextSpan],
2918 font_size: f64,
2919 text_align: TextAlign,
2920 font_family: FontFamily,
2921 mapper: &CoordinateMapper,
2922 abs_y_xfa: f64,
2923 node_style: &FormNodeStyle,
2924 config: &XfaRenderConfig,
2925 ops: &mut Vec<u8>,
2926) {
2927 if lines.is_empty() || spans.is_empty() {
2928 return;
2929 }
2930 let pad_left = node_style.margin_left_pt.unwrap_or(0.0);
2933 let pad_right = node_style.margin_right_pt.unwrap_or(0.0);
2934 let space_above = node_style.space_above_pt.unwrap_or(0.0);
2935 let text_indent = node_style.text_indent_pt.unwrap_or(0.0);
2936 let font_metrics = build_font_metrics(font_size, font_family, node_style, config);
2937 let line_height = node_style
2938 .line_height_pt
2939 .unwrap_or_else(|| font_metrics.line_height_pt());
2940 let asc_pt = if let (Some(asc), Some(upem)) =
2941 (font_metrics.resolved_ascender, font_metrics.resolved_upem)
2942 {
2943 if upem > 0 {
2944 asc as f64 / upem as f64 * font_size
2945 } else {
2946 font_size
2947 }
2948 } else {
2949 font_size
2950 };
2951 let total_text_h = lines.len() as f64 * line_height;
2952 let first_line_y_xfa = match node_style.v_align {
2953 Some(VerticalAlign::Middle) => {
2954 abs_y_xfa + space_above + (container_height - space_above - total_text_h) / 2.0 + asc_pt
2955 }
2956 Some(VerticalAlign::Bottom) => abs_y_xfa + container_height - total_text_h + asc_pt,
2957 _ => abs_y_xfa + space_above + asc_pt,
2958 };
2959 let first_line_pdf_y = mapper.xfa_to_pdf_y(first_line_y_xfa, 0.0);
2960 let content_w = (container_width - pad_left - pad_right).max(0.0);
2961 let line_segments = map_spans_to_lines(spans, lines);
2962
2963 ops.extend_from_slice(b"BT\n");
2964 let base_font_ref = resolve_font_ref(&config.font_map, node_style, font_family);
2965 let base_tc = node_style
2966 .text_color
2967 .map(|(r, g, b)| [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0])
2968 .unwrap_or(config.text_color);
2969 let idh_metrics = lookup_font_metrics(node_style, config);
2970
2971 let mut cur_font_ref = base_font_ref;
2972 let mut cur_fs = font_size;
2973 let mut cur_rise: f64 = 0.0;
2975 let mut cur_tc = base_tc;
2976 write_ops(
2977 ops,
2978 format_args!(
2979 "{:.3} {:.3} {:.3} rg\n{} {:.1} Tf\n",
2980 cur_tc[0], cur_tc[1], cur_tc[2], cur_font_ref, cur_fs,
2981 ),
2982 );
2983 emit_text_style_ops(node_style, ops);
2984
2985 let mut prev_x = x + pad_left;
2986 for (i, line) in lines.iter().enumerate() {
2987 let is_para_start = first_line_of_para.get(i).copied().unwrap_or(false);
2988 let indent_offset = if is_para_start { text_indent } else { 0.0 };
2989 let line_y = first_line_pdf_y - (i as f64 * line_height);
2990 let line_w = font_metrics.measure_width(line);
2991 let text_x = match text_align {
2992 TextAlign::Center => {
2993 x + pad_left + indent_offset + ((content_w - indent_offset - line_w) / 2.0).max(0.0)
2994 }
2995 TextAlign::Right => x + pad_left + (content_w - line_w).max(0.0),
2996 _ => x + pad_left + indent_offset,
2997 };
2998 if i == 0 {
2999 write_ops(ops, format_args!("{:.2} {:.2} Td\n", text_x, line_y));
3000 } else {
3001 let dx = text_x - prev_x;
3002 write_ops(ops, format_args!("{:.2} {:.2} Td\n", dx, -line_height));
3003 }
3004 prev_x = text_x;
3005
3006 if let Some(segs) = line_segments.get(i) {
3007 if segs.is_empty() {
3008 let encoded = pdf_encode_text(line, idh_metrics);
3009 write_ops(ops, format_args!("{} Tj\n", encoded));
3010 continue;
3011 }
3012 for seg in segs {
3013 let span = &spans[seg.span_idx];
3014 let span_family = span
3015 .font_family
3016 .as_deref()
3017 .map(classify_font_family)
3018 .unwrap_or(font_family);
3019 let span_style = span_to_node_style(span, node_style);
3020 let span_font_ref = resolve_font_ref(&config.font_map, &span_style, span_family);
3021 let span_fs = span.font_size.unwrap_or(font_size);
3022 let span_tc = span
3023 .text_color
3024 .map(|(r, g, b)| [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0])
3025 .unwrap_or(base_tc);
3026
3027 if span_font_ref != cur_font_ref || (span_fs - cur_fs).abs() > 0.01 {
3028 write_ops(ops, format_args!("{} {:.1} Tf\n", span_font_ref, span_fs));
3029 cur_font_ref = span_font_ref;
3030 cur_fs = span_fs;
3031 }
3032 let span_rise = span.baseline_shift * span_fs;
3042 if (span_rise - cur_rise).abs() > 0.01 {
3043 write_ops(ops, format_args!("{span_rise:.2} Ts\n"));
3044 cur_rise = span_rise;
3045 }
3046 if (span_tc[0] - cur_tc[0]).abs() > 0.001
3047 || (span_tc[1] - cur_tc[1]).abs() > 0.001
3048 || (span_tc[2] - cur_tc[2]).abs() > 0.001
3049 {
3050 write_ops(
3051 ops,
3052 format_args!("{:.3} {:.3} {:.3} rg\n", span_tc[0], span_tc[1], span_tc[2]),
3053 );
3054 cur_tc = span_tc;
3055 }
3056 let is_span_bold = span.font_weight.as_deref() == Some("bold");
3057 let span_has_real_bold =
3058 style_uses_real_bold_variant(&config.font_map, &span_style);
3059 if is_span_bold && !span_has_real_bold {
3060 let stroke_w = span_fs * 0.03;
3061 write_ops(
3062 ops,
3063 format_args!(
3064 "2 Tr\n{:.4} w\n{:.3} {:.3} {:.3} RG\n",
3065 stroke_w, span_tc[0], span_tc[1], span_tc[2],
3066 ),
3067 );
3068 }
3069 let encoded = pdf_encode_text(&seg.text, idh_metrics);
3070 write_ops(ops, format_args!("{} Tj\n", encoded));
3071 if is_span_bold && !span_has_real_bold {
3072 write_ops(ops, format_args!("0 Tr\n"));
3073 }
3074 }
3075 } else {
3076 let encoded = pdf_encode_text(line, idh_metrics);
3077 write_ops(ops, format_args!("{} Tj\n", encoded));
3078 }
3079 }
3080 reset_text_style_ops(node_style, ops);
3081 if cur_rise.abs() > 0.01 {
3085 ops.extend_from_slice(b"0 Ts\n");
3086 }
3087 ops.extend_from_slice(b"ET\n");
3088}
3089
3090fn span_to_node_style(span: &RichTextSpan, base: &FormNodeStyle) -> FormNodeStyle {
3091 let mut style = base.clone();
3092 if let Some(ref fam) = span.font_family {
3093 style.font_family = Some(fam.clone());
3094 }
3095 if let Some(ref w) = span.font_weight {
3096 style.font_weight = Some(w.clone());
3097 }
3098 if let Some(ref s) = span.font_style {
3099 style.font_style = Some(s.clone());
3100 }
3101 style
3102}
3103
3104fn classify_font_family(name: &str) -> FontFamily {
3105 if name.contains("Courier") || name.contains("Mono") {
3106 FontFamily::Monospace
3107 } else if name.contains("Helvetica")
3108 || name.contains("Arial")
3109 || name.contains("Sans")
3110 || name.contains("Myriad")
3111 {
3112 FontFamily::SansSerif
3113 } else {
3114 FontFamily::Serif
3115 }
3116}
3117
3118struct LineSpanSegment {
3119 text: String,
3120 span_idx: usize,
3121}
3122
3123fn leading_whitespace_len(s: &str) -> usize {
3124 s.char_indices()
3125 .find(|(_, ch)| !ch.is_whitespace())
3126 .map(|(idx, _)| idx)
3127 .unwrap_or(s.len())
3128}
3129
3130fn map_spans_to_lines(spans: &[RichTextSpan], lines: &[String]) -> Vec<Vec<LineSpanSegment>> {
3131 let mut result = Vec::with_capacity(lines.len());
3132 let mut span_idx = 0_usize;
3133 let mut span_off = 0_usize;
3134
3135 for line in lines {
3136 while span_idx < spans.len() {
3137 if spans[span_idx].text == "\n" || span_off >= spans[span_idx].text.len() {
3138 span_idx += 1;
3139 span_off = 0;
3140 } else {
3141 break;
3142 }
3143 }
3144
3145 let mut segs: Vec<LineSpanSegment> = Vec::new();
3146 let mut line_pos = 0_usize;
3147
3148 while line_pos < line.len() && span_idx < spans.len() {
3149 let span = &spans[span_idx];
3150 if span.text == "\n" {
3151 span_idx += 1;
3152 span_off = 0;
3153 continue;
3154 }
3155 let span_rest = &span.text[span_off..];
3156 let line_rest = &line[line_pos..];
3157
3158 let common = line_rest
3159 .chars()
3160 .zip(span_rest.chars())
3161 .take_while(|(a, b)| a == b || (a.is_whitespace() && b.is_whitespace()))
3162 .count();
3163
3164 if common > 0 {
3165 let common_str: String = line_rest.chars().take(common).collect();
3166 let common_line_byte_len = common_str.len();
3167 let common_span_byte_len: usize =
3168 span_rest.chars().take(common).map(char::len_utf8).sum();
3169 segs.push(LineSpanSegment {
3170 text: common_str,
3171 span_idx,
3172 });
3173 line_pos += common_line_byte_len;
3174 span_off += common_span_byte_len;
3175 if span_off >= span.text.len() {
3176 span_idx += 1;
3177 span_off = 0;
3178 }
3179 } else {
3180 let span_skip = leading_whitespace_len(span_rest);
3181 if span_skip > 0 {
3182 span_off += span_skip;
3183 if span_off >= span.text.len() {
3184 span_idx += 1;
3185 span_off = 0;
3186 }
3187 continue;
3188 }
3189
3190 let line_skip = leading_whitespace_len(line_rest);
3191 if line_skip > 0 {
3192 segs.push(LineSpanSegment {
3193 text: line_rest[..line_skip].to_string(),
3194 span_idx,
3195 });
3196 line_pos += line_skip;
3197 } else {
3198 segs.push(LineSpanSegment {
3199 text: line_rest.to_string(),
3200 span_idx: 0,
3201 });
3202 break;
3203 }
3204 }
3205 }
3206
3207 result.push(segs);
3208
3209 while span_idx < spans.len() {
3210 let span = &spans[span_idx];
3211 if span.text == "\n" {
3212 break;
3213 }
3214 let rest = &span.text[span_off..];
3215 let skip = leading_whitespace_len(rest);
3216 if skip > 0 {
3217 span_off += skip;
3218 if span_off >= span.text.len() {
3219 span_idx += 1;
3220 span_off = 0;
3221 }
3222 } else {
3223 break;
3224 }
3225 }
3226 }
3227
3228 result
3229}
3230
3231fn wrap_text(text: &str, max_width: f64, metrics: &FontMetrics) -> Vec<String> {
3232 let mut lines = Vec::new();
3233 let mut current = String::new();
3234 for word in text.split_whitespace() {
3235 if current.is_empty() {
3236 current = word.to_string();
3237 } else {
3238 let candidate = format!("{} {}", current, word);
3239 if metrics.measure_width(&candidate) <= max_width {
3240 current = candidate;
3241 } else {
3242 lines.push(current);
3243 current = word.to_string();
3244 }
3245 }
3246 }
3247 if !current.is_empty() {
3248 lines.push(current);
3249 }
3250 if lines.is_empty() && !text.is_empty() {
3251 lines.push(text.to_string());
3252 }
3253 lines
3254}
3255
3256fn push_pdf_string_byte(out: &mut String, b: u8) {
3257 match b {
3258 b'(' => out.push_str("\\("),
3259 b')' => out.push_str("\\)"),
3260 b'\\' => out.push_str("\\\\"),
3261 0x20..=0x7E => out.push(b as char),
3262 _ => {
3263 use std::fmt::Write;
3264 let _ = write!(out, "\\{:03o}", b);
3265 }
3266 }
3267}
3268
3269fn pdf_escape_with_simple_encoding(s: &str, unicode_to_code: Option<&HashMap<u16, u8>>) -> String {
3270 let mut out = String::with_capacity(s.len());
3271 for c in s.chars() {
3272 if let Some(map) = unicode_to_code {
3273 let mapped = u16::try_from(c as u32)
3274 .ok()
3275 .and_then(|cp| map.get(&cp).copied())
3276 .or_else(|| unicode_to_winansi(c));
3278 if let Some(b) = mapped {
3279 push_pdf_string_byte(&mut out, b);
3280 } else {
3281 out.push('?');
3282 }
3283 continue;
3284 }
3285
3286 match c {
3287 '(' => out.push_str("\\("),
3288 ')' => out.push_str("\\)"),
3289 '\\' => out.push_str("\\\\"),
3290 '\x20'..='\x7e' => out.push(c),
3291 _ => {
3292 if let Some(b) = unicode_to_winansi(c) {
3293 push_pdf_string_byte(&mut out, b);
3294 } else {
3295 out.push('?');
3296 }
3297 }
3298 }
3299 }
3300 out
3301}
3302
3303fn pdf_escape(s: &str) -> String {
3304 pdf_escape_with_simple_encoding(s, None)
3305}
3306
3307fn pdf_encode_text(s: &str, metrics: Option<&FontMetricsData>) -> String {
3310 if let Some(data) = metrics {
3311 if let Some(ref font_bytes) = data.font_data {
3312 if let Ok(face) = ttf_parser::Face::parse(font_bytes, data.face_index) {
3313 let mut hex = String::with_capacity(s.len() * 4 + 2);
3314 hex.push('<');
3315 for ch in s.chars() {
3316 let gid = face.glyph_index(ch).map(|g| g.0).unwrap_or(0);
3317 use std::fmt::Write;
3318 let _ = write!(hex, "{:04X}", gid);
3319 }
3320 hex.push('>');
3321 return hex;
3322 }
3323 }
3324 }
3325 let simple_map = metrics.and_then(|m| m.simple_unicode_to_code.as_ref());
3326 format!("({})", pdf_escape_with_simple_encoding(s, simple_map))
3327}
3328
3329fn lookup_font_metrics<'a>(
3338 node_style: &FormNodeStyle,
3339 config: &'a XfaRenderConfig,
3340) -> Option<&'a FontMetricsData> {
3341 node_style.font_family.as_ref().and_then(|tf| {
3342 let vkey = font_variant_key(
3343 tf,
3344 node_style.font_weight.as_deref(),
3345 node_style.font_style.as_deref(),
3346 );
3347 config
3348 .font_metrics_data
3349 .get(&vkey)
3350 .or_else(|| config.font_metrics_data.get(tf))
3351 })
3352}
3353
3354fn draw_line_span_enabled() -> bool {
3359 !matches!(
3360 std::env::var("XFA_DRAW_LINE_SPAN").as_deref(),
3361 Ok("0") | Ok("off") | Ok("false") | Ok("no")
3362 )
3363}
3364
3365#[allow(clippy::too_many_arguments)]
3366fn render_draw(
3367 draw_content: &DrawContent,
3368 abs_x: f64,
3369 pdf_y: f64,
3370 container_w: f64,
3371 container_h: f64,
3372 node_style: &FormNodeStyle,
3373 config: &XfaRenderConfig,
3374 ops: &mut Vec<u8>,
3375) {
3376 match draw_content {
3377 DrawContent::Text(text) => {
3378 if !text.is_empty() {
3379 let fs = node_style.font_size.unwrap_or(config.default_font_size);
3380 let font_family = match node_style.font_family.as_deref() {
3381 Some(f) if f.contains("Courier") || f.contains("Mono") => FontFamily::Monospace,
3382 Some(f)
3383 if f.contains("Helvetica")
3384 || f.contains("Arial")
3385 || f.contains("Sans")
3386 || f.contains("Myriad") =>
3387 {
3388 FontFamily::SansSerif
3389 }
3390 _ => FontFamily::Serif,
3391 };
3392 let font_ref = resolve_font_ref(&config.font_map, node_style, font_family);
3393 let tc = node_style
3394 .text_color
3395 .map(|(r, g, b)| [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0])
3396 .unwrap_or(config.text_color);
3397 let idh_metrics = lookup_font_metrics(node_style, config);
3398 let encoded = pdf_encode_text(text, idh_metrics);
3399 write_ops(
3400 ops,
3401 format_args!(
3402 "BT\n{:.3} {:.3} {:.3} rg\n{} {:.1} Tf\n",
3403 tc[0], tc[1], tc[2], font_ref, fs,
3404 ),
3405 );
3406 emit_synthetic_bold_ops(&config.font_map, node_style, fs, &tc, ops);
3407 write_ops(
3408 ops,
3409 format_args!("{:.2} {:.2} Td\n{} Tj\n", abs_x, pdf_y, encoded),
3410 );
3411 reset_synthetic_bold_ops(&config.font_map, node_style, ops);
3412 ops.extend_from_slice(b"ET\n");
3413 }
3414 }
3415 DrawContent::Line { x1, y1, x2, y2 } => {
3416 let coordless = *x1 == 0.0 && *y1 == 0.0 && *x2 == 0.0 && *y2 == 0.0;
3430 let span =
3431 draw_line_span_enabled() && coordless && (container_w > 0.0 || container_h > 0.0);
3432 if span {
3433 if let Some((r, g, b)) = node_style.border_color {
3434 write_ops(
3435 ops,
3436 format_args!(
3437 "{:.4} {:.4} {:.4} RG\n",
3438 r as f64 / 255.0,
3439 g as f64 / 255.0,
3440 b as f64 / 255.0
3441 ),
3442 );
3443 }
3444 if let Some(w_pt) = node_style.border_width_pt {
3445 write_ops(ops, format_args!("{:.2} w\n", w_pt));
3446 }
3447 }
3448 let (lx1, ly1, lx2, ly2) = if span {
3449 (0.0, 0.0, container_w, container_h)
3450 } else {
3451 (*x1, *y1, *x2, *y2)
3452 };
3453 let start_x = abs_x + lx1;
3454 let start_y = pdf_y + container_h - ly1;
3455 let end_x = abs_x + lx2;
3456 let end_y = pdf_y + container_h - ly2;
3457 write_ops(
3458 ops,
3459 format_args!(
3460 "{:.2} {:.2} m\n{:.2} {:.2} l\nS\n",
3461 start_x, start_y, end_x, end_y
3462 ),
3463 );
3464 }
3465 DrawContent::Rectangle { x, y, w, h, radius } => {
3466 let rx = abs_x + x;
3467 let ry = pdf_y + container_h - y - h;
3468 if let Some((r, g, b)) = node_style.border_color {
3470 write_ops(
3471 ops,
3472 format_args!(
3473 "{:.4} {:.4} {:.4} RG\n",
3474 r as f64 / 255.0,
3475 g as f64 / 255.0,
3476 b as f64 / 255.0
3477 ),
3478 );
3479 }
3480 if let Some(w_pt) = node_style.border_width_pt {
3481 write_ops(ops, format_args!("{:.2} w\n", w_pt));
3482 }
3483 if *radius <= 0.0 {
3484 write_ops(
3485 ops,
3486 format_args!("{:.2} {:.2} {:.2} {:.2} re\nS\n", rx, ry, w, h),
3487 );
3488 } else {
3489 let r = radius.min(w / 2.0).min(h / 2.0);
3490 let k = r * 0.5522847498;
3491 write_ops(
3492 ops,
3493 format_args!(
3494 "{:.2} {:.2} m\n\
3495 {:.2} {:.2} l\n\
3496 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
3497 {:.2} {:.2} l\n\
3498 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
3499 {:.2} {:.2} l\n\
3500 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
3501 {:.2} {:.2} l\n\
3502 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\n\
3503 h\nS\n",
3504 rx,
3505 ry + r,
3506 rx,
3507 ry + h - r,
3508 rx,
3509 ry + h - r + k,
3510 rx + r - k,
3511 ry + h,
3512 rx + r,
3513 ry + h,
3514 rx + w - r,
3515 ry + h,
3516 rx + w - r + k,
3517 ry + h,
3518 rx + w,
3519 ry + h - r + k,
3520 rx + w,
3521 ry + h - r,
3522 rx + w,
3523 ry + r,
3524 rx + w,
3525 ry + r - k,
3526 rx + w - r + k,
3527 ry,
3528 rx + w - r,
3529 ry,
3530 rx + r,
3531 ry,
3532 rx + r - k,
3533 ry,
3534 rx,
3535 ry + r - k,
3536 rx,
3537 ry + r,
3538 ),
3539 );
3540 }
3541 }
3542 DrawContent::Arc {
3543 x,
3544 y,
3545 w,
3546 h,
3547 start_angle,
3548 sweep_angle,
3549 } => {
3550 let cx = abs_x + x + w / 2.0;
3551 let cy = pdf_y + container_h - y - h / 2.0;
3552 let rx = w / 2.0;
3553 let ry = h / 2.0;
3554 let start_rad = start_angle.to_radians();
3555 let sweep_rad = sweep_angle.to_radians();
3556 let end_angle = start_rad + sweep_rad;
3557 let k = 0.5522847498;
3558 let cos_start = start_rad.cos();
3559 let sin_start = start_rad.sin();
3560 let cos_end = end_angle.cos();
3561 let sin_end = end_angle.sin();
3562 let p1x = cx + rx * cos_start;
3563 let p1y = cy + ry * sin_start;
3564 let p2x = cx + rx * cos_end;
3565 let p2y = cy + ry * sin_end;
3566 let cp1x = cx - rx * k * sin_start;
3567 let cp1y = cy + ry * k * cos_start;
3568 let cp2x = cx + rx * k * sin_end;
3569 let cp2y = cy - ry * k * cos_end;
3570 write_ops(
3571 ops,
3572 format_args!(
3573 "{:.2} {:.2} m\n{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c\nS\n",
3574 p1x, p1y, cp1x, cp1y, cp2x, cp2y, p2x, p2y
3575 ),
3576 );
3577 }
3578 }
3579}
3580
3581pub(crate) fn unicode_to_winansi(c: char) -> Option<u8> {
3582 let cp = c as u32;
3583 if (0xA0..=0xFF).contains(&cp) {
3584 return Some(cp as u8);
3585 }
3586 match c {
3587 '\u{20AC}' => Some(0x80),
3588 '\u{201A}' => Some(0x82),
3589 '\u{0192}' => Some(0x83),
3590 '\u{201E}' => Some(0x84),
3591 '\u{2026}' => Some(0x85),
3592 '\u{2020}' => Some(0x86),
3593 '\u{2021}' => Some(0x87),
3594 '\u{02C6}' => Some(0x88),
3595 '\u{2030}' => Some(0x89),
3596 '\u{0160}' => Some(0x8A),
3597 '\u{2039}' => Some(0x8B),
3598 '\u{0152}' => Some(0x8C),
3599 '\u{017D}' => Some(0x8E),
3600 '\u{2018}' => Some(0x91),
3601 '\u{2019}' => Some(0x92),
3602 '\u{201C}' => Some(0x93),
3603 '\u{201D}' => Some(0x94),
3604 '\u{2022}' => Some(0x95),
3605 '\u{2013}' => Some(0x96),
3606 '\u{2014}' => Some(0x97),
3607 '\u{02DC}' => Some(0x98),
3608 '\u{2122}' => Some(0x99),
3609 '\u{0161}' => Some(0x9A),
3610 '\u{203A}' => Some(0x9B),
3611 '\u{0153}' => Some(0x9C),
3612 '\u{017E}' => Some(0x9E),
3613 '\u{0178}' => Some(0x9F),
3614 _ => None,
3615 }
3616}
3617
3618fn write_ops(buf: &mut Vec<u8>, args: std::fmt::Arguments<'_>) {
3619 use std::io::Write;
3620 let _ = buf.write_fmt(args);
3621}
3622
3623#[cfg(test)]
3624mod tests {
3625 use super::*;
3626 use xfa_layout_engine::form::FormNodeId;
3627 use xfa_layout_engine::types::Rect;
3628
3629 fn make_page(nodes: Vec<LayoutNode>) -> LayoutPage {
3630 LayoutPage {
3631 width: 612.0,
3632 height: 792.0,
3633 nodes,
3634 runtime_instantiated: false,
3635 }
3636 }
3637
3638 fn make_field_node(x: f64, y: f64, w: f64, h: f64, value: &str) -> LayoutNode {
3639 LayoutNode {
3640 form_node: FormNodeId(0),
3641 rect: Rect::new(x, y, w, h),
3642 name: "field1".to_string(),
3643 content: LayoutContent::Field {
3644 value: value.to_string(),
3645 field_kind: FieldKind::Text,
3646 font_size: 0.0,
3647 font_family: FontFamily::Serif,
3648 },
3649 children: vec![],
3650 style: Default::default(),
3651 display_items: vec![],
3652 save_items: vec![],
3653 }
3654 }
3655
3656 fn make_styled_field(
3657 x: f64,
3658 y: f64,
3659 w: f64,
3660 h: f64,
3661 value: &str,
3662 style: FormNodeStyle,
3663 ) -> LayoutNode {
3664 LayoutNode {
3665 form_node: FormNodeId(0),
3666 rect: Rect::new(x, y, w, h),
3667 name: "styled".to_string(),
3668 content: LayoutContent::Field {
3669 value: value.to_string(),
3670 field_kind: FieldKind::Text,
3671 font_size: 10.0,
3672 font_family: FontFamily::Serif,
3673 },
3674 children: vec![],
3675 style,
3676 display_items: vec![],
3677 save_items: vec![],
3678 }
3679 }
3680
3681 fn make_styled_field_kind(
3682 x: f64,
3683 y: f64,
3684 w: f64,
3685 h: f64,
3686 value: &str,
3687 field_kind: FieldKind,
3688 style: FormNodeStyle,
3689 ) -> LayoutNode {
3690 LayoutNode {
3691 form_node: FormNodeId(0),
3692 rect: Rect::new(x, y, w, h),
3693 name: "styled-kind".to_string(),
3694 content: LayoutContent::Field {
3695 value: value.to_string(),
3696 field_kind,
3697 font_size: 10.0,
3698 font_family: FontFamily::Serif,
3699 },
3700 children: vec![],
3701 style,
3702 display_items: vec![],
3703 save_items: vec![],
3704 }
3705 }
3706
3707 fn make_styled_checkbox(
3708 x: f64,
3709 y: f64,
3710 w: f64,
3711 h: f64,
3712 value: &str,
3713 style: FormNodeStyle,
3714 ) -> LayoutNode {
3715 LayoutNode {
3716 form_node: FormNodeId(0),
3717 rect: Rect::new(x, y, w, h),
3718 name: "checkbox".to_string(),
3719 content: LayoutContent::Field {
3720 value: value.to_string(),
3721 field_kind: FieldKind::Checkbox,
3722 font_size: 10.0,
3723 font_family: FontFamily::Serif,
3724 },
3725 children: vec![],
3726 style,
3727 display_items: vec![],
3728 save_items: vec![],
3729 }
3730 }
3731
3732 fn make_styled_radio(
3733 x: f64,
3734 y: f64,
3735 w: f64,
3736 h: f64,
3737 value: &str,
3738 style: FormNodeStyle,
3739 ) -> LayoutNode {
3740 LayoutNode {
3741 form_node: FormNodeId(0),
3742 rect: Rect::new(x, y, w, h),
3743 name: "radio".to_string(),
3744 content: LayoutContent::Field {
3745 value: value.to_string(),
3746 field_kind: FieldKind::Radio,
3747 font_size: 10.0,
3748 font_family: FontFamily::Serif,
3749 },
3750 children: vec![],
3751 style,
3752 display_items: vec![],
3753 save_items: vec![],
3754 }
3755 }
3756
3757 fn make_styled_button(
3758 x: f64,
3759 y: f64,
3760 w: f64,
3761 h: f64,
3762 value: &str,
3763 style: FormNodeStyle,
3764 ) -> LayoutNode {
3765 LayoutNode {
3766 form_node: FormNodeId(0),
3767 rect: Rect::new(x, y, w, h),
3768 name: "button".to_string(),
3769 content: LayoutContent::Field {
3770 value: value.to_string(),
3771 field_kind: FieldKind::Button,
3772 font_size: 10.0,
3773 font_family: FontFamily::Serif,
3774 },
3775 children: vec![],
3776 style,
3777 display_items: vec![],
3778 save_items: vec![],
3779 }
3780 }
3781
3782 #[test]
3783 fn coordinate_mapping() {
3784 let mapper = CoordinateMapper::new(792.0, 612.0);
3785 assert!((mapper.xfa_to_pdf_y(0.0, 20.0) - 772.0).abs() < 0.001);
3786 }
3787
3788 fn overlay_str(page: &LayoutPage) -> String {
3789 let o = generate_page_overlay(page, &XfaRenderConfig::default()).unwrap();
3790 String::from_utf8_lossy(&o.content_stream).into_owned()
3791 }
3792
3793 #[test]
3794 fn empty_page_overlay() {
3795 let s = overlay_str(&make_page(vec![]));
3796 assert!(s.starts_with("q\n") && s.ends_with("Q\n"));
3797 }
3798
3799 #[test]
3800 fn field_renders_text() {
3801 let s = overlay_str(&make_page(vec![make_field_node(
3802 10.0, 10.0, 100.0, 20.0, "Hello",
3803 )]));
3804 assert!(s.contains("(Hello) Tj") && s.contains("BT") && s.contains("ET"));
3805 }
3806
3807 #[test]
3808 fn empty_field_no_text() {
3809 let s = overlay_str(&make_page(vec![make_field_node(
3810 10.0, 10.0, 100.0, 20.0, "",
3811 )]));
3812 assert!(!s.contains("BT"));
3813 }
3814
3815 #[test]
3816 fn dropdown_renders_display_item_for_matching_save_value() {
3817 let node = LayoutNode {
3818 form_node: FormNodeId(0),
3819 rect: Rect::new(10.0, 10.0, 100.0, 20.0),
3820 name: "choice".to_string(),
3821 content: LayoutContent::Field {
3822 value: "CA".to_string(),
3823 field_kind: FieldKind::Dropdown,
3824 font_size: 10.0,
3825 font_family: FontFamily::Serif,
3826 },
3827 children: vec![],
3828 style: Default::default(),
3829 display_items: vec!["California".to_string(), "Nevada".to_string()],
3830 save_items: vec!["CA".to_string(), "NV".to_string()],
3831 };
3832
3833 let s = overlay_str(&make_page(vec![node]));
3834 assert!(
3835 s.contains("(California) Tj"),
3836 "dropdown should render display item: {s}"
3837 );
3838 assert!(
3839 !s.contains("(CA) Tj"),
3840 "dropdown should not render raw save value: {s}"
3841 );
3842 }
3843
3844 #[test]
3845 fn all_overlays() {
3846 let layout = LayoutDom {
3847 pages: vec![
3848 make_page(vec![make_field_node(0.0, 0.0, 50.0, 20.0, "P1")]),
3849 make_page(vec![make_field_node(0.0, 0.0, 50.0, 20.0, "P2")]),
3850 ],
3851 };
3852 assert_eq!(
3853 generate_all_overlays(&layout, &XfaRenderConfig::default())
3854 .unwrap()
3855 .len(),
3856 2
3857 );
3858 }
3859
3860 #[test]
3861 fn pdf_escape_winansi_encoding() {
3862 assert_eq!(pdf_escape("Hello"), "Hello");
3863 assert_eq!(pdf_escape("a(b)c\\d"), "a\\(b\\)c\\\\d");
3864 assert_eq!(pdf_escape("\u{2013}"), "\\226");
3865 assert_eq!(pdf_escape("\u{2022}"), "\\225");
3866 assert_eq!(pdf_escape("\u{00A9}"), "\\251");
3867 assert_eq!(pdf_escape("\u{4E16}"), "?");
3868 }
3869
3870 fn styled_overlay_str(node: LayoutNode) -> String {
3871 let o = generate_page_overlay(&make_page(vec![node]), &XfaRenderConfig::default()).unwrap();
3872 String::from_utf8_lossy(&o.content_stream).into_owned()
3873 }
3874
3875 fn styled_overlay_str_with_config(node: LayoutNode, config: XfaRenderConfig) -> String {
3876 let o = generate_page_overlay(&make_page(vec![node]), &config).unwrap();
3877 String::from_utf8_lossy(&o.content_stream).into_owned()
3878 }
3879
3880 #[test]
3881 fn rounded_border_emits_bezier() {
3882 let style = FormNodeStyle {
3883 border_width_pt: Some(1.0),
3884 border_radius_pt: Some(5.0),
3885 ..Default::default()
3886 };
3887 let s = styled_overlay_str(make_styled_field(10.0, 10.0, 100.0, 20.0, "Hi", style));
3888 assert!(s.contains(" c\n"), "expected Bezier");
3889 assert!(s.contains("h\n"), "expected close-path");
3890 }
3891
3892 #[test]
3893 fn button_default_border_radius_is_zero() {
3894 let s = styled_overlay_str(make_styled_button(
3895 10.0,
3896 10.0,
3897 100.0,
3898 20.0,
3899 "Click",
3900 FormNodeStyle::default(),
3901 ));
3902 assert!(
3903 !s.contains(" c\n"),
3904 "default button border radius should stay square: {s}"
3905 );
3906 }
3907
3908 #[test]
3909 fn button_with_caption_renders_even_when_value_is_empty() {
3910 let style = FormNodeStyle {
3911 caption_text: Some("Click".to_string()),
3912 ..Default::default()
3913 };
3914 let s = styled_overlay_str(make_styled_button(10.0, 10.0, 100.0, 20.0, "", style));
3915 assert!(
3916 s.contains("(Click) Tj"),
3917 "button caption should render as label: {s}"
3918 );
3919 }
3920
3921 #[test]
3922 fn caption_uses_its_own_font_not_the_field_font() {
3923 let mk = || {
3927 let mut cfg = XfaRenderConfig::default();
3928 let mut fm = HashMap::new();
3929 fm.insert("Times New Roman".to_string(), "/Fserif".to_string());
3930 fm.insert("Arial".to_string(), "/Fsans".to_string());
3931 cfg.font_map = Arc::new(fm);
3932 let style = FormNodeStyle {
3933 font_family: Some("Times New Roman".to_string()),
3934 caption_text: Some("Name".to_string()),
3935 caption_placement: Some("top".to_string()),
3936 caption_reserve: Some(12.0),
3937 caption_font_family: Some("Arial".to_string()),
3938 ..Default::default()
3939 };
3940 (make_styled_field(10.0, 10.0, 200.0, 30.0, "", style), cfg)
3941 };
3942 let font_of_name = |s: &str| -> String {
3944 let i = s.find("(Name) Tj").expect("caption must render");
3945 let pre = &s[..i];
3946 let tf = pre.rfind(" Tf").expect("a Tf before the caption");
3947 pre[..tf]
3948 .rsplit('/')
3949 .next()
3950 .unwrap()
3951 .split_whitespace()
3952 .next()
3953 .unwrap()
3954 .to_string()
3955 };
3956
3957 std::env::remove_var("XFA_CAPTION_OWN_FONT");
3959 let (node, cfg) = mk();
3960 assert_eq!(
3961 font_of_name(&styled_overlay_str_with_config(node, cfg)),
3962 "Fsans",
3963 "caption must render in its own Arial face"
3964 );
3965
3966 std::env::set_var("XFA_CAPTION_OWN_FONT", "0");
3968 let (node, cfg) = mk();
3969 assert_eq!(
3970 font_of_name(&styled_overlay_str_with_config(node, cfg)),
3971 "Fserif",
3972 "opt-out must restore the field font for the caption"
3973 );
3974 std::env::remove_var("XFA_CAPTION_OWN_FONT");
3975 }
3976
3977 #[test]
3978 fn dashed_border_emits_dash_pattern() {
3979 let style = FormNodeStyle {
3980 border_width_pt: Some(1.0),
3981 border_style: Some("dashed".to_string()),
3982 ..Default::default()
3983 };
3984 let s = styled_overlay_str(make_styled_field(10.0, 10.0, 100.0, 20.0, "Hi", style));
3985 assert!(s.contains("[3 2] 0 d"), "expected dash");
3986 assert!(s.contains("[] 0 d"), "expected reset");
3987 }
3988
3989 #[test]
3990 fn field_per_edge_widths_render_without_uniform_border_width() {
3991 let style = FormNodeStyle {
3992 border_widths: Some([1.0, 2.0, 1.0, 3.0]),
3993 border_edges: [false, true, false, true],
3994 ..Default::default()
3995 };
3996 let s = styled_overlay_str(make_styled_field(10.0, 10.0, 100.0, 20.0, "", style));
3997 assert!(s.contains("2.00 w"), "right edge width should be used: {s}");
3998 assert!(s.contains("3.00 w"), "left edge width should be used: {s}");
3999 assert!(
4000 s.contains("110.00 762.00 m 110.00 782.00 l S"),
4001 "right edge should render even without border_width_pt: {s}"
4002 );
4003 assert!(
4004 s.contains("10.00 762.00 m 10.00 782.00 l S"),
4005 "left edge should render even without border_width_pt: {s}"
4006 );
4007 }
4008
4009 #[test]
4010 fn container_per_edge_widths_render_without_uniform_border_width() {
4011 let node = LayoutNode {
4012 form_node: FormNodeId(0),
4013 rect: Rect::new(10.0, 10.0, 100.0, 20.0),
4014 name: "box".to_string(),
4015 content: LayoutContent::None,
4016 children: vec![],
4017 style: FormNodeStyle {
4018 border_widths: Some([1.0, 2.0, 1.0, 3.0]),
4019 border_edges: [false, true, false, true],
4020 ..Default::default()
4021 },
4022 display_items: vec![],
4023 save_items: vec![],
4024 };
4025 let s = styled_overlay_str(node);
4026 assert!(s.contains("2.00 w"), "right edge width should be used: {s}");
4027 assert!(s.contains("3.00 w"), "left edge width should be used: {s}");
4028 assert!(
4029 s.contains("110.00 762.00 m 110.00 782.00 l S"),
4030 "right edge should render for non-field nodes: {s}"
4031 );
4032 assert!(
4033 s.contains("10.00 762.00 m 10.00 782.00 l S"),
4034 "left edge should render for non-field nodes: {s}"
4035 );
4036 }
4037
4038 #[test]
4039 fn para_margins_applied() {
4040 let style = FormNodeStyle {
4041 margin_left_pt: Some(5.0),
4042 margin_right_pt: Some(3.0),
4043 space_above_pt: Some(2.0),
4044 ..Default::default()
4045 };
4046 let s = styled_overlay_str(make_styled_field(10.0, 10.0, 200.0, 30.0, "Test", style));
4047 assert!(s.contains("15.00"), "expected margin_left offset 10+5=15");
4048 }
4049
4050 #[test]
4051 fn top_caption_renders_after_field_fill() {
4052 let style = FormNodeStyle {
4053 caption_text: Some("PROJECT INFORMATION/NAME".to_string()),
4054 caption_placement: Some("top".to_string()),
4055 caption_reserve: Some(12.0),
4056 bg_color: Some((12, 34, 56)),
4057 ..Default::default()
4058 };
4059 let s = styled_overlay_str(make_styled_field(10.0, 100.0, 200.0, 30.0, "", style));
4060 let fill_idx = s
4061 .find("0.047 0.133 0.220 rg")
4062 .expect("explicit field fill should be present");
4063 let caption_idx = s
4064 .find("(PROJECT INFORMATION/NAME) Tj")
4065 .expect("caption text should render");
4066 assert!(
4067 caption_idx > fill_idx,
4068 "caption should render after the field fill so it stays visible: {s}"
4069 );
4070 }
4071
4072 #[test]
4073 fn left_caption_stays_in_pre_body_render_path() {
4074 let style = FormNodeStyle {
4075 caption_text: Some("Field 1".to_string()),
4076 caption_placement: Some("left".to_string()),
4077 bg_color: Some((12, 34, 56)),
4078 ..Default::default()
4079 };
4080 let s = styled_overlay_str(make_styled_field(10.0, 100.0, 200.0, 30.0, "", style));
4081 let caption_idx = s.find("(Field 1) Tj").expect("caption text should render");
4082 let fill_idx = s
4083 .find("0.047 0.133 0.220 rg")
4084 .expect("explicit field fill should be present");
4085 assert!(
4086 caption_idx < fill_idx,
4087 "left captions should keep the legacy pre-body ordering: {s}"
4088 );
4089 }
4090
4091 #[test]
4092 fn left_caption_without_explicit_reserve_shifts_field_body() {
4093 let style = FormNodeStyle {
4094 caption_text: Some("Field 1".to_string()),
4095 caption_placement: Some("left".to_string()),
4096 bg_color: Some((12, 34, 56)),
4097 ..Default::default()
4098 };
4099 let config = XfaRenderConfig::default();
4100 let reserve = effective_caption_reserve(&style, 10.0, FontFamily::Serif, &config);
4101 let metrics = build_font_metrics(10.0, FontFamily::Serif, &style, &config);
4102 assert!(
4103 (reserve - metrics.measure_width("Field 1")).abs() < 0.01,
4104 "auto reserve should match caption width for simple left captions"
4105 );
4106
4107 let s = styled_overlay_str(make_styled_field(10.0, 100.0, 200.0, 30.0, "", style));
4108 let mapper = CoordinateMapper::new(792.0, 612.0);
4109 let expected_fill = format!(
4110 "{:.2} {:.2} {:.2} 30.00 re",
4111 10.0 + reserve,
4112 mapper.xfa_to_pdf_y(100.0, 30.0),
4113 200.0 - reserve
4114 );
4115 assert!(
4116 s.contains(&expected_fill),
4117 "field body should be shifted right by the caption reserve: {s}"
4118 );
4119 }
4120
4121 #[test]
4122 fn button_caption_does_not_shrink_button_body() {
4123 let style = FormNodeStyle {
4124 caption_text: Some("Click".to_string()),
4125 caption_placement: Some("left".to_string()),
4126 bg_color: Some((12, 34, 56)),
4127 ..Default::default()
4128 };
4129 let config = XfaRenderConfig::default();
4130 let reserve = effective_caption_reserve(&style, 10.0, FontFamily::Serif, &config);
4131 assert!(
4132 reserve > 0.0,
4133 "button caption should still have measurable text"
4134 );
4135
4136 let s = styled_overlay_str(make_styled_button(10.0, 100.0, 200.0, 30.0, "", style));
4137 let mapper = CoordinateMapper::new(792.0, 612.0);
4138 let expected_fill = format!(
4139 "{:.2} {:.2} 200.00 30.00 re",
4140 10.0,
4141 mapper.xfa_to_pdf_y(100.0, 30.0)
4142 );
4143 assert!(
4144 s.contains(&expected_fill),
4145 "button body should keep the full field width because its caption is rendered internally: {s}"
4146 );
4147 }
4148
4149 #[test]
4150 fn top_caption_uses_full_inner_rect_height() {
4151 let style = FormNodeStyle {
4152 caption_text: Some("TOP CAPTION".to_string()),
4153 caption_placement: Some("top".to_string()),
4154 caption_reserve: Some(12.0),
4155 ..Default::default()
4156 };
4157 let s = styled_overlay_str(make_styled_field(
4158 10.0,
4159 100.0,
4160 200.0,
4161 30.0,
4162 "",
4163 style.clone(),
4164 ));
4165 let config = XfaRenderConfig::default();
4166 let metrics = build_font_metrics(10.0, FontFamily::Serif, &style, &config);
4167 let asc_pt = ascender_pt(&metrics, 10.0);
4168 let mapper = CoordinateMapper::new(792.0, 612.0);
4169 let inner_pdf_y = mapper.xfa_to_pdf_y(100.0, 30.0);
4170 let expected = format!(
4171 "{:.2} {:.2} Td\n(TOP CAPTION) Tj",
4172 10.0,
4173 inner_pdf_y + 30.0 - asc_pt
4174 );
4175 assert!(
4176 s.contains(&expected),
4177 "top caption should be positioned against the full inner rect, not the value area: {s}"
4178 );
4179 }
4180
4181 #[test]
4182 fn v_align_middle() {
4183 let style = FormNodeStyle {
4184 v_align: Some(VerticalAlign::Middle),
4185 ..Default::default()
4186 };
4187 let s = styled_overlay_str(make_styled_field(0.0, 0.0, 200.0, 40.0, "Mid", style));
4188 assert!(s.contains("(Mid) Tj"));
4189 }
4190
4191 #[test]
4192 fn text_field_without_explicit_fill_has_no_default_background() {
4193 let s = styled_overlay_str(make_styled_field(
4194 10.0,
4195 10.0,
4196 100.0,
4197 20.0,
4198 "Hi",
4199 FormNodeStyle::default(),
4200 ));
4201 assert!(
4202 !s.contains("0.949 0.949 0.949 rg"),
4203 "flatten output should not synthesize an interactive default field fill: {s}"
4204 );
4205 }
4206
4207 #[test]
4208 fn numeric_field_without_explicit_fill_has_no_default_background() {
4209 let s = styled_overlay_str(make_styled_field_kind(
4210 10.0,
4211 10.0,
4212 100.0,
4213 20.0,
4214 "42",
4215 FieldKind::NumericEdit,
4216 FormNodeStyle::default(),
4217 ));
4218 assert!(
4219 !s.contains("0.949 0.949 0.949 rg"),
4220 "numeric fields should also require explicit template fill to paint a background: {s}"
4221 );
4222 }
4223
4224 #[test]
4225 fn password_field_masks_plaintext_value() {
4226 let s = styled_overlay_str(make_styled_field_kind(
4227 10.0,
4228 10.0,
4229 100.0,
4230 20.0,
4231 "secret",
4232 FieldKind::PasswordEdit,
4233 FormNodeStyle::default(),
4234 ));
4235 assert!(
4236 !s.contains("(secret) Tj"),
4237 "password fields must not emit plaintext into the content stream: {s}"
4238 );
4239 assert!(
4240 s.contains("(\\225\\225\\225\\225\\225\\225) Tj"),
4241 "password fields should render bullet masking instead of plaintext: {s}"
4242 );
4243 }
4244
4245 #[test]
4246 fn explicit_white_field_background_is_preserved() {
4247 let s = styled_overlay_str(make_styled_field(
4248 10.0,
4249 10.0,
4250 100.0,
4251 20.0,
4252 "Hi",
4253 FormNodeStyle {
4254 bg_color: Some((255, 255, 255)),
4255 ..Default::default()
4256 },
4257 ));
4258 assert!(
4259 s.contains("1.000 1.000 1.000 rg"),
4260 "explicit white field fills should stay white: {s}"
4261 );
4262 }
4263
4264 #[test]
4265 fn checkbox_does_not_use_edit_field_default_background() {
4266 let s = styled_overlay_str(make_styled_checkbox(
4267 10.0,
4268 10.0,
4269 20.0,
4270 20.0,
4271 "0",
4272 FormNodeStyle {
4273 border_width_pt: Some(0.25),
4274 ..Default::default()
4275 },
4276 ));
4277 assert!(
4278 !s.contains("0.949 0.949 0.949 rg"),
4279 "non-edit widgets should not inherit the text field gray fill: {s}"
4280 );
4281 }
4282
4283 #[test]
4284 fn checkbox_explicit_background_fill_is_rendered() {
4285 let s = styled_overlay_str(make_styled_checkbox(
4286 10.0,
4287 10.0,
4288 20.0,
4289 20.0,
4290 "0",
4291 FormNodeStyle {
4292 bg_color: Some((255, 255, 255)),
4293 ..Default::default()
4294 },
4295 ));
4296 assert!(
4297 s.contains("1.000 1.000 1.000 rg"),
4298 "checkbox fill color should be emitted when bg_color is present: {s}"
4299 );
4300 assert!(
4301 s.contains("20.00 20.00 re\nf"),
4302 "checkbox background should be painted as a filled rectangle before the border: {s}"
4303 );
4304 }
4305
4306 #[test]
4307 fn radio_explicit_background_fill_is_rendered() {
4308 let s = styled_overlay_str(make_styled_radio(
4309 10.0,
4310 10.0,
4311 20.0,
4312 20.0,
4313 "N",
4314 FormNodeStyle {
4315 bg_color: Some((255, 255, 255)),
4316 check_button_on_value: Some("Y".to_string()),
4317 check_button_off_value: Some("N".to_string()),
4318 ..Default::default()
4319 },
4320 ));
4321 assert!(
4322 s.contains("1.000 1.000 1.000 rg"),
4323 "radio fill color should be emitted when bg_color is present: {s}"
4324 );
4325 assert!(
4326 s.contains(" c\n") && s.contains("\nf\n"),
4327 "radio background should be painted as a filled circle path before the border: {s}"
4328 );
4329 }
4330
4331 #[test]
4332 fn checkbox_ignores_global_background_without_explicit_fill() {
4333 let config = XfaRenderConfig {
4334 background_color: Some([0.949, 0.949, 0.949]),
4335 ..Default::default()
4336 };
4337 let s = styled_overlay_str_with_config(
4338 make_styled_checkbox(10.0, 10.0, 20.0, 20.0, "0", FormNodeStyle::default()),
4339 config,
4340 );
4341 assert!(
4342 !s.contains("0.949 0.949 0.949 rg"),
4343 "checkbox should only fill from explicit style.bg_color: {s}"
4344 );
4345 }
4346
4347 #[test]
4348 fn radio_ignores_global_background_without_explicit_fill() {
4349 let config = XfaRenderConfig {
4350 background_color: Some([0.949, 0.949, 0.949]),
4351 ..Default::default()
4352 };
4353 let s = styled_overlay_str_with_config(
4354 make_styled_radio(
4355 10.0,
4356 10.0,
4357 20.0,
4358 20.0,
4359 "N",
4360 FormNodeStyle {
4361 check_button_on_value: Some("Y".to_string()),
4362 check_button_off_value: Some("N".to_string()),
4363 ..Default::default()
4364 },
4365 ),
4366 config,
4367 );
4368 assert!(
4369 !s.contains("0.949 0.949 0.949 rg"),
4370 "radio should only fill from explicit style.bg_color: {s}"
4371 );
4372 }
4373
4374 #[test]
4375 fn pdf_escape_polish_chars_fallback() {
4376 assert_eq!(pdf_escape("łżść"), "????");
4378 }
4379
4380 #[test]
4381 fn pdf_encode_text_winansi_fallback() {
4382 let encoded = pdf_encode_text("Hello", None);
4384 assert_eq!(encoded, "(Hello)");
4385 }
4386
4387 #[test]
4388 fn pdf_encode_text_identity_h() {
4389 let metrics = FontMetricsData {
4391 widths: vec![500; 256],
4392 upem: 1000,
4393 ascender: 800,
4394 descender: -200,
4395 font_data: None,
4396 face_index: 0,
4397 simple_unicode_to_code: None,
4398 };
4399 let encoded = pdf_encode_text("AB", Some(&metrics));
4401 assert_eq!(encoded, "(AB)");
4402 }
4403
4404 #[test]
4405 fn pdf_encode_text_simple_encoding_fallback() {
4406 let mut custom_map = HashMap::new();
4408 custom_map.insert(0x0163, 0x80);
4409 let metrics = FontMetricsData {
4410 widths: vec![500; 256],
4411 upem: 1000,
4412 ascender: 800,
4413 descender: -200,
4414 font_data: None,
4415 face_index: 0,
4416 simple_unicode_to_code: Some(custom_map),
4417 };
4418 let encoded = pdf_encode_text("ţ", Some(&metrics));
4419 assert_eq!(encoded, "(\\200)");
4420 }
4421
4422 #[test]
4423 fn rich_text_span_mapping_preserves_space_after_bold_label() {
4424 let spans = vec![
4425 RichTextSpan {
4426 text: "Instructions:".to_string(),
4427 font_size: None,
4428 font_family: None,
4429 font_weight: Some("bold".to_string()),
4430 font_style: None,
4431 text_color: None,
4432 underline: false,
4433 line_through: false,
4434 baseline_shift: 0.0,
4435 },
4436 RichTextSpan {
4437 text: "This form is for your use.".to_string(),
4438 font_size: None,
4439 font_family: None,
4440 font_weight: Some("normal".to_string()),
4441 font_style: None,
4442 text_color: None,
4443 underline: false,
4444 line_through: false,
4445 baseline_shift: 0.0,
4446 },
4447 ];
4448 let lines = vec!["Instructions: This form is for your use.".to_string()];
4449
4450 let mapped = map_spans_to_lines(&spans, &lines);
4451
4452 assert_eq!(mapped.len(), 1);
4453 assert_eq!(mapped[0].len(), 3);
4454 assert_eq!(mapped[0][0].text, "Instructions:");
4455 assert_eq!(mapped[0][0].span_idx, 0);
4456 assert_eq!(mapped[0][1].text, " ");
4457 assert_eq!(mapped[0][1].span_idx, 1);
4458 assert_eq!(mapped[0][2].text, "This form is for your use.");
4459 assert_eq!(mapped[0][2].span_idx, 1);
4460 }
4461
4462 #[test]
4463 fn rich_text_span_mapping_treats_nbsp_spaceruns_as_normal_spaces() {
4464 let spans = vec![
4465 RichTextSpan {
4466 text: "Instructions:".to_string(),
4467 font_size: None,
4468 font_family: None,
4469 font_weight: Some("bold".to_string()),
4470 font_style: None,
4471 text_color: None,
4472 underline: false,
4473 line_through: false,
4474 baseline_shift: 0.0,
4475 },
4476 RichTextSpan {
4477 text: "This form is for your use.".to_string(),
4478 font_size: None,
4479 font_family: None,
4480 font_weight: Some("normal".to_string()),
4481 font_style: None,
4482 text_color: None,
4483 underline: false,
4484 line_through: false,
4485 baseline_shift: 0.0,
4486 },
4487 RichTextSpan {
4488 text: "\u{00A0}\u{00A0}".to_string(),
4489 font_size: None,
4490 font_family: None,
4491 font_weight: Some("normal".to_string()),
4492 font_style: None,
4493 text_color: None,
4494 underline: false,
4495 line_through: false,
4496 baseline_shift: 0.0,
4497 },
4498 RichTextSpan {
4499 text: "Mail in at least 14 days before".to_string(),
4500 font_size: None,
4501 font_family: None,
4502 font_weight: Some("normal".to_string()),
4503 font_style: None,
4504 text_color: None,
4505 underline: false,
4506 line_through: false,
4507 baseline_shift: 0.0,
4508 },
4509 ];
4510 let lines = vec![
4511 "Instructions: This form is for your use.".to_string(),
4512 "Mail in at least 14 days before".to_string(),
4513 ];
4514
4515 let mapped = map_spans_to_lines(&spans, &lines);
4516
4517 assert_eq!(mapped.len(), 2);
4518 assert_eq!(mapped[0][0].span_idx, 0);
4519 assert_eq!(mapped[0][2].span_idx, 1);
4520 assert_eq!(mapped[1].len(), 1);
4521 assert_eq!(mapped[1][0].text, "Mail in at least 14 days before");
4522 assert_eq!(mapped[1][0].span_idx, 3);
4523 }
4524
4525 #[test]
4526 fn real_bold_font_variant_skips_synthetic_bold_stroke() {
4527 let mut config = XfaRenderConfig::default();
4528 let mut fm = HashMap::new();
4529 fm.insert("Arial_Bold_Normal".to_string(), "/XFA_Fbold".to_string());
4530 config.font_map = Arc::new(fm);
4531
4532 let s = styled_overlay_str_with_config(
4533 make_styled_field(
4534 10.0,
4535 10.0,
4536 200.0,
4537 20.0,
4538 "Bold",
4539 FormNodeStyle {
4540 font_family: Some("Arial".to_string()),
4541 font_weight: Some("bold".to_string()),
4542 ..Default::default()
4543 },
4544 ),
4545 config,
4546 );
4547
4548 assert!(
4549 !s.contains("2 Tr"),
4550 "actual bold variants should not get synthetic stroke bolding: {s}"
4551 );
4552 assert!(
4553 s.contains("/XFA_Fbold 10.0 Tf"),
4554 "expected real bold resource: {s}"
4555 );
4556 }
4557
4558 #[test]
4559 fn container_insets_offset_children() {
4560 let child = LayoutNode {
4563 form_node: FormNodeId(1),
4564 rect: Rect::new(0.0, 0.0, 50.0, 20.0),
4565 name: "child".to_string(),
4566 content: LayoutContent::Field {
4567 value: "Test".to_string(),
4568 field_kind: FieldKind::Text,
4569 font_size: 10.0,
4570 font_family: FontFamily::Serif,
4571 },
4572 children: vec![],
4573 style: Default::default(),
4574 display_items: vec![],
4575 save_items: vec![],
4576 };
4577 let parent = LayoutNode {
4578 form_node: FormNodeId(0),
4579 rect: Rect::new(100.0, 200.0, 200.0, 100.0),
4580 name: "parent".to_string(),
4581 content: LayoutContent::None,
4582 children: vec![child],
4583 style: FormNodeStyle {
4584 inset_left_pt: Some(10.0),
4585 inset_top_pt: Some(5.0),
4586 ..Default::default()
4587 },
4588 display_items: vec![],
4589 save_items: vec![],
4590 };
4591 let s = overlay_str(&make_page(vec![parent]));
4592 assert!(
4595 s.contains("110.00"),
4596 "child x should include parent left inset offset: {s}"
4597 );
4598 }
4599
4600 #[test]
4601 fn field_insets_reduce_text_wrap_width() {
4602 let node = LayoutNode {
4605 form_node: FormNodeId(0),
4606 rect: Rect::new(10.0, 10.0, 100.0, 30.0),
4607 name: "field".to_string(),
4608 content: LayoutContent::Field {
4609 value: "Hello".to_string(),
4610 field_kind: FieldKind::Text,
4611 font_size: 10.0,
4612 font_family: FontFamily::Serif,
4613 },
4614 children: vec![],
4615 style: FormNodeStyle {
4616 inset_left_pt: Some(8.0),
4617 inset_right_pt: Some(8.0),
4618 ..Default::default()
4619 },
4620 display_items: vec![],
4621 save_items: vec![],
4622 };
4623 let s = overlay_str(&make_page(vec![node]));
4624 assert!(
4626 s.contains("18.00"),
4627 "text x should include field left inset: {s}"
4628 );
4629 }
4630
4631 #[test]
4632 fn checkbox_border_width_respects_style() {
4633 let s = styled_overlay_str(make_styled_checkbox(
4634 10.0,
4635 10.0,
4636 20.0,
4637 20.0,
4638 "0",
4639 FormNodeStyle {
4640 border_width_pt: Some(0.25),
4641 ..Default::default()
4642 },
4643 ));
4644 assert!(
4645 s.contains("\n0.25 w\n"),
4646 "checkbox should use styled border width: {s}"
4647 );
4648 assert!(
4649 !s.contains("\n0.50 w\n"),
4650 "checkbox should not fall back to default 0.5pt border width: {s}"
4651 );
4652 assert!(
4653 !s.contains("\n1.00 w\n"),
4654 "checkbox should not clamp to 1pt border width: {s}"
4655 );
4656 }
4657
4658 #[test]
4659 fn container_children_y_offset_includes_inset() {
4660 let child = LayoutNode {
4661 form_node: FormNodeId(1),
4662 rect: Rect::new(0.0, 0.0, 50.0, 20.0),
4663 name: "child-box".to_string(),
4664 content: LayoutContent::None,
4665 children: vec![],
4666 style: FormNodeStyle {
4667 border_width_pt: Some(1.0),
4668 ..Default::default()
4669 },
4670 display_items: vec![],
4671 save_items: vec![],
4672 };
4673 let parent = LayoutNode {
4674 form_node: FormNodeId(0),
4675 rect: Rect::new(100.0, 200.0, 200.0, 100.0),
4676 name: "parent".to_string(),
4677 content: LayoutContent::None,
4678 children: vec![child],
4679 style: FormNodeStyle {
4680 inset_top_pt: Some(10.0),
4681 ..Default::default()
4682 },
4683 display_items: vec![],
4684 save_items: vec![],
4685 };
4686 let s = overlay_str(&make_page(vec![parent]));
4687 assert!(
4688 s.contains("100.00 562.00 50.00 20.00 re"),
4689 "child y should include parent inset_top offset: {s}"
4690 );
4691 assert!(
4692 !s.contains("100.00 572.00 50.00 20.00 re"),
4693 "child y should no longer ignore parent inset_top offset: {s}"
4694 );
4695 }
4696
4697 #[test]
4698 fn checkbox_mark_style_controls_rendered_symbol() {
4699 let default_overlay = styled_overlay_str(make_styled_checkbox(
4700 10.0,
4701 10.0,
4702 20.0,
4703 20.0,
4704 "1",
4705 FormNodeStyle::default(),
4706 ));
4707 let circle_overlay = styled_overlay_str(make_styled_checkbox(
4708 10.0,
4709 10.0,
4710 20.0,
4711 20.0,
4712 "1",
4713 FormNodeStyle {
4714 check_button_mark: Some("circle".to_string()),
4715 ..Default::default()
4716 },
4717 ));
4718
4719 assert!(
4720 !default_overlay.contains(" c\n"),
4721 "default checkbox mark should not emit Bezier circle commands: {default_overlay}"
4722 );
4723 assert!(
4724 circle_overlay.contains(" c\n"),
4725 "circle checkbox mark should emit Bezier circle commands: {circle_overlay}"
4726 );
4727 }
4728
4729 #[test]
4730 fn unchecked_checkbox_with_empty_value_still_draws_outline() {
4731 let overlay = styled_overlay_str(make_styled_checkbox(
4732 10.0,
4733 10.0,
4734 20.0,
4735 20.0,
4736 "",
4737 FormNodeStyle::default(),
4738 ));
4739
4740 assert!(
4741 overlay.contains("10.00 762.00 20.00 20.00 re"),
4742 "unchecked checkbox should still render its outline: {overlay}"
4743 );
4744 }
4745
4746 #[test]
4747 fn checkbox_checked_state_uses_template_item_values() {
4748 let checked_overlay = styled_overlay_str(make_styled_checkbox(
4749 10.0,
4750 10.0,
4751 20.0,
4752 20.0,
4753 "Yes",
4754 FormNodeStyle {
4755 check_button_on_value: Some("Yes".to_string()),
4756 check_button_off_value: Some("No".to_string()),
4757 ..Default::default()
4758 },
4759 ));
4760 let unchecked_overlay = styled_overlay_str(make_styled_checkbox(
4761 10.0,
4762 10.0,
4763 20.0,
4764 20.0,
4765 "No",
4766 FormNodeStyle {
4767 check_button_on_value: Some("Yes".to_string()),
4768 check_button_off_value: Some("No".to_string()),
4769 ..Default::default()
4770 },
4771 ));
4772
4773 assert!(
4774 checked_overlay.matches(" l\nS\n").count() >= 2,
4775 "asserted template on-value should render the check/cross mark: {checked_overlay}"
4776 );
4777 assert!(
4778 unchecked_overlay.matches(" l\nS\n").count() < 3,
4779 "template off-value should not render the asserted mark: {unchecked_overlay}"
4780 );
4781 }
4782
4783 #[test]
4784 fn radio_explicit_mark_overrides_default_circle() {
4785 let default_overlay = styled_overlay_str(make_styled_radio(
4786 10.0,
4787 10.0,
4788 20.0,
4789 20.0,
4790 "Y",
4791 FormNodeStyle {
4792 check_button_on_value: Some("Y".to_string()),
4793 check_button_off_value: Some("N".to_string()),
4794 ..Default::default()
4795 },
4796 ));
4797 let cross_overlay = styled_overlay_str(make_styled_radio(
4798 10.0,
4799 10.0,
4800 20.0,
4801 20.0,
4802 "Y",
4803 FormNodeStyle {
4804 check_button_mark: Some("cross".to_string()),
4805 check_button_on_value: Some("Y".to_string()),
4806 check_button_off_value: Some("N".to_string()),
4807 ..Default::default()
4808 },
4809 ));
4810
4811 assert!(
4812 default_overlay.matches(" c\n").count() >= 8,
4813 "default radio should render outer circle plus filled inner circle: {default_overlay}"
4814 );
4815 assert!(
4816 cross_overlay.matches(" l\nS\n").count() >= 2,
4817 "explicit radio mark should render the requested symbol: {cross_overlay}"
4818 );
4819 }
4820
4821 #[test]
4824 fn render_tree_page_dimensions() {
4825 let page = make_page(vec![]);
4826 let layout = LayoutDom { pages: vec![page] };
4827 let tree = layout_dom_to_render_tree(&layout, &XfaRenderConfig::default());
4828 assert_eq!(tree.pages.len(), 1);
4829 match &tree.pages[0] {
4830 RenderNode::Page { width, height, .. } => {
4831 assert!((*width - 612.0).abs() < 0.01, "width should be 612pt");
4832 assert!((*height - 792.0).abs() < 0.01, "height should be 792pt");
4833 }
4834 other => panic!("expected Page node, got {other:?}"),
4835 }
4836 }
4837
4838 #[test]
4839 fn render_tree_text_node_captured() {
4840 let node = LayoutNode {
4841 form_node: xfa_layout_engine::form::FormNodeId(0),
4842 rect: xfa_layout_engine::types::Rect::new(10.0, 20.0, 100.0, 15.0),
4843 name: "lbl".to_string(),
4844 content: LayoutContent::Text("Hello tree".to_string()),
4845 children: vec![],
4846 style: Default::default(),
4847 display_items: vec![],
4848 save_items: vec![],
4849 };
4850 let layout = LayoutDom {
4851 pages: vec![LayoutPage {
4852 width: 612.0,
4853 height: 792.0,
4854 nodes: vec![node],
4855 runtime_instantiated: false,
4856 }],
4857 };
4858 let tree = layout_dom_to_render_tree(&layout, &XfaRenderConfig::default());
4859 let debug = tree.to_debug_string();
4860 assert!(
4861 debug.contains("Hello tree"),
4862 "debug string should contain text content: {debug}"
4863 );
4864 assert!(
4865 debug.contains("Text("),
4866 "debug string should contain Text node: {debug}"
4867 );
4868 }
4869
4870 #[test]
4873 fn text_node_emits_correct_font_and_size_operators() {
4874 let node = LayoutNode {
4877 form_node: xfa_layout_engine::form::FormNodeId(0),
4878 rect: xfa_layout_engine::types::Rect::new(10.0, 10.0, 100.0, 20.0),
4879 name: "f".to_string(),
4880 content: LayoutContent::Field {
4881 value: "test value".to_string(),
4882 field_kind: FieldKind::Text,
4883 font_size: 12.0,
4884 font_family: xfa_layout_engine::text::FontFamily::Serif,
4885 },
4886 children: vec![],
4887 style: Default::default(),
4888 display_items: vec![],
4889 save_items: vec![],
4890 };
4891 let s = overlay_str(&make_page(vec![node]));
4892 assert!(
4894 s.contains("Tf"),
4895 "should contain Tf font-select operator: {s}"
4896 );
4897 assert!(
4898 s.contains("12.0 Tf"),
4899 "should use specified font size 12.0: {s}"
4900 );
4901 assert!(
4902 s.contains("(test value) Tj"),
4903 "should render the value: {s}"
4904 );
4905 }
4906
4907 #[test]
4908 fn multiline_text_has_correct_td_offsets() {
4909 let node = LayoutNode {
4911 form_node: xfa_layout_engine::form::FormNodeId(0),
4912 rect: xfa_layout_engine::types::Rect::new(10.0, 10.0, 100.0, 40.0),
4913 name: "ml".to_string(),
4914 content: LayoutContent::WrappedText {
4915 lines: vec!["line one".to_string(), "line two".to_string()],
4916 first_line_of_para: vec![true, false],
4917 font_size: 10.0,
4918 text_align: xfa_layout_engine::types::TextAlign::Left,
4919 font_family: xfa_layout_engine::text::FontFamily::Serif,
4920 space_above_pt: None,
4921 space_below_pt: None,
4922 from_field: false,
4924 },
4925 children: vec![],
4926 style: Default::default(),
4927 display_items: vec![],
4928 save_items: vec![],
4929 };
4930 let s = overlay_str(&make_page(vec![node]));
4931 assert!(s.contains("(line one) Tj"), "first line missing: {s}");
4933 assert!(s.contains("(line two) Tj"), "second line missing: {s}");
4934 let td_count = s.matches(" Td\n").count();
4936 assert!(td_count >= 2, "expected ≥2 Td operators for two lines: {s}");
4937 }
4938
4939 #[test]
4942 fn checkbox_checked_renders_nonempty_stream() {
4943 let node = make_styled_checkbox(
4944 10.0,
4945 10.0,
4946 20.0,
4947 20.0,
4948 "1",
4949 FormNodeStyle {
4950 check_button_on_value: Some("1".to_string()),
4951 check_button_off_value: Some("0".to_string()),
4952 border_width_pt: Some(0.5),
4953 ..Default::default()
4954 },
4955 );
4956 let s = overlay_str(&make_page(vec![node]));
4957 assert!(!s.is_empty(), "checked checkbox overlay must not be empty");
4958 assert!(s.contains("re"), "checkbox must emit a rectangle: {s}");
4959 }
4960
4961 #[test]
4962 fn radio_selected_renders_nonempty_stream() {
4963 let node = make_styled_radio(
4964 10.0,
4965 10.0,
4966 20.0,
4967 20.0,
4968 "yes",
4969 FormNodeStyle {
4970 check_button_on_value: Some("yes".to_string()),
4971 check_button_off_value: Some("no".to_string()),
4972 ..Default::default()
4973 },
4974 );
4975 let s = overlay_str(&make_page(vec![node]));
4976 assert!(!s.is_empty(), "selected radio overlay must not be empty");
4977 assert!(
4979 s.contains(" c\n"),
4980 "selected radio must render circular fill: {s}"
4981 );
4982 }
4983
4984 #[test]
4985 fn dropdown_selected_value_renders_nonempty_stream() {
4986 let node = LayoutNode {
4987 form_node: xfa_layout_engine::form::FormNodeId(0),
4988 rect: xfa_layout_engine::types::Rect::new(10.0, 10.0, 100.0, 20.0),
4989 name: "dd".to_string(),
4990 content: LayoutContent::Field {
4991 value: "opt1".to_string(),
4992 field_kind: FieldKind::Dropdown,
4993 font_size: 10.0,
4994 font_family: xfa_layout_engine::text::FontFamily::Serif,
4995 },
4996 children: vec![],
4997 style: Default::default(),
4998 display_items: vec!["Option 1".to_string()],
4999 save_items: vec!["opt1".to_string()],
5000 };
5001 let s = overlay_str(&make_page(vec![node]));
5002 assert!(!s.is_empty(), "dropdown overlay must not be empty");
5003 assert!(
5004 s.contains("(Option 1) Tj"),
5005 "dropdown must render display label: {s}"
5006 );
5007 }
5008
5009 #[test]
5012 fn text_field_renders_bound_value() {
5013 let node = LayoutNode {
5014 form_node: xfa_layout_engine::form::FormNodeId(0),
5015 rect: xfa_layout_engine::types::Rect::new(10.0, 10.0, 100.0, 20.0),
5016 name: "name_field".to_string(),
5017 content: LayoutContent::Field {
5018 value: "John Doe".to_string(),
5019 field_kind: FieldKind::Text,
5020 font_size: 10.0,
5021 font_family: xfa_layout_engine::text::FontFamily::Serif,
5022 },
5023 children: vec![],
5024 style: Default::default(),
5025 display_items: vec![],
5026 save_items: vec![],
5027 };
5028 let s = overlay_str(&make_page(vec![node]));
5029 assert!(
5030 s.contains("(John Doe) Tj"),
5031 "text field must render its bound value: {s}"
5032 );
5033 }
5034
5035 #[test]
5036 fn numeric_field_formats_value_with_default_pattern() {
5037 let node = LayoutNode {
5039 form_node: xfa_layout_engine::form::FormNodeId(0),
5040 rect: xfa_layout_engine::types::Rect::new(10.0, 10.0, 80.0, 20.0),
5041 name: "amount".to_string(),
5042 content: LayoutContent::Field {
5043 value: "42.00000000".to_string(),
5044 field_kind: FieldKind::NumericEdit,
5045 font_size: 10.0,
5046 font_family: xfa_layout_engine::text::FontFamily::Serif,
5047 },
5048 children: vec![],
5049 style: Default::default(),
5050 display_items: vec![],
5051 save_items: vec![],
5052 };
5053 let s = overlay_str(&make_page(vec![node]));
5054 assert!(
5056 s.contains("(42) Tj"),
5057 "numeric field should render cleaned value: {s}"
5058 );
5059 }
5060
5061 #[test]
5062 fn date_field_renders_value_with_format_pattern() {
5063 let node = LayoutNode {
5066 form_node: xfa_layout_engine::form::FormNodeId(0),
5067 rect: xfa_layout_engine::types::Rect::new(10.0, 10.0, 100.0, 20.0),
5068 name: "date_field".to_string(),
5069 content: LayoutContent::Field {
5070 value: "2024-01-15".to_string(),
5071 field_kind: FieldKind::DateTimePicker,
5072 font_size: 10.0,
5073 font_family: xfa_layout_engine::text::FontFamily::Serif,
5074 },
5075 children: vec![],
5076 style: Default::default(),
5077 display_items: vec![],
5078 save_items: vec![],
5079 };
5080 let s = overlay_str(&make_page(vec![node]));
5081 assert!(
5082 s.contains("(2024-01-15) Tj"),
5083 "date field must render its value: {s}"
5084 );
5085 }
5086
5087 #[test]
5088 fn draw_line_span_gate_default_on_and_opt_out() {
5089 let line = DrawContent::Line {
5096 x1: 0.0,
5097 y1: 0.0,
5098 x2: 0.0,
5099 y2: 0.0,
5100 };
5101 let style = FormNodeStyle {
5102 border_width_pt: Some(6.01),
5103 border_color: Some((0, 0, 0)),
5104 ..Default::default()
5105 };
5106 let config = XfaRenderConfig::default();
5107
5108 std::env::remove_var("XFA_DRAW_LINE_SPAN");
5110 let mut ops_on = Vec::new();
5111 render_draw(&line, 30.0, 50.0, 200.0, 0.0, &style, &config, &mut ops_on);
5112 let s_on = String::from_utf8(ops_on).unwrap();
5113 assert!(s_on.contains("6.01 w\n"), "authored stroke weight:\n{s_on}");
5114 assert!(
5115 s_on.contains("30.00 50.00 m\n230.00 50.00 l\nS\n"),
5116 "horizontal grid rule must span the box (30 -> 230):\n{s_on}"
5117 );
5118
5119 std::env::set_var("XFA_DRAW_LINE_SPAN", "0");
5121 let mut ops_off = Vec::new();
5122 render_draw(&line, 30.0, 50.0, 200.0, 0.0, &style, &config, &mut ops_off);
5123 std::env::remove_var("XFA_DRAW_LINE_SPAN");
5124 let s_off = String::from_utf8(ops_off).unwrap();
5125 assert!(
5126 !s_off.contains(" w\n"),
5127 "opt-out emits no stroke weight:\n{s_off}"
5128 );
5129 assert!(
5130 s_off.contains("30.00 50.00 m\n30.00 50.00 l\nS\n"),
5131 "opt-out = legacy zero-length point:\n{s_off}"
5132 );
5133 }
5134
5135 fn config_with_border(border: bool) -> XfaRenderConfig {
5139 XfaRenderConfig {
5140 draw_borders: border,
5141 border_width: if border { 1.0 } else { 0.0 },
5142 ..XfaRenderConfig::default()
5143 }
5144 }
5145
5146 #[test]
5147 fn an_empty_signature_draws_the_border_its_template_declares() {
5148 let mut ops = Vec::new();
5153 render_signature(
5154 10.0,
5155 20.0,
5156 100.0,
5157 30.0,
5158 "",
5159 &FormNodeStyle::default(),
5160 &config_with_border(true),
5161 &mut ops,
5162 );
5163 let out = String::from_utf8_lossy(&ops);
5164 assert!(
5165 !ops.is_empty(),
5166 "an empty signature field with a border drew nothing"
5167 );
5168 assert!(
5169 out.contains(" RG"),
5170 "no stroke colour set, so no border is drawn: {out}"
5171 );
5172 assert!(
5173 out.trim_end().ends_with('S'),
5174 "the path is never stroked (no S operator): {out}"
5175 );
5176 }
5177
5178 #[test]
5179 fn without_a_declared_border_nothing_changes() {
5180 let mut ops = Vec::new();
5186 render_signature(
5187 10.0,
5188 20.0,
5189 100.0,
5190 30.0,
5191 "",
5192 &FormNodeStyle::default(),
5193 &config_with_border(false),
5194 &mut ops,
5195 );
5196 assert!(
5197 ops.is_empty(),
5198 "something was drawn while the template declares no border: {}",
5199 String::from_utf8_lossy(&ops)
5200 );
5201 }
5202
5203 #[test]
5204 fn a_filled_signature_still_draws_its_content() {
5205 let mut ops = Vec::new();
5206 render_signature(
5207 10.0,
5208 20.0,
5209 100.0,
5210 30.0,
5211 "J. de Winter",
5212 &FormNodeStyle::default(),
5213 &config_with_border(true),
5214 &mut ops,
5215 );
5216 assert!(!ops.is_empty(), "een ingevulde handtekening tekende niets");
5217 }
5218
5219 #[test]
5222 fn een_barcodeveld_tekent_balken_en_geen_tekst() {
5223 let mut ops = Vec::new();
5224 render_barcode(
5225 10.0,
5226 20.0,
5227 200.0,
5228 40.0,
5229 "ABC-123",
5230 &FormNodeStyle::default(),
5231 &XfaRenderConfig::default(),
5232 &mut ops,
5233 );
5234 let uit = String::from_utf8_lossy(&ops);
5235 assert!(!ops.is_empty(), "er is niets getekend");
5236 assert!(
5237 uit.contains(" re\n"),
5238 "geen rechthoeken; er staan geen balken: {uit}"
5239 );
5240 assert!(uit.trim_end().ends_with('f'), "het pad wordt niet gevuld");
5241 assert!(
5242 !uit.contains(" Tj"),
5243 "er wordt tekst getekend in plaats van een barcode"
5244 );
5245 }
5246
5247 #[test]
5248 fn een_onbekend_teken_tekent_niets() {
5249 let mut ops = Vec::new();
5252 render_barcode(
5253 10.0,
5254 20.0,
5255 200.0,
5256 40.0,
5257 "hallo!",
5258 &FormNodeStyle::default(),
5259 &XfaRenderConfig::default(),
5260 &mut ops,
5261 );
5262 assert!(
5263 ops.is_empty(),
5264 "er is toch iets getekend: {}",
5265 String::from_utf8_lossy(&ops)
5266 );
5267 }
5268
5269 #[test]
5270 fn een_te_smal_veld_tekent_niets() {
5271 let mut ops = Vec::new();
5275 render_barcode(
5276 10.0,
5277 20.0,
5278 2.0,
5279 40.0,
5280 "ABC-123",
5281 &FormNodeStyle::default(),
5282 &XfaRenderConfig::default(),
5283 &mut ops,
5284 );
5285 assert!(
5286 ops.is_empty(),
5287 "een onleesbaar smalle barcode werd toch getekend"
5288 );
5289 }
5290
5291 #[test]
5292 fn de_balken_passen_binnen_de_veldbreedte() {
5293 let mut ops = Vec::new();
5297 let (x, breedte) = (10.0_f64, 200.0_f64);
5298 render_barcode(
5299 x,
5300 20.0,
5301 breedte,
5302 40.0,
5303 "123",
5304 &FormNodeStyle::default(),
5305 &XfaRenderConfig::default(),
5306 &mut ops,
5307 );
5308 let uit = String::from_utf8_lossy(&ops);
5309 let mut rechtsterand = x;
5310 for regel in uit.lines().filter(|l| l.ends_with(" re")) {
5311 let d: Vec<f64> = regel
5312 .trim_end_matches(" re")
5313 .split(' ')
5314 .filter_map(|t| t.parse().ok())
5315 .collect();
5316 if d.len() == 4 {
5317 rechtsterand = rechtsterand.max(d[0] + d[2]);
5318 }
5319 }
5320 assert!(
5321 rechtsterand <= x + breedte + 0.5,
5322 "de barcode loopt tot {rechtsterand} en het veld eindigt op {}",
5323 x + breedte
5324 );
5325 }
5326}