1use rpdfium_core::Rect;
7use rpdfium_parser::ObjectId;
8
9use crate::icon_fit::IconFit;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum HighlightingMode {
14 None,
16 #[default]
18 Invert,
19 Outline,
21 Push,
23 Toggle,
25}
26
27impl HighlightingMode {
28 pub fn from_name(name: &str) -> Self {
30 match name {
31 "N" => Self::None,
32 "I" => Self::Invert,
33 "O" => Self::Outline,
34 "P" => Self::Push,
35 "T" => Self::Toggle,
36 _ => Self::Invert,
37 }
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum TextPosition {
47 #[default]
49 CaptionOnly = 0,
50 IconOnly = 1,
52 CaptionBelow = 2,
54 CaptionAbove = 3,
56 CaptionRight = 4,
58 CaptionLeft = 5,
60 Overlaid = 6,
62}
63
64impl TextPosition {
65 pub fn from_value(v: u32) -> Self {
67 match v {
68 0 => Self::CaptionOnly,
69 1 => Self::IconOnly,
70 2 => Self::CaptionBelow,
71 3 => Self::CaptionAbove,
72 4 => Self::CaptionRight,
73 5 => Self::CaptionLeft,
74 6 => Self::Overlaid,
75 _ => Self::CaptionOnly,
76 }
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct FormControl {
83 pub field_name: String,
85 pub rect: Rect,
87 pub appearance_state: Option<String>,
89 pub page_index: Option<usize>,
91 pub highlighting_mode: HighlightingMode,
93 pub rotation: u32,
95 pub border_color: Option<Vec<f32>>,
97 pub background_color: Option<Vec<f32>>,
99 pub caption: Option<String>,
101 pub rollover_caption: Option<String>,
103 pub alt_caption: Option<String>,
105 pub default_appearance: Option<String>,
107 pub normal_icon: Option<ObjectId>,
109 pub rollover_icon: Option<ObjectId>,
111 pub down_icon: Option<ObjectId>,
113 pub icon_fit: Option<IconFit>,
115 pub text_position: TextPosition,
117}
118
119impl FormControl {
120 pub fn rect(&self) -> Rect {
124 self.rect
125 }
126
127 #[deprecated(
129 note = "use `rect()` — there is no public FPDF_* API (internal `CPDF_FormControl::GetRect`)"
130 )]
131 #[inline]
132 pub fn get_rect(&self) -> Rect {
133 self.rect()
134 }
135
136 pub fn checked_ap_state(&self) -> Option<&str> {
140 self.appearance_state.as_deref()
141 }
142
143 #[deprecated(
145 note = "use `checked_ap_state()` — there is no public FPDF_* API (internal `CPDF_FormControl::GetCheckedAPState`)"
146 )]
147 #[inline]
148 pub fn get_checked_ap_state(&self) -> Option<&str> {
149 self.checked_ap_state()
150 }
151
152 pub fn export_value(&self) -> Option<&str> {
158 self.appearance_state.as_deref()
159 }
160
161 #[deprecated(
163 note = "use `export_value()` — there is no public FPDF_* API (internal `CPDF_FormControl::GetExportValue`)"
164 )]
165 #[inline]
166 pub fn get_export_value(&self) -> Option<&str> {
167 self.export_value()
168 }
169
170 pub fn highlighting_mode(&self) -> HighlightingMode {
174 self.highlighting_mode
175 }
176
177 #[deprecated(
179 note = "use `highlighting_mode()` — there is no public FPDF_* API (internal `CPDF_FormControl::GetHighlightingMode`)"
180 )]
181 #[inline]
182 pub fn get_highlighting_mode(&self) -> HighlightingMode {
183 self.highlighting_mode()
184 }
185
186 pub fn rotation(&self) -> u32 {
190 self.rotation
191 }
192
193 #[deprecated(
195 note = "use `rotation()` — there is no public FPDF_* API (internal `CPDF_FormControl::GetRotation`)"
196 )]
197 #[inline]
198 pub fn get_rotation(&self) -> u32 {
199 self.rotation()
200 }
201
202 pub fn normal_caption(&self) -> Option<&str> {
206 self.caption.as_deref()
207 }
208
209 #[deprecated(
211 note = "use `normal_caption()` — there is no public FPDF_* API (internal `CPDF_FormControl::GetNormalCaption`)"
212 )]
213 #[inline]
214 pub fn get_normal_caption(&self) -> Option<&str> {
215 self.normal_caption()
216 }
217
218 pub fn rollover_caption(&self) -> Option<&str> {
222 self.rollover_caption.as_deref()
223 }
224
225 #[deprecated(
227 note = "use `rollover_caption()` — there is no public FPDF_* API (internal `CPDF_FormControl::GetRolloverCaption`)"
228 )]
229 #[inline]
230 pub fn get_rollover_caption(&self) -> Option<&str> {
231 self.rollover_caption()
232 }
233
234 pub fn down_caption(&self) -> Option<&str> {
238 self.alt_caption.as_deref()
239 }
240
241 #[deprecated(
243 note = "use `down_caption()` — there is no public FPDF_* API (internal `CPDF_FormControl::GetDownCaption`)"
244 )]
245 #[inline]
246 pub fn get_down_caption(&self) -> Option<&str> {
247 self.down_caption()
248 }
249
250 pub fn text_position(&self) -> TextPosition {
254 self.text_position
255 }
256
257 #[deprecated(
259 note = "use `text_position()` — there is no public FPDF_* API (internal `CPDF_FormControl::GetTextPosition`)"
260 )]
261 #[inline]
262 pub fn get_text_position(&self) -> TextPosition {
263 self.text_position()
264 }
265
266 pub fn is_checked(&self) -> bool {
268 self.appearance_state.as_deref() == Some("Yes")
269 }
270
271 pub fn is_default_checked(&self) -> bool {
279 self.is_checked()
282 }
283
284 pub fn border_color_argb(&self) -> Option<u32> {
292 color_components_to_argb(self.border_color.as_deref())
293 }
294
295 pub fn background_color_argb(&self) -> Option<u32> {
303 color_components_to_argb(self.background_color.as_deref())
304 }
305}
306
307fn color_components_to_argb(components: Option<&[f32]>) -> Option<u32> {
312 let c = components?;
313 let to_byte = |f: f32| (f.clamp(0.0, 1.0) * 255.0).round() as u32;
314 let argb = match c.len() {
315 1 => {
316 let g = to_byte(c[0]);
317 0xFF00_0000 | (g << 16) | (g << 8) | g
318 }
319 3 => {
320 let r = to_byte(c[0]);
321 let g = to_byte(c[1]);
322 let b = to_byte(c[2]);
323 0xFF00_0000 | (r << 16) | (g << 8) | b
324 }
325 _ => return None,
326 };
327 Some(argb)
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 fn make_control() -> FormControl {
335 FormControl {
336 field_name: "checkbox1".to_string(),
337 rect: Rect {
338 left: 10.0,
339 bottom: 20.0,
340 right: 30.0,
341 top: 40.0,
342 },
343 appearance_state: Some("Yes".to_string()),
344 page_index: Some(0),
345 highlighting_mode: HighlightingMode::Invert,
346 rotation: 0,
347 border_color: None,
348 background_color: None,
349 caption: None,
350 rollover_caption: None,
351 alt_caption: None,
352 default_appearance: None,
353 normal_icon: None,
354 rollover_icon: None,
355 down_icon: None,
356 icon_fit: None,
357 text_position: TextPosition::default(),
358 }
359 }
360
361 #[test]
362 fn test_form_control_creation() {
363 let ctrl = make_control();
364 assert_eq!(ctrl.field_name, "checkbox1");
365 assert_eq!(ctrl.rect.left, 10.0);
366 assert_eq!(ctrl.appearance_state.as_deref(), Some("Yes"));
367 assert_eq!(ctrl.page_index, Some(0));
368 }
369
370 #[test]
371 fn test_form_control_no_page_index() {
372 let mut ctrl = make_control();
373 ctrl.field_name = "text1".to_string();
374 ctrl.rect = Rect {
375 left: 0.0,
376 bottom: 0.0,
377 right: 100.0,
378 top: 20.0,
379 };
380 ctrl.appearance_state = None;
381 ctrl.page_index = None;
382 assert!(ctrl.page_index.is_none());
383 assert!(ctrl.appearance_state.is_none());
384 }
385
386 #[test]
387 fn test_is_checked() {
388 let ctrl = make_control();
389 assert!(ctrl.is_checked());
390 }
391
392 #[test]
393 fn test_is_not_checked() {
394 let mut ctrl = make_control();
395 ctrl.appearance_state = Some("Off".to_string());
396 assert!(!ctrl.is_checked());
397 }
398
399 #[test]
400 fn test_is_checked_none() {
401 let mut ctrl = make_control();
402 ctrl.appearance_state = None;
403 assert!(!ctrl.is_checked());
404 }
405
406 #[test]
407 fn test_highlighting_mode_from_name() {
408 assert_eq!(HighlightingMode::from_name("N"), HighlightingMode::None);
409 assert_eq!(HighlightingMode::from_name("I"), HighlightingMode::Invert);
410 assert_eq!(HighlightingMode::from_name("O"), HighlightingMode::Outline);
411 assert_eq!(HighlightingMode::from_name("P"), HighlightingMode::Push);
412 assert_eq!(HighlightingMode::from_name("T"), HighlightingMode::Toggle);
413 assert_eq!(
414 HighlightingMode::from_name("Unknown"),
415 HighlightingMode::Invert
416 );
417 }
418
419 #[test]
420 fn test_form_control_with_mk_data() {
421 let mut ctrl = make_control();
422 ctrl.rotation = 90;
423 ctrl.border_color = Some(vec![1.0, 0.0, 0.0]);
424 ctrl.background_color = Some(vec![1.0, 1.0, 1.0]);
425 ctrl.caption = Some("Click me".to_string());
426 ctrl.rollover_caption = Some("Hover text".to_string());
427 ctrl.alt_caption = Some("Down text".to_string());
428 ctrl.highlighting_mode = HighlightingMode::Push;
429
430 assert_eq!(ctrl.rotation, 90);
431 assert_eq!(ctrl.border_color.as_ref().unwrap().len(), 3);
432 assert_eq!(ctrl.caption.as_deref(), Some("Click me"));
433 assert_eq!(ctrl.rollover_caption.as_deref(), Some("Hover text"));
434 assert_eq!(ctrl.alt_caption.as_deref(), Some("Down text"));
435 assert_eq!(ctrl.highlighting_mode, HighlightingMode::Push);
436 }
437
438 #[test]
439 fn test_text_position_from_value() {
440 assert_eq!(TextPosition::from_value(0), TextPosition::CaptionOnly);
441 assert_eq!(TextPosition::from_value(1), TextPosition::IconOnly);
442 assert_eq!(TextPosition::from_value(2), TextPosition::CaptionBelow);
443 assert_eq!(TextPosition::from_value(3), TextPosition::CaptionAbove);
444 assert_eq!(TextPosition::from_value(4), TextPosition::CaptionRight);
445 assert_eq!(TextPosition::from_value(5), TextPosition::CaptionLeft);
446 assert_eq!(TextPosition::from_value(6), TextPosition::Overlaid);
447 assert_eq!(TextPosition::from_value(99), TextPosition::CaptionOnly);
448 }
449
450 #[test]
451 fn test_text_position_default() {
452 assert_eq!(TextPosition::default(), TextPosition::CaptionOnly);
453 }
454
455 #[test]
456 fn test_form_control_with_icon_data() {
457 let mut ctrl = make_control();
458 ctrl.normal_icon = Some(ObjectId::new(10, 0));
459 ctrl.rollover_icon = Some(ObjectId::new(11, 0));
460 ctrl.down_icon = Some(ObjectId::new(12, 0));
461 ctrl.text_position = TextPosition::IconOnly;
462
463 assert_eq!(ctrl.normal_icon, Some(ObjectId::new(10, 0)));
464 assert_eq!(ctrl.rollover_icon, Some(ObjectId::new(11, 0)));
465 assert_eq!(ctrl.down_icon, Some(ObjectId::new(12, 0)));
466 assert_eq!(ctrl.text_position, TextPosition::IconOnly);
467 }
468
469 #[test]
470 fn test_border_color_argb_rgb_red() {
471 let mut ctrl = make_control();
472 ctrl.border_color = Some(vec![1.0, 0.0, 0.0]);
473 assert_eq!(ctrl.border_color_argb(), Some(0xFFFF0000));
474 }
475
476 #[test]
477 fn test_background_color_argb_grayscale() {
478 let mut ctrl = make_control();
479 ctrl.background_color = Some(vec![1.0]); assert_eq!(ctrl.background_color_argb(), Some(0xFFFFFFFF));
481 }
482
483 #[test]
484 fn test_background_color_argb_black_grayscale() {
485 let mut ctrl = make_control();
486 ctrl.background_color = Some(vec![0.0]); assert_eq!(ctrl.background_color_argb(), Some(0xFF000000));
488 }
489
490 #[test]
491 fn test_border_color_argb_none_for_cmyk() {
492 let mut ctrl = make_control();
493 ctrl.border_color = Some(vec![0.1, 0.2, 0.3, 0.4]); assert_eq!(ctrl.border_color_argb(), None);
495 }
496
497 #[test]
498 fn test_color_argb_none_when_absent() {
499 let ctrl = make_control();
500 assert_eq!(ctrl.border_color_argb(), None);
502 assert_eq!(ctrl.background_color_argb(), None);
503 }
504
505 #[test]
506 fn test_border_color_argb_rgb_green() {
507 let mut ctrl = make_control();
508 ctrl.border_color = Some(vec![0.0, 1.0, 0.0]);
509 assert_eq!(ctrl.border_color_argb(), Some(0xFF00FF00));
510 }
511}