1use crate::button::{button_kind, ButtonKind};
10use crate::choice::choice_kind;
11use crate::text::text_field_kind;
12use crate::tree::*;
13use std::io::Write as _;
14
15#[derive(Debug, Clone, Default)]
17pub struct DefaultAppearance {
18 pub font_name: Option<String>,
20 pub font_size: f32,
22 pub color: Vec<f32>,
24 pub color_op: Option<String>,
26}
27
28pub fn parse_da(da: &str) -> DefaultAppearance {
30 let mut result = DefaultAppearance::default();
31 let tokens: Vec<&str> = da.split_whitespace().collect();
32 let mut i = 0;
33 while i < tokens.len() {
34 match tokens[i] {
35 "Tf" if i >= 2 => {
36 result.font_size = tokens[i - 1].parse().unwrap_or(0.0);
37 let name = tokens[i - 2];
38 result.font_name = Some(name.strip_prefix('/').unwrap_or(name).to_string());
39 }
40 "g" if i >= 1 => {
41 if let Ok(g) = tokens[i - 1].parse::<f32>() {
42 result.color = vec![g];
43 result.color_op = Some("g".into());
44 }
45 }
46 "rg" if i >= 3 => {
47 if let (Ok(r), Ok(g), Ok(b)) = (
48 tokens[i - 3].parse::<f32>(),
49 tokens[i - 2].parse::<f32>(),
50 tokens[i - 1].parse::<f32>(),
51 ) {
52 result.color = vec![r, g, b];
53 result.color_op = Some("rg".into());
54 }
55 }
56 "k" if i >= 4 => {
57 if let (Ok(c), Ok(m), Ok(y), Ok(k)) = (
58 tokens[i - 4].parse::<f32>(),
59 tokens[i - 3].parse::<f32>(),
60 tokens[i - 2].parse::<f32>(),
61 tokens[i - 1].parse::<f32>(),
62 ) {
63 result.color = vec![c, m, y, k];
64 result.color_op = Some("k".into());
65 }
66 }
67 _ => {}
68 }
69 i += 1;
70 }
71 result
72}
73
74pub fn generate_text_appearance(
76 tree: &FieldTree,
77 id: FieldId,
78 da: &DefaultAppearance,
79 text: &str,
80) -> Vec<u8> {
81 let node = tree.get(id);
82 let rect = node.rect.unwrap_or([0.0, 0.0, 100.0, 20.0]);
83 let width = rect[2] - rect[0];
84 let height = rect[3] - rect[1];
85 let quadding = tree.effective_quadding(id);
86 let flags = tree.effective_flags(id);
87 let font_name = da.font_name.as_deref().unwrap_or("Helv");
88 let font_size = if da.font_size > 0.0 {
89 da.font_size
90 } else {
91 (height - 2.0).clamp(4.0, 24.0)
92 };
93 let kind = text_field_kind(flags);
94 let mut buf = Vec::new();
95 let bw = node.border_style.as_ref().map(|b| b.width).unwrap_or(1.0);
96
97 if let Some(ref mk) = node.mk {
98 if let Some(ref bg) = mk.background_color {
99 write_color(&mut buf, bg, false);
100 let _ = writeln!(buf, "{} {} {} {} re f", 0.0, 0.0, width, height);
101 }
102 if let Some(ref bc) = mk.border_color {
103 write_color(&mut buf, bc, true);
104 let _ = writeln!(
105 buf,
106 "{} w {} {} {} {} re S",
107 bw,
108 bw / 2.0,
109 bw / 2.0,
110 width - bw,
111 height - bw
112 );
113 }
114 }
115
116 let margin = bw + 1.0;
117 let _ = writeln!(
118 buf,
119 "{} {} {} {} re W n",
120 margin,
121 margin,
122 width - margin * 2.0,
123 height - margin * 2.0
124 );
125 buf.extend_from_slice(b"BT\n");
126 if !da.color.is_empty() {
127 write_color(&mut buf, &da.color, false);
128 }
129 let _ = writeln!(buf, "/{} {} Tf", font_name, font_size);
130
131 let display_text = if flags.password() {
132 "*".repeat(text.len())
133 } else {
134 text.to_string()
135 };
136
137 match kind {
138 crate::text::TextFieldKind::Comb => {
139 if let Some(max_len) = tree.effective_max_len(id) {
140 let cell_w = width / max_len as f32;
141 for (i, ch) in display_text.chars().take(max_len as usize).enumerate() {
142 let x = margin + cell_w * i as f32 + cell_w * 0.25;
143 let y = margin + (height - margin * 2.0 - font_size) / 2.0;
144 let _ = writeln!(buf, "{} {} Td ({}) Tj", x, y, escape_pdf_string_char(ch));
145 }
146 }
147 }
148 crate::text::TextFieldKind::Multiline => {
149 let leading = font_size * 1.2;
150 let _ = writeln!(buf, "{} TL", leading);
151 let _ = writeln!(buf, "{} {} Td", margin, height - margin - font_size);
152 for (i, line) in display_text.lines().enumerate() {
153 if i > 0 {
154 buf.extend_from_slice(b"T*\n");
155 }
156 let _ = writeln!(buf, "({}) Tj", escape_pdf_string(line));
157 }
158 }
159 _ => {
160 let approx_w = display_text.len() as f32 * font_size * 0.5;
161 let x = match quadding {
162 Quadding::Center => margin + (width - margin * 2.0 - approx_w) / 2.0,
163 Quadding::Right => width - margin - approx_w,
164 Quadding::Left => margin,
165 }
166 .max(margin);
167 let y = margin + (height - margin * 2.0 - font_size) / 2.0;
168 let _ = writeln!(buf, "{} {} Td", x, y);
169 let _ = writeln!(buf, "({}) Tj", escape_pdf_string(&display_text));
170 }
171 }
172 buf.extend_from_slice(b"ET\n");
173 buf
174}
175
176pub fn generate_checkbox_appearance(tree: &FieldTree, id: FieldId, checked: bool) -> Vec<u8> {
178 let node = tree.get(id);
179 let rect = node.rect.unwrap_or([0.0, 0.0, 12.0, 12.0]);
180 let (w, h) = (rect[2] - rect[0], rect[3] - rect[1]);
181 let mut buf = Vec::new();
182 let _ = writeln!(buf, "1 g 0 0 {} {} re f", w, h);
183 let _ = writeln!(buf, "0 g 0.5 w 0 0 {} {} re S", w, h);
184 if checked {
185 let m = w * 0.15;
186 let _ = writeln!(
187 buf,
188 "0 g 1.5 w {} {} m {} {} l {} {} l S",
189 m,
190 h * 0.5,
191 w * 0.4,
192 m,
193 w - m,
194 h - m
195 );
196 }
197 buf
198}
199
200pub fn generate_radio_appearance(tree: &FieldTree, id: FieldId, selected: bool) -> Vec<u8> {
202 let node = tree.get(id);
203 let rect = node.rect.unwrap_or([0.0, 0.0, 12.0, 12.0]);
204 let size = (rect[2] - rect[0]).min(rect[3] - rect[1]);
205 let (cx, cy, r) = (size / 2.0, size / 2.0, size / 2.0 - 1.0);
206 let k = 0.5523_f32;
207 let mut buf = Vec::new();
208 write_circle(&mut buf, cx, cy, r, k, "1 g", "f");
209 write_circle(&mut buf, cx, cy, r, k, "0 g 0.5 w", "S");
210 if selected {
211 write_circle(&mut buf, cx, cy, r * 0.4, k, "0 g", "f");
212 }
213 buf
214}
215
216fn write_circle(buf: &mut Vec<u8>, cx: f32, cy: f32, r: f32, k: f32, prefix: &str, op: &str) {
217 let kr = k * r;
218 let _ = writeln!(
219 buf,
220 "{prefix} {cx} {bot} m {r1} {bot} {right} {b1} {right} {cy} c {right} {t1} {r1} {top} {cx} {top} c {l1} {top} {left} {t1} {left} {cy} c {left} {b1} {l1} {bot} {cx} {bot} c {op}",
221 prefix = prefix,
222 op = op,
223 cx = cx,
224 cy = cy,
225 bot = cy - r,
226 top = cy + r,
227 left = cx - r,
228 right = cx + r,
229 r1 = cx + kr,
230 l1 = cx - kr,
231 t1 = cy + kr,
232 b1 = cy - kr,
233 );
234}
235
236pub fn generate_choice_appearance(
238 tree: &FieldTree,
239 id: FieldId,
240 da: &DefaultAppearance,
241) -> Vec<u8> {
242 let node = tree.get(id);
243 let rect = node.rect.unwrap_or([0.0, 0.0, 150.0, 20.0]);
244 let (width, height) = (rect[2] - rect[0], rect[3] - rect[1]);
245 let flags = tree.effective_flags(id);
246 let kind = choice_kind(flags);
247 let font_name = da.font_name.as_deref().unwrap_or("Helv");
248 let font_size = if da.font_size > 0.0 {
249 da.font_size
250 } else {
251 (height - 4.0).clamp(4.0, 12.0)
252 };
253 let mut buf = Vec::new();
254 let _ = writeln!(buf, "1 g 0 0 {} {} re f", width, height);
255 let _ = writeln!(buf, "0 g 0.5 w 0 0 {} {} re S", width, height);
256 match kind {
257 crate::choice::ChoiceKind::ComboBox | crate::choice::ChoiceKind::EditableCombo => {
258 let selected = crate::choice::get_selection(tree, id);
259 let text = selected.first().map(|s| s.as_str()).unwrap_or("");
260 let arrow_w = height.min(20.0);
261 let _ = writeln!(
262 buf,
263 "0.9 g {} 0 {} {} re f",
264 width - arrow_w,
265 arrow_w,
266 height
267 );
268 let (ax, aw) = (width - arrow_w / 2.0, arrow_w * 0.25);
269 let _ = writeln!(
270 buf,
271 "0 g {} {} m {} {} l {} {} l f",
272 ax - aw,
273 height * 0.65,
274 ax + aw,
275 height * 0.65,
276 ax,
277 height * 0.35
278 );
279 buf.extend_from_slice(b"BT\n");
280 write_color(&mut buf, &da.color, false);
281 let _ = writeln!(buf, "/{} {} Tf", font_name, font_size);
282 let _ = writeln!(buf, "2 {} Td", (height - font_size) / 2.0);
283 let _ = writeln!(buf, "({}) Tj", escape_pdf_string(text));
284 buf.extend_from_slice(b"ET\n");
285 }
286 _ => {
287 let leading = font_size * 1.2;
290 let selected = crate::choice::get_selection(tree, id);
291 let top_idx = node.top_index.unwrap_or(0) as usize;
292 let _ = writeln!(buf, "q 1 1 {} {} re W n", width - 2.0, height - 2.0);
293 for (i, opt) in node.options.iter().skip(top_idx).enumerate() {
294 let row_top = height - 2.0 - i as f32 * leading;
295 if row_top <= 0.0 {
296 break;
297 }
298 if selected.contains(&opt.export) || selected.contains(&opt.display) {
299 let _ = writeln!(
300 buf,
301 "0.6 0.75 0.85 rg 1 {} {} {} re f",
302 row_top - leading,
303 width - 2.0,
304 leading
305 );
306 }
307 buf.extend_from_slice(b"BT\n");
308 write_color(&mut buf, &da.color, false);
309 let _ = writeln!(buf, "/{} {} Tf", font_name, font_size);
310 let _ = writeln!(
311 buf,
312 "3 {} Td",
313 row_top - leading + (leading - font_size) / 2.0 + font_size * 0.2
314 );
315 let _ = writeln!(buf, "({}) Tj", escape_pdf_string(&opt.display));
316 buf.extend_from_slice(b"ET\n");
317 }
318 buf.extend_from_slice(b"Q\n");
319 }
320 }
321 buf
322}
323
324pub fn generate_appearance(tree: &FieldTree, id: FieldId) -> Option<Vec<u8>> {
326 let ft = tree.effective_field_type(id)?;
327 let da_str = tree.effective_da(id).unwrap_or("0 g /Helv 12 Tf");
328 let da = parse_da(da_str);
329 match ft {
330 FieldType::Text => {
331 let text = crate::text::get_text_value(tree, id).unwrap_or_default();
332 Some(generate_text_appearance(tree, id, &da, &text))
333 }
334 FieldType::Button => {
335 let flags = tree.effective_flags(id);
336 match button_kind(flags) {
337 ButtonKind::Checkbox => Some(generate_checkbox_appearance(
338 tree,
339 id,
340 crate::button::is_checked(tree, id),
341 )),
342 ButtonKind::Radio => Some(generate_radio_appearance(
343 tree,
344 id,
345 crate::button::is_checked(tree, id),
346 )),
347 ButtonKind::PushButton => {
348 let caption = tree
349 .get(id)
350 .mk
351 .as_ref()
352 .and_then(|m| m.caption.as_deref())
353 .unwrap_or("");
354 Some(generate_text_appearance(tree, id, &da, caption))
355 }
356 }
357 }
358 FieldType::Choice => Some(generate_choice_appearance(tree, id, &da)),
359 FieldType::Signature => None,
360 }
361}
362
363fn write_color(buf: &mut Vec<u8>, color: &[f32], stroke: bool) {
364 let op = match (color.len(), stroke) {
365 (1, false) => "g",
366 (1, true) => "G",
367 (3, false) => "rg",
368 (3, true) => "RG",
369 (4, false) => "k",
370 (4, true) => "K",
371 _ => return,
372 };
373 for c in color {
374 let _ = write!(buf, "{} ", c);
375 }
376 let _ = writeln!(buf, "{}", op);
377}
378
379fn escape_pdf_string(s: &str) -> String {
380 s.chars()
381 .map(|ch| match ch {
382 '(' => "\\(".to_string(),
383 ')' => "\\)".to_string(),
384 '\\' => "\\\\".to_string(),
385 _ => ch.to_string(),
386 })
387 .collect()
388}
389
390fn escape_pdf_string_char(ch: char) -> String {
391 match ch {
392 '(' => "\\(".into(),
393 ')' => "\\)".into(),
394 '\\' => "\\\\".into(),
395 _ => ch.to_string(),
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use crate::flags::FieldFlags;
403
404 #[test]
405 fn test_parse_da_simple() {
406 let da = parse_da("0 g /Helv 12 Tf");
407 assert_eq!(da.font_name.as_deref(), Some("Helv"));
408 assert_eq!(da.font_size, 12.0);
409 assert_eq!(da.color, vec![0.0]);
410 }
411 #[test]
412 fn test_parse_da_rgb() {
413 let da = parse_da("0 0 1 rg /Cour 10 Tf");
414 assert_eq!(da.font_name.as_deref(), Some("Cour"));
415 assert_eq!(da.color, vec![0.0, 0.0, 1.0]);
416 }
417 #[test]
418 fn test_escape() {
419 assert_eq!(escape_pdf_string("a(b)"), "a\\(b\\)");
420 }
421 #[test]
422 fn test_checkbox_appearance() {
423 let mut tree = FieldTree::new();
424 let id = tree.alloc(FieldNode {
425 partial_name: "cb".into(),
426 alternate_name: None,
427 mapping_name: None,
428 field_type: Some(FieldType::Button),
429 flags: FieldFlags::empty(),
430 value: None,
431 default_value: None,
432 default_appearance: None,
433 quadding: None,
434 max_len: None,
435 options: vec![],
436 top_index: None,
437 rect: Some([0.0, 0.0, 12.0, 12.0]),
438 appearance_state: None,
439 on_state: None,
440 page_index: None,
441 parent: None,
442 children: vec![],
443 object_id: None,
444 has_actions: false,
445 mk: None,
446 border_style: None,
447 });
448 assert!(
449 generate_checkbox_appearance(&tree, id, true).len()
450 > generate_checkbox_appearance(&tree, id, false).len()
451 );
452 }
453}