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