Skip to main content

pdfluent_forms/
appearance.rs

1//! Default Appearance (DA) parsing and appearance stream generation (B.6).
2
3use 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/// Parsed default appearance components.
10#[derive(Debug, Clone, Default)]
11pub struct DefaultAppearance {
12    /// Font name (PDF name without leading /).
13    pub font_name: Option<String>,
14    /// Font size (0 = auto-size).
15    pub font_size: f32,
16    /// Fill color components (gray, RGB, or CMYK).
17    pub color: Vec<f32>,
18    /// Color space operator: "g", "rg", or "k".
19    pub color_op: Option<String>,
20}
21
22/// Parse a /DA string like "0 0 0 rg /Helv 12 Tf".
23pub 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
68/// Generate a raw PDF content stream for a text field appearance.
69pub 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
170/// Generate appearance stream for a checkbox.
171pub 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
194/// Generate appearance stream for a radio button.
195pub 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
230/// Generate appearance stream for a choice field.
231pub 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
317/// Generate the appropriate appearance stream for any field type.
318pub 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}