1use crate::error::Result;
13use std::collections::HashMap;
14use std::io::Write as _;
15use xfa_layout_engine::form::{FieldKind, FormNodeId};
16use xfa_layout_engine::layout::{LayoutContent, LayoutDom, LayoutNode};
17
18#[derive(Debug, Clone)]
20pub struct AppearanceConfig {
21 pub default_font: String,
23 pub default_font_size: f64,
25 pub border_width: f64,
27 pub border_color: [f64; 3],
29 pub background_color: Option<[f64; 3]>,
31 pub text_color: [f64; 3],
33 pub text_padding: f64,
35}
36
37impl Default for AppearanceConfig {
38 fn default() -> Self {
39 Self {
40 default_font: "Helvetica".to_string(),
41 default_font_size: 10.0,
42 border_width: 0.5,
43 border_color: [0.0, 0.0, 0.0],
44 background_color: Some([1.0, 1.0, 1.0]),
45 text_color: [0.0, 0.0, 0.0],
46 text_padding: 2.0,
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
53pub struct AppearanceStream {
54 pub content: Vec<u8>,
56 pub bbox: [f64; 4],
58 pub font_resources: Vec<(String, String)>,
60}
61
62pub struct AppearanceCache {
64 cache: HashMap<(usize, u64), AppearanceStream>,
65}
66
67impl AppearanceCache {
68 pub fn new() -> Self {
70 Self {
71 cache: HashMap::new(),
72 }
73 }
74 pub fn get_or_generate(
76 &mut self,
77 node_id: FormNodeId,
78 value: &str,
79 width: f64,
80 height: f64,
81 config: &AppearanceConfig,
82 ) -> &AppearanceStream {
83 let key = (node_id.0, simple_hash(value));
84 self.cache
85 .entry(key)
86 .or_insert_with(|| field_appearance(value, width, height, config))
87 }
88 pub fn invalidate(&mut self, node_id: FormNodeId) {
90 self.cache.retain(|(id, _), _| *id != node_id.0);
91 }
92 pub fn clear(&mut self) {
94 self.cache.clear();
95 }
96}
97
98impl Default for AppearanceCache {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104pub fn generate_appearances(
106 layout: &LayoutDom,
107 config: &AppearanceConfig,
108) -> Result<Vec<PageAppearances>> {
109 let mut pages = Vec::new();
110 for page in &layout.pages {
111 let mut entries = Vec::new();
112 collect_appearances(&page.nodes, 0.0, 0.0, config, &mut entries);
113 pages.push(PageAppearances {
114 width: page.width,
115 height: page.height,
116 entries,
117 });
118 }
119 Ok(pages)
120}
121#[derive(Debug)]
124pub struct PageAppearances {
125 pub width: f64,
127 pub height: f64,
129 pub entries: Vec<AppearanceEntry>,
131}
132#[derive(Debug)]
135pub struct AppearanceEntry {
136 pub name: String,
138 pub abs_x: f64,
140 pub abs_y: f64,
142 pub appearance: AppearanceStream,
144}
145
146fn collect_appearances(
147 nodes: &[LayoutNode],
148 parent_x: f64,
149 parent_y: f64,
150 config: &AppearanceConfig,
151 result: &mut Vec<AppearanceEntry>,
152) {
153 for node in nodes {
154 let abs_x = node.rect.x + parent_x;
155 let abs_y = node.rect.y + parent_y;
156 let w = node.rect.width;
157 let h = node.rect.height;
158
159 let ap = match &node.content {
160 LayoutContent::Field {
161 value,
162 field_kind: FieldKind::Checkbox | FieldKind::Radio,
163 ..
164 } => {
165 let checked = checkbox_raw_value_is_checked(value);
166 Some(checkbox_appearance(checked, w, h))
167 }
168 LayoutContent::Field { value, .. } => Some(field_appearance(value, w, h, config)),
169 LayoutContent::Text(text) => Some(draw_appearance(text, w, h, config)),
170 LayoutContent::WrappedText {
171 lines, font_size, ..
172 } => Some(multiline_appearance(
173 lines,
174 *font_size,
175 font_size * 1.2,
176 w,
177 h,
178 config,
179 )),
180 LayoutContent::Image { .. } => None,
181 LayoutContent::Draw(_) => None,
182 LayoutContent::None => None,
183 };
184 if let Some(ap) = ap {
185 result.push(AppearanceEntry {
186 name: node.name.clone(),
187 abs_x,
188 abs_y,
189 appearance: ap,
190 });
191 }
192 if !node.children.is_empty() {
193 collect_appearances(&node.children, abs_x, abs_y, config, result);
194 }
195 }
196}
197pub fn field_appearance(
199 value: &str,
200 width: f64,
201 height: f64,
202 config: &AppearanceConfig,
203) -> AppearanceStream {
204 let mut ops = Vec::new();
205 if let Some(bg) = &config.background_color {
206 let _ = write!(
207 ops,
208 "{:.3} {:.3} {:.3} rg\n{:.2} {:.2} {:.2} {:.2} re\nf\n",
209 bg[0], bg[1], bg[2], 0.0, 0.0, width, height
210 );
211 }
212 if config.border_width > 0.0 {
213 let _ = write!(
214 ops,
215 "{:.2} w\n{:.3} {:.3} {:.3} RG\n{:.2} {:.2} {:.2} {:.2} re\nS\n",
216 config.border_width,
217 config.border_color[0],
218 config.border_color[1],
219 config.border_color[2],
220 0.0,
221 0.0,
222 width,
223 height
224 );
225 }
226 if !value.is_empty() {
227 let fs = config.default_font_size;
228 let p = config.text_padding;
229 let _ = write!(
230 ops,
231 "BT\n{:.3} {:.3} {:.3} rg\n/F1 {:.1} Tf\n{:.2} {:.2} Td\n({}) Tj\nET\n",
232 config.text_color[0],
233 config.text_color[1],
234 config.text_color[2],
235 fs,
236 p,
237 height - fs - p,
238 pdf_escape(value)
239 );
240 AppearanceStream {
241 content: ops,
242 bbox: [0.0, 0.0, width, height],
243 font_resources: vec![("F1".to_string(), config.default_font.clone())],
244 }
245 } else {
246 AppearanceStream {
247 content: ops,
248 bbox: [0.0, 0.0, width, height],
249 font_resources: vec![],
250 }
251 }
252}
253pub fn draw_appearance(
255 text: &str,
256 width: f64,
257 height: f64,
258 config: &AppearanceConfig,
259) -> AppearanceStream {
260 let mut ops = Vec::new();
261 if let Some(bg) = &config.background_color {
262 let _ = write!(
263 ops,
264 "{:.3} {:.3} {:.3} rg\n{:.2} {:.2} {:.2} {:.2} re\nf\n",
265 bg[0], bg[1], bg[2], 0.0, 0.0, width, height
266 );
267 }
268 if !text.is_empty() {
269 let fs = config.default_font_size;
270 let p = config.text_padding;
271 let _ = write!(
272 ops,
273 "BT\n{:.3} {:.3} {:.3} rg\n/F1 {:.1} Tf\n{:.2} {:.2} Td\n({}) Tj\nET\n",
274 config.text_color[0],
275 config.text_color[1],
276 config.text_color[2],
277 fs,
278 p,
279 height - fs - p,
280 pdf_escape(text)
281 );
282 AppearanceStream {
283 content: ops,
284 bbox: [0.0, 0.0, width, height],
285 font_resources: vec![("F1".to_string(), config.default_font.clone())],
286 }
287 } else {
288 AppearanceStream {
289 content: ops,
290 bbox: [0.0, 0.0, width, height],
291 font_resources: vec![],
292 }
293 }
294}
295pub fn multiline_appearance(
297 lines: &[String],
298 font_size: f64,
299 line_height: f64,
300 width: f64,
301 height: f64,
302 config: &AppearanceConfig,
303) -> AppearanceStream {
304 let mut ops = Vec::new();
305 if let Some(bg) = &config.background_color {
306 let _ = write!(
307 ops,
308 "{:.3} {:.3} {:.3} rg\n{:.2} {:.2} {:.2} {:.2} re\nf\n",
309 bg[0], bg[1], bg[2], 0.0, 0.0, width, height
310 );
311 }
312 if config.border_width > 0.0 {
313 let _ = write!(
314 ops,
315 "{:.2} w\n{:.3} {:.3} {:.3} RG\n{:.2} {:.2} {:.2} {:.2} re\nS\n",
316 config.border_width,
317 config.border_color[0],
318 config.border_color[1],
319 config.border_color[2],
320 0.0,
321 0.0,
322 width,
323 height
324 );
325 }
326 if !lines.is_empty() {
327 let p = config.text_padding;
328 let _ = write!(
329 ops,
330 "BT\n{:.3} {:.3} {:.3} rg\n/F1 {:.1} Tf\n",
331 config.text_color[0], config.text_color[1], config.text_color[2], font_size
332 );
333 let start_y = height - font_size - p;
334 for (i, line) in lines.iter().enumerate() {
335 let ay = start_y - (i as f64 * line_height);
336 if ay < 0.0 {
337 break;
338 }
339 if i == 0 {
340 let _ = writeln!(ops, "{:.2} {:.2} Td", p, ay);
341 } else {
342 let _ = writeln!(ops, "{:.2} {:.2} Td", 0.0, -line_height);
343 }
344 let _ = writeln!(ops, "({}) Tj", pdf_escape(line));
345 }
346 ops.extend_from_slice(b"ET\n");
347 AppearanceStream {
348 content: ops,
349 bbox: [0.0, 0.0, width, height],
350 font_resources: vec![("F1".to_string(), config.default_font.clone())],
351 }
352 } else {
353 AppearanceStream {
354 content: ops,
355 bbox: [0.0, 0.0, width, height],
356 font_resources: vec![],
357 }
358 }
359}
360pub fn checkbox_raw_value_is_checked(raw_value: &str) -> bool {
372 !raw_value.is_empty() && raw_value != "0"
373}
374
375pub fn checkbox_appearance(checked: bool, width: f64, height: f64) -> AppearanceStream {
377 let mut ops = Vec::new();
378 let size = width.min(height);
379 let _ = write!(
380 ops,
381 "0.50 w\n0.000 0.000 0.000 RG\n0.00 0.00 {:.2} {:.2} re\nS\n",
382 size, size
383 );
384 if checked {
385 let pad = size * 0.2;
386 let _ = write!(ops,
387 "1.50 w\n0.000 0.000 0.000 RG\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\n",
388 pad, pad, size - pad, size - pad, size - pad, pad, pad, size - pad);
389 }
390 AppearanceStream {
391 content: ops,
392 bbox: [0.0, 0.0, size, size],
393 font_resources: vec![],
394 }
395}
396
397pub fn format_value(value: &str, pattern: Option<&str>) -> String {
406 let Some(pattern) = pattern else {
407 return value.to_string();
408 };
409 if pattern.starts_with("num{") && pattern.ends_with('}') {
410 let inner = &pattern[4..pattern.len() - 1];
411 if let Ok(num) = value.parse::<f64>() {
412 format_numeric(num, inner)
413 } else {
414 value.to_string()
415 }
416 } else {
417 value.to_string()
418 }
419}
420
421pub fn format_numeric_default(value: &str) -> String {
426 let trimmed = value.trim();
427 let Ok(num) = trimmed.parse::<f64>() else {
428 return value.to_string();
429 };
430 if num.fract() == 0.0 {
431 format!("{}", num as i64)
433 } else {
434 let s = format!("{}", num);
436 s.trim_end_matches('0').trim_end_matches('.').to_string()
437 }
438}
439
440fn format_numeric(num: f64, pattern: &str) -> String {
442 let is_negative = num < 0.0;
443 let abs_num = num.abs();
444
445 let (int_pat, dec_pat) = match pattern.find('.') {
447 Some(pos) => (&pattern[..pos], Some(&pattern[pos + 1..])),
448 None => (pattern, None),
449 };
450
451 let decimal_places = dec_pat
453 .map(|d| d.chars().filter(|c| *c == '9' || *c == 'z').count())
454 .unwrap_or(0);
455
456 let factor = 10f64.powi(decimal_places as i32);
458 let rounded = (abs_num * factor).round() / factor;
459
460 let int_part = rounded.trunc() as u64;
462 let frac_part = ((rounded - rounded.trunc()) * factor).round() as u64;
463
464 let int_str = int_part.to_string();
466
467 let pat_digit_slots: Vec<char> = int_pat.chars().filter(|c| *c == 'z' || *c == '9').collect();
469 let num_slots = pat_digit_slots.len();
470
471 let padded_len = num_slots.max(int_str.len());
473 let mut digits = vec![0u8; padded_len];
474 for (i, b) in int_str.bytes().rev().enumerate() {
475 digits[padded_len - 1 - i] = b - b'0';
476 }
477
478 let mut int_result = String::new();
480 let mut seen_significant = false;
481
482 for d in digits.iter().take(padded_len.saturating_sub(num_slots)) {
484 int_result.push((b'0' + d) as char);
485 seen_significant = true;
486 }
487
488 let mut di = padded_len.saturating_sub(num_slots);
489 for ch in int_pat.chars() {
490 match ch {
491 'z' => {
492 let d = digits[di];
493 di += 1;
494 if d != 0 || seen_significant {
495 int_result.push((b'0' + d) as char);
496 seen_significant = true;
497 }
498 }
499 '9' => {
500 let d = digits[di];
501 di += 1;
502 int_result.push((b'0' + d) as char);
503 seen_significant = true;
504 }
505 ',' => {
506 if seen_significant {
509 int_result.push(',');
510 }
511 }
512 _ => int_result.push(ch),
513 }
514 }
515
516 let result_dec = if let Some(dp) = dec_pat {
518 let frac_str = format!("{:0>width$}", frac_part, width = decimal_places);
519 let frac_bytes: Vec<u8> = frac_str.bytes().map(|b| b - b'0').collect();
520 let mut dec_result = String::new();
521 let mut fi = 0;
522 for ch in dp.chars() {
523 match ch {
524 '9' | 'z' => {
525 if fi < frac_bytes.len() {
526 dec_result.push((b'0' + frac_bytes[fi]) as char);
527 fi += 1;
528 } else {
529 dec_result.push('0');
530 }
531 }
532 _ => dec_result.push(ch),
533 }
534 }
535 Some(dec_result)
536 } else {
537 None
538 };
539
540 let mut result = String::new();
542 if is_negative {
543 result.push('-');
544 }
545 if int_result.is_empty() {
546 result.push('0');
547 } else {
548 result.push_str(&int_result);
549 }
550 if let Some(dec) = result_dec {
551 result.push('.');
552 result.push_str(&dec);
553 }
554 result
555}
556
557fn pdf_escape(s: &str) -> String {
558 let mut r = String::with_capacity(s.len());
559 for c in s.chars() {
560 match c {
561 '(' => r.push_str("\\("),
562 ')' => r.push_str("\\)"),
563 '\\' => r.push_str("\\\\"),
564 _ => r.push(c),
565 }
566 }
567 r
568}
569
570fn simple_hash(s: &str) -> u64 {
571 let mut h: u64 = 5381;
572 for b in s.bytes() {
573 h = h.wrapping_mul(33).wrapping_add(b as u64);
574 }
575 h
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 #[test]
583 fn field_appearance_basic() {
584 let config = AppearanceConfig::default();
585 let ap = field_appearance("Hello", 100.0, 20.0, &config);
586 let content = String::from_utf8_lossy(&ap.content);
587 assert!(content.contains("(Hello) Tj"));
588 }
589
590 #[test]
591 fn field_appearance_empty() {
592 let config = AppearanceConfig::default();
593 let ap = field_appearance("", 100.0, 20.0, &config);
594 assert!(!String::from_utf8_lossy(&ap.content).contains("BT"));
595 }
596
597 #[test]
598 fn cache_hit() {
599 let mut cache = AppearanceCache::new();
600 let config = AppearanceConfig::default();
601 let _ = cache.get_or_generate(FormNodeId(0), "Hello", 100.0, 20.0, &config);
602 let _ = cache.get_or_generate(FormNodeId(0), "Hello", 100.0, 20.0, &config);
603 assert_eq!(cache.cache.len(), 1);
604 }
605
606 #[test]
607 fn cache_invalidate() {
608 let mut cache = AppearanceCache::new();
609 let config = AppearanceConfig::default();
610 let _ = cache.get_or_generate(FormNodeId(0), "A", 100.0, 20.0, &config);
611 let _ = cache.get_or_generate(FormNodeId(1), "B", 100.0, 20.0, &config);
612 cache.invalidate(FormNodeId(0));
613 assert_eq!(cache.cache.len(), 1);
614 }
615
616 #[test]
617 fn format_value_numeric() {
618 assert_eq!(format_value("42.5", Some("num{zzz.99}")), "42.50");
619 assert_eq!(format_value("hello", None), "hello");
620 assert_eq!(format_value("1.00000000", Some("num{z,zzz}")), "1");
622 assert_eq!(format_value("2.00000000", Some("num{z,zzz}")), "2");
623 assert_eq!(format_value("1234", Some("num{z,zzz}")), "1,234");
624 assert_eq!(format_value("0", Some("num{z,zzz}")), "0");
625 assert_eq!(format_value("3.14159", Some("num{z.99}")), "3.14");
627 assert_eq!(format_value("0.5", Some("num{z.99}")), "0.50");
628 assert_eq!(format_value("5", Some("num{999}")), "005");
630 assert_eq!(format_value("42", Some("num{999}")), "042");
631 assert_eq!(format_value("-7.5", Some("num{z.99}")), "-7.50");
633 assert_eq!(format_value("abc", Some("num{z,zzz}")), "abc");
635 }
636
637 #[test]
638 fn checkbox_checked() {
639 let ap = checkbox_appearance(true, 12.0, 12.0);
640 let content = String::from_utf8_lossy(&ap.content);
641 assert!(content.contains("re\nS"));
642 assert!(content.contains("m\n"));
643 }
644}