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;
288 let selected = crate::choice::get_selection(tree, id);
289 let top_idx = node.top_index.unwrap_or(0) as usize;
290 let visible = (height / leading).floor() as usize;
291 buf.extend_from_slice(b"BT\n");
292 write_color(&mut buf, &da.color, false);
293 let _ = writeln!(buf, "/{} {} Tf", font_name, font_size);
294 let _ = writeln!(buf, "{} TL", leading);
295 let y_start = height - 2.0 - font_size;
296 let _ = writeln!(buf, "2 {} Td", y_start);
297 for (i, opt) in node.options.iter().skip(top_idx).take(visible).enumerate() {
298 if selected.contains(&opt.export) || selected.contains(&opt.display) {
299 buf.extend_from_slice(b"ET\n");
300 let _ = writeln!(
301 buf,
302 "0.6 0.75 0.95 rg 0 {} {} {} re f",
303 y_start - i as f32 * leading - 1.0,
304 width,
305 leading
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!(buf, "2 {} Td", y_start - i as f32 * leading);
311 }
312 if i > 0 {
313 buf.extend_from_slice(b"T*\n");
314 }
315 let _ = writeln!(buf, "({}) Tj", escape_pdf_string(&opt.display));
316 }
317 buf.extend_from_slice(b"ET\n");
318 }
319 }
320 buf
321}
322
323pub fn generate_appearance(tree: &FieldTree, id: FieldId) -> Option<Vec<u8>> {
325 let ft = tree.effective_field_type(id)?;
326 let da_str = tree.effective_da(id).unwrap_or("0 g /Helv 12 Tf");
327 let da = parse_da(da_str);
328 match ft {
329 FieldType::Text => {
330 let text = crate::text::get_text_value(tree, id).unwrap_or_default();
331 Some(generate_text_appearance(tree, id, &da, &text))
332 }
333 FieldType::Button => {
334 let flags = tree.effective_flags(id);
335 match button_kind(flags) {
336 ButtonKind::Checkbox => Some(generate_checkbox_appearance(
337 tree,
338 id,
339 crate::button::is_checked(tree, id),
340 )),
341 ButtonKind::Radio => Some(generate_radio_appearance(
342 tree,
343 id,
344 crate::button::is_checked(tree, id),
345 )),
346 ButtonKind::PushButton => {
347 let caption = tree
348 .get(id)
349 .mk
350 .as_ref()
351 .and_then(|m| m.caption.as_deref())
352 .unwrap_or("");
353 Some(generate_text_appearance(tree, id, &da, caption))
354 }
355 }
356 }
357 FieldType::Choice => Some(generate_choice_appearance(tree, id, &da)),
358 FieldType::Signature => None,
359 }
360}
361
362fn write_color(buf: &mut Vec<u8>, color: &[f32], stroke: bool) {
363 let op = match (color.len(), stroke) {
364 (1, false) => "g",
365 (1, true) => "G",
366 (3, false) => "rg",
367 (3, true) => "RG",
368 (4, false) => "k",
369 (4, true) => "K",
370 _ => return,
371 };
372 for c in color {
373 let _ = write!(buf, "{} ", c);
374 }
375 let _ = writeln!(buf, "{}", op);
376}
377
378fn escape_pdf_string(s: &str) -> String {
379 s.chars()
380 .map(|ch| match ch {
381 '(' => "\\(".to_string(),
382 ')' => "\\)".to_string(),
383 '\\' => "\\\\".to_string(),
384 _ => ch.to_string(),
385 })
386 .collect()
387}
388
389fn escape_pdf_string_char(ch: char) -> String {
390 match ch {
391 '(' => "\\(".into(),
392 ')' => "\\)".into(),
393 '\\' => "\\\\".into(),
394 _ => ch.to_string(),
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use crate::flags::FieldFlags;
402
403 #[test]
404 fn test_parse_da_simple() {
405 let da = parse_da("0 g /Helv 12 Tf");
406 assert_eq!(da.font_name.as_deref(), Some("Helv"));
407 assert_eq!(da.font_size, 12.0);
408 assert_eq!(da.color, vec![0.0]);
409 }
410 #[test]
411 fn test_parse_da_rgb() {
412 let da = parse_da("0 0 1 rg /Cour 10 Tf");
413 assert_eq!(da.font_name.as_deref(), Some("Cour"));
414 assert_eq!(da.color, vec![0.0, 0.0, 1.0]);
415 }
416 #[test]
417 fn test_escape() {
418 assert_eq!(escape_pdf_string("a(b)"), "a\\(b\\)");
419 }
420 #[test]
421 fn test_checkbox_appearance() {
422 let mut tree = FieldTree::new();
423 let id = tree.alloc(FieldNode {
424 partial_name: "cb".into(),
425 alternate_name: None,
426 mapping_name: None,
427 field_type: Some(FieldType::Button),
428 flags: FieldFlags::empty(),
429 value: None,
430 default_value: None,
431 default_appearance: None,
432 quadding: None,
433 max_len: None,
434 options: vec![],
435 top_index: None,
436 rect: Some([0.0, 0.0, 12.0, 12.0]),
437 appearance_state: None,
438 on_state: None,
439 page_index: None,
440 parent: None,
441 children: vec![],
442 object_id: None,
443 has_actions: false,
444 mk: None,
445 border_style: None,
446 });
447 assert!(
448 generate_checkbox_appearance(&tree, id, true).len()
449 > generate_checkbox_appearance(&tree, id, false).len()
450 );
451 }
452}