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