1use oxideav_scene::Scene;
34
35use crate::error::PdfError;
36use crate::info::{build_info_dict, has_metadata};
37use crate::objects::{Dict, Document, Object, ObjectId};
38use crate::page::{build_pages, PageInput};
39use crate::resources::ResourceCollector;
40use crate::sig::Signer;
41use crate::writer::render_frame_for_linearize as render_frame;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49pub enum FieldJustification {
50 #[default]
52 Left,
53 Center,
55 Right,
57}
58
59impl FieldJustification {
60 fn as_int(self) -> i64 {
61 match self {
62 Self::Left => 0,
63 Self::Center => 1,
64 Self::Right => 2,
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct FormFieldText {
72 pub name: String,
74 pub rect: [f32; 4],
76 pub page_index: usize,
79 pub value: Option<String>,
81 pub max_length: Option<u32>,
83 pub multi_line: bool,
85 pub justification: FieldJustification,
87 pub default_appearance: Option<String>,
90}
91
92#[derive(Debug, Clone)]
96pub struct FormFieldCheckbox {
97 pub name: String,
99 pub rect: [f32; 4],
101 pub page_index: usize,
103 pub checked: bool,
105 pub default_appearance: Option<String>,
107}
108
109#[derive(Debug, Clone)]
113pub struct RadioOption {
114 pub export_value: String,
118 pub rect: [f32; 4],
120 pub page_index: usize,
122}
123
124#[derive(Debug, Clone)]
127pub struct FormFieldRadioGroup {
128 pub name: String,
130 pub options: Vec<RadioOption>,
132 pub value: Option<String>,
134}
135
136#[derive(Debug, Clone)]
139pub struct FormFieldChoice {
140 pub name: String,
142 pub rect: [f32; 4],
144 pub page_index: usize,
146 pub options: Vec<String>,
148 pub value: Option<String>,
151 pub combo_box: bool,
153 pub default_appearance: Option<String>,
155}
156
157pub struct FormFieldSignature {
161 pub name: String,
163 pub rect: [f32; 4],
165 pub page_index: usize,
167 pub signer: Box<dyn Signer>,
170 pub identity: crate::sig::SignerIdentity,
172}
173
174impl std::fmt::Debug for FormFieldSignature {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 f.debug_struct("FormFieldSignature")
177 .field("name", &self.name)
178 .field("rect", &self.rect)
179 .field("page_index", &self.page_index)
180 .field("signer", &"<dyn Signer>")
181 .finish()
182 }
183}
184
185#[allow(missing_docs)]
189pub enum FormField {
190 Text(FormFieldText),
191 Checkbox(FormFieldCheckbox),
192 RadioGroup(FormFieldRadioGroup),
193 Choice(FormFieldChoice),
194 Signature(FormFieldSignature),
195}
196
197impl std::fmt::Debug for FormField {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 match self {
200 Self::Text(t) => f.debug_tuple("Text").field(t).finish(),
201 Self::Checkbox(c) => f.debug_tuple("Checkbox").field(c).finish(),
202 Self::RadioGroup(r) => f.debug_tuple("RadioGroup").field(r).finish(),
203 Self::Choice(c) => f.debug_tuple("Choice").field(c).finish(),
204 Self::Signature(s) => f.debug_tuple("Signature").field(s).finish(),
205 }
206 }
207}
208
209const DEFAULT_DA: &str = "/Helv 12 Tf 0 g";
211
212const CONTENTS_HEX_LEN: usize = 8192;
214
215const BYTE_RANGE_SLOT_MAX: i64 = 99_999_999;
221const BYTE_RANGE_SLOT_WIDTH: usize = 8;
222
223pub fn write_pdf_with_form(scene: &Scene, form_fields: &[FormField]) -> Result<Vec<u8>, PdfError> {
238 let pages = scene
239 .pages
240 .as_ref()
241 .filter(|p| !p.is_empty())
242 .ok_or_else(|| {
243 PdfError::other(
244 "write_pdf_with_form: scene is not in pages mode (scene.pages is None or empty)",
245 )
246 })?;
247 let n_pages = pages.len();
248
249 let signature_count = form_fields
250 .iter()
251 .filter(|f| matches!(f, FormField::Signature(_)))
252 .count();
253 if signature_count > 1 {
254 return Err(PdfError::other(
255 "write_pdf_with_form: only one /FT /Sig field per call is supported (round 31)",
256 ));
257 }
258 validate_pages(form_fields, n_pages)?;
259
260 struct Rendered<'a> {
262 frame: &'a oxideav_core::vector::VectorFrame,
263 width: f32,
264 height: f32,
265 content_bytes: Vec<u8>,
266 resources: ResourceCollector,
267 }
268 let rendered: Vec<Rendered<'_>> = pages
269 .iter()
270 .map(|page| {
271 let (content_bytes, resources) = render_frame(&page.content);
272 Rendered {
273 frame: &page.content,
274 width: page.width,
275 height: page.height,
276 content_bytes,
277 resources,
278 }
279 })
280 .collect();
281
282 let inputs: Vec<PageInput<'_>> = rendered
283 .into_iter()
284 .map(|r| PageInput {
285 width: r.width,
286 height: r.height,
287 content_bytes: r.content_bytes,
288 resources: r.resources,
289 frame: r.frame,
290 })
291 .collect();
292
293 let mut doc = Document::new();
294 let pages_build = build_pages(&mut doc, inputs);
295 if has_metadata(&scene.metadata) {
296 let info_id = doc.add(Object::Dict(build_info_dict(&scene.metadata)));
297 doc.info = Some(info_id);
298 }
299
300 let mut top_field_ids: Vec<ObjectId> = Vec::with_capacity(form_fields.len());
305 let mut widgets_per_page: Vec<Vec<ObjectId>> = (0..n_pages).map(|_| Vec::new()).collect();
309 let mut sig_field_idx: Option<usize> = None;
311 let mut sig_dict_id: Option<ObjectId> = None;
312 let mut radio_kid_ids: Vec<Vec<ObjectId>> = Vec::with_capacity(form_fields.len());
314
315 for (i, field) in form_fields.iter().enumerate() {
316 let id = doc.allocate_id();
317 top_field_ids.push(id);
318 match field {
319 FormField::Text(t) => {
320 widgets_per_page[t.page_index].push(id);
321 radio_kid_ids.push(Vec::new());
322 }
323 FormField::Checkbox(c) => {
324 widgets_per_page[c.page_index].push(id);
325 radio_kid_ids.push(Vec::new());
326 }
327 FormField::RadioGroup(r) => {
328 let mut kids = Vec::with_capacity(r.options.len());
329 for opt in &r.options {
330 let kid_id = doc.allocate_id();
331 kids.push(kid_id);
332 widgets_per_page[opt.page_index].push(kid_id);
333 }
334 radio_kid_ids.push(kids);
335 }
336 FormField::Choice(c) => {
337 widgets_per_page[c.page_index].push(id);
338 radio_kid_ids.push(Vec::new());
339 }
340 FormField::Signature(s) => {
341 sig_field_idx = Some(i);
342 let sdid = doc.allocate_id();
343 sig_dict_id = Some(sdid);
344 widgets_per_page[s.page_index].push(id);
345 radio_kid_ids.push(Vec::new());
346 }
347 }
348 }
349
350 let mut contents_hex_offset_marker: Option<u32> = None;
352 for (i, field) in form_fields.iter().enumerate() {
353 let id = top_field_ids[i];
354 match field {
355 FormField::Text(t) => {
356 let dict = build_text_field_dict(t);
357 doc.add_object(id, Object::Dict(dict));
358 }
359 FormField::Checkbox(c) => {
360 let mut dict = build_checkbox_dict(c);
361 let ap = button_appearance_dict(
364 &mut doc,
365 c.rect,
366 "Yes",
367 checkbox_appearance_content(c.rect, true),
368 checkbox_appearance_content(c.rect, false),
369 );
370 dict.set("AP", ap);
371 doc.add_object(id, Object::Dict(dict));
372 }
373 FormField::RadioGroup(r) => {
374 let kid_ids = &radio_kid_ids[i];
375 let aggregate = build_radio_aggregate_dict(r, id, kid_ids);
376 doc.add_object(id, Object::Dict(aggregate));
377 for (opt, kid_id) in r.options.iter().zip(kid_ids.iter()) {
378 let active = matches!(&r.value, Some(v) if v == &opt.export_value);
379 let mut kid = build_radio_kid_dict(opt, id, active);
380 let ap = button_appearance_dict(
384 &mut doc,
385 opt.rect,
386 &opt.export_value,
387 radio_appearance_content(opt.rect, true),
388 radio_appearance_content(opt.rect, false),
389 );
390 kid.set("AP", ap);
391 doc.add_object(*kid_id, Object::Dict(kid));
392 }
393 }
394 FormField::Choice(c) => {
395 let dict = build_choice_field_dict(c);
396 doc.add_object(id, Object::Dict(dict));
397 }
398 FormField::Signature(s) => {
399 let dict = Dict::new()
401 .with("Type", Object::Name("Annot".into()))
402 .with("Subtype", Object::Name("Widget".into()))
403 .with("FT", Object::Name("Sig".into()))
404 .with("T", text_string(&s.name))
405 .with("Rect", rect_array(s.rect))
406 .with("F", Object::Integer(4))
407 .with(
408 "V",
409 Object::Reference(
410 sig_dict_id.expect("sig_dict_id allocated for signature field"),
411 ),
412 )
413 .with("P", Object::Reference(pages_build.page_ids[s.page_index]));
414 doc.add_object(id, Object::Dict(dict));
415
416 let contents_placeholder = vec![0u8; CONTENTS_HEX_LEN / 2];
424 let signer_cert_hex = {
425 let cert_bytes = s
426 .identity
427 .cert_chain
428 .first()
429 .map(|v| v.as_slice())
430 .unwrap_or(&[]);
431 cert_bytes.to_vec()
432 };
433 let sig_dict = Dict::new()
438 .with("Type", Object::Name("Sig".into()))
439 .with("Filter", Object::Name("Adobe.PPKLite".into()))
440 .with("SubFilter", Object::Name("adbe.pkcs7.detached".into()))
441 .with(
442 "ByteRange",
443 Object::Array(vec![
444 Object::Integer(BYTE_RANGE_SLOT_MAX),
445 Object::Integer(BYTE_RANGE_SLOT_MAX),
446 Object::Integer(BYTE_RANGE_SLOT_MAX),
447 Object::Integer(BYTE_RANGE_SLOT_MAX),
448 ]),
449 )
450 .with("Contents", Object::HexString(contents_placeholder))
451 .with("Cert", Object::HexString(signer_cert_hex));
452 doc.add_object(sig_dict_id.unwrap(), Object::Dict(sig_dict));
453 contents_hex_offset_marker = Some(sig_dict_id.unwrap().number);
454 }
455 }
456 }
457
458 let acroform_id = doc.allocate_id();
460 let mut acroform_dict = Dict::new()
461 .with(
462 "Fields",
463 Object::Array(
464 top_field_ids
465 .iter()
466 .map(|id| Object::Reference(*id))
467 .collect(),
468 ),
469 )
470 .with("DA", Object::LiteralString(DEFAULT_DA.as_bytes().to_vec()));
471 if sig_field_idx.is_some() {
473 acroform_dict.set("SigFlags", Object::Integer(3));
474 }
475 acroform_dict.set("NeedAppearances", Object::Bool(true));
479 doc.add_object(acroform_id, Object::Dict(acroform_dict));
480
481 let catalog = doc
483 .object_mut(pages_build.catalog_id)
484 .ok_or_else(|| PdfError::other("write_pdf_with_form: catalog id missing"))?;
485 if let Object::Dict(d) = catalog {
486 d.set("AcroForm", Object::Reference(acroform_id));
487 }
488
489 for (page_idx, widgets) in widgets_per_page.iter().enumerate() {
491 if widgets.is_empty() {
492 continue;
493 }
494 let page_id = pages_build.page_ids[page_idx];
495 let page_obj = doc
496 .object_mut(page_id)
497 .ok_or_else(|| PdfError::other("write_pdf_with_form: page id missing"))?;
498 if let Object::Dict(d) = page_obj {
499 d.set(
500 "Annots",
501 Object::Array(widgets.iter().map(|w| Object::Reference(*w)).collect()),
502 );
503 }
504 }
505
506 if let Some(sig_idx) = sig_field_idx {
508 sign_path(
514 &mut doc,
515 form_fields,
516 sig_idx,
517 sig_dict_id.expect("sig dict id"),
518 contents_hex_offset_marker,
519 )
520 } else {
521 let mut out = Vec::with_capacity(4096);
522 doc.write_to(&mut out)?;
523 Ok(out)
524 }
525}
526
527fn rect_array(rect: [f32; 4]) -> Object {
532 Object::Array(rect.iter().map(|v| Object::Real(*v as f64)).collect())
533}
534
535fn emit_widget_appearance(doc: &mut Document, rect: [f32; 4], content: String) -> ObjectId {
553 let dict = Dict::new()
554 .with("Type", Object::Name("XObject".into()))
555 .with("Subtype", Object::Name("Form".into()))
556 .with("BBox", rect_array(rect));
557 doc.add(Object::Stream(crate::objects::Stream::new(
558 dict,
559 content.into_bytes(),
560 )))
561}
562
563fn checkbox_appearance_content(rect: [f32; 4], checked: bool) -> String {
567 use crate::operators::format_real;
568 let fr = |v: f32| format_real(f64::from(v));
569 let (x0, y0) = (rect[0] + 0.5, rect[1] + 0.5);
570 let (x1, y1) = (rect[2] - 0.5, rect[3] - 0.5);
571 let (w, h) = (x1 - x0, y1 - y0);
572 let mut ops = format!("0 G 1 w\n{} {} {} {} re\nS\n", fr(x0), fr(y0), fr(w), fr(h));
573 if checked && w > 0.0 && h > 0.0 {
574 let lw = (w.min(h) * 0.12).max(0.4);
575 ops.push_str(&format!(
576 "{} w\n1 J 1 j\n{} {} m\n{} {} l\n{} {} l\nS\n",
577 fr(lw),
578 fr(x0 + 0.20 * w),
579 fr(y0 + 0.50 * h),
580 fr(x0 + 0.45 * w),
581 fr(y0 + 0.25 * h),
582 fr(x0 + 0.80 * w),
583 fr(y0 + 0.75 * h),
584 ));
585 }
586 ops
587}
588
589fn radio_appearance_content(rect: [f32; 4], on: bool) -> String {
593 use crate::operators::format_real;
594 let fr = |v: f32| format_real(f64::from(v));
595 let (x0, y0) = (rect[0] + 0.5, rect[1] + 0.5);
596 let (x1, y1) = (rect[2] - 0.5, rect[3] - 0.5);
597 let (cx, cy) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0);
598 let (rx, ry) = (((x1 - x0) / 2.0).max(0.0), ((y1 - y0) / 2.0).max(0.0));
599 let ellipse = |ops: &mut String, rx: f32, ry: f32| {
600 let k = crate::annotations::ARC_KAPPA;
601 let (kx, ky) = (rx * k, ry * k);
602 ops.push_str(&format!("{} {} m\n", fr(cx + rx), fr(cy)));
603 for (c1, c2, end) in [
604 ((cx + rx, cy + ky), (cx + kx, cy + ry), (cx, cy + ry)),
605 ((cx - kx, cy + ry), (cx - rx, cy + ky), (cx - rx, cy)),
606 ((cx - rx, cy - ky), (cx - kx, cy - ry), (cx, cy - ry)),
607 ((cx + kx, cy - ry), (cx + rx, cy - ky), (cx + rx, cy)),
608 ] {
609 ops.push_str(&format!(
610 "{} {} {} {} {} {} c\n",
611 fr(c1.0),
612 fr(c1.1),
613 fr(c2.0),
614 fr(c2.1),
615 fr(end.0),
616 fr(end.1)
617 ));
618 }
619 ops.push_str("h\n");
620 };
621 let mut ops = String::from("0 G 1 w\n");
622 ellipse(&mut ops, rx, ry);
623 ops.push_str("S\n");
624 if on {
625 ops.push_str("0 g\n");
626 ellipse(&mut ops, rx * 0.5, ry * 0.5);
627 ops.push_str("f\n");
628 }
629 ops
630}
631
632fn button_appearance_dict(
636 doc: &mut Document,
637 rect: [f32; 4],
638 on_state: &str,
639 on_content: String,
640 off_content: String,
641) -> Object {
642 let on_id = emit_widget_appearance(doc, rect, on_content);
643 let off_id = emit_widget_appearance(doc, rect, off_content);
644 let states = Dict::new()
645 .with(on_state, Object::Reference(on_id))
646 .with("Off", Object::Reference(off_id));
647 Object::Dict(Dict::new().with("N", Object::Dict(states)))
648}
649
650fn text_string(s: &str) -> Object {
651 if s.bytes().all(|b| b.is_ascii() && b != 0) {
655 Object::LiteralString(s.as_bytes().to_vec())
656 } else {
657 let mut bytes = vec![0xFE, 0xFF];
658 for cp in s.encode_utf16() {
659 bytes.push((cp >> 8) as u8);
660 bytes.push((cp & 0xFF) as u8);
661 }
662 Object::HexString(bytes)
663 }
664}
665
666fn build_text_field_dict(t: &FormFieldText) -> Dict {
667 let mut d = Dict::new()
668 .with("Type", Object::Name("Annot".into()))
669 .with("Subtype", Object::Name("Widget".into()))
670 .with("FT", Object::Name("Tx".into()))
671 .with("T", text_string(&t.name))
672 .with("Rect", rect_array(t.rect))
673 .with("F", Object::Integer(4)); if let Some(v) = &t.value {
675 d.set("V", text_string(v));
676 d.set("DV", text_string(v));
677 }
678 if let Some(m) = t.max_length {
679 d.set("MaxLen", Object::Integer(m as i64));
680 }
681 if t.multi_line {
683 d.set("Ff", Object::Integer(0x1000));
684 }
685 d.set("Q", Object::Integer(t.justification.as_int()));
686 let da = t.default_appearance.as_deref().unwrap_or(DEFAULT_DA);
687 d.set("DA", Object::LiteralString(da.as_bytes().to_vec()));
688 d
689}
690
691fn build_checkbox_dict(c: &FormFieldCheckbox) -> Dict {
692 let mut d = Dict::new()
693 .with("Type", Object::Name("Annot".into()))
694 .with("Subtype", Object::Name("Widget".into()))
695 .with("FT", Object::Name("Btn".into()))
696 .with("T", text_string(&c.name))
697 .with("Rect", rect_array(c.rect))
698 .with("F", Object::Integer(4));
699 if c.checked {
700 d.set("V", Object::Name("Yes".into()));
701 d.set("AS", Object::Name("Yes".into()));
702 d.set("DV", Object::Name("Yes".into()));
703 } else {
704 d.set("V", Object::Name("Off".into()));
705 d.set("AS", Object::Name("Off".into()));
706 d.set("DV", Object::Name("Off".into()));
707 }
708 let da = c.default_appearance.as_deref().unwrap_or(DEFAULT_DA);
709 d.set("DA", Object::LiteralString(da.as_bytes().to_vec()));
710 d
713}
714
715fn build_radio_aggregate_dict(
716 r: &FormFieldRadioGroup,
717 _self_id: ObjectId,
718 kid_ids: &[ObjectId],
719) -> Dict {
720 let ff: i64 = 0x8000 | 0x4000;
724 let mut d = Dict::new()
725 .with("FT", Object::Name("Btn".into()))
726 .with("T", text_string(&r.name))
727 .with("Ff", Object::Integer(ff))
728 .with(
729 "Kids",
730 Object::Array(kid_ids.iter().map(|id| Object::Reference(*id)).collect()),
731 );
732 if let Some(v) = &r.value {
733 d.set("V", Object::Name(v.clone()));
734 d.set("DV", Object::Name(v.clone()));
735 } else {
736 d.set("V", Object::Name("Off".into()));
737 d.set("DV", Object::Name("Off".into()));
738 }
739 d
740}
741
742fn build_radio_kid_dict(opt: &RadioOption, parent_id: ObjectId, active: bool) -> Dict {
743 let mut d = Dict::new()
744 .with("Type", Object::Name("Annot".into()))
745 .with("Subtype", Object::Name("Widget".into()))
746 .with("Parent", Object::Reference(parent_id))
747 .with("Rect", rect_array(opt.rect))
748 .with("F", Object::Integer(4));
749 let as_name = if active {
752 Object::Name(opt.export_value.clone())
753 } else {
754 Object::Name("Off".into())
755 };
756 d.set("AS", as_name);
757 d
758}
759
760fn build_choice_field_dict(c: &FormFieldChoice) -> Dict {
761 let mut d = Dict::new()
762 .with("Type", Object::Name("Annot".into()))
763 .with("Subtype", Object::Name("Widget".into()))
764 .with("FT", Object::Name("Ch".into()))
765 .with("T", text_string(&c.name))
766 .with("Rect", rect_array(c.rect))
767 .with("F", Object::Integer(4));
768 let opt_array: Vec<Object> = c.options.iter().map(|s| text_string(s)).collect();
770 d.set("Opt", Object::Array(opt_array));
771 if let Some(v) = &c.value {
772 d.set("V", text_string(v));
773 d.set("DV", text_string(v));
774 }
775 if c.combo_box {
777 d.set("Ff", Object::Integer(0x20000));
778 }
779 let da = c.default_appearance.as_deref().unwrap_or(DEFAULT_DA);
780 d.set("DA", Object::LiteralString(da.as_bytes().to_vec()));
781 d
782}
783
784fn validate_pages(form_fields: &[FormField], n_pages: usize) -> Result<(), PdfError> {
785 for field in form_fields {
786 match field {
787 FormField::Text(t) => check_page(t.page_index, n_pages)?,
788 FormField::Checkbox(c) => check_page(c.page_index, n_pages)?,
789 FormField::Choice(c) => check_page(c.page_index, n_pages)?,
790 FormField::Signature(s) => check_page(s.page_index, n_pages)?,
791 FormField::RadioGroup(r) => {
792 if r.options.is_empty() {
793 return Err(PdfError::other(
794 "write_pdf_with_form: radio group has no options",
795 ));
796 }
797 for opt in &r.options {
798 check_page(opt.page_index, n_pages)?;
799 }
800 }
801 }
802 }
803 Ok(())
804}
805
806fn check_page(page_index: usize, n_pages: usize) -> Result<(), PdfError> {
807 if page_index >= n_pages {
808 Err(PdfError::other(format!(
809 "write_pdf_with_form: form field page_index {page_index} \
810 out of range (scene has {n_pages} page(s))",
811 )))
812 } else {
813 Ok(())
814 }
815}
816
817fn sign_path(
822 doc: &mut Document,
823 form_fields: &[FormField],
824 sig_idx: usize,
825 sig_dict_id: ObjectId,
826 _contents_hex_offset_marker: Option<u32>,
827) -> Result<Vec<u8>, PdfError> {
828 let mut out = Vec::with_capacity(4096);
836 doc.write_to(&mut out)?;
837
838 let id_prefix = format!("{} 0 obj\n", sig_dict_id.number);
840 let obj_start = out
841 .windows(id_prefix.len())
842 .position(|w| w == id_prefix.as_bytes())
843 .ok_or_else(|| PdfError::other("sign_path: sig dict missing in serialised PDF"))?;
844 let body_start = obj_start + id_prefix.len();
845 let endobj_off = find_subslice(&out[body_start..], b"\nendobj\n")
846 .ok_or_else(|| PdfError::other("sign_path: endobj missing after sig dict"))?;
847 let body_end = body_start + endobj_off;
848 let body = &out[body_start..body_end];
849
850 let contents_marker = b"/Contents <";
852 let contents_in_body = find_subslice(body, contents_marker)
853 .ok_or_else(|| PdfError::other("sign_path: /Contents <…> marker missing"))?;
854 let contents_hex_start = body_start + contents_in_body + contents_marker.len();
855
856 let br_marker = b"/ByteRange [";
858 let br_in_body = find_subslice(body, br_marker)
859 .ok_or_else(|| PdfError::other("sign_path: /ByteRange marker missing"))?;
860 let br_array_start = body_start + br_in_body + br_marker.len();
861 let array_body_len = BYTE_RANGE_SLOT_WIDTH * 4 + 3;
869 let br_array_end = br_array_start + array_body_len;
870 if out.get(br_array_end) != Some(&b']') {
872 return Err(PdfError::other(format!(
873 "sign_path: /ByteRange array width drift (expected `]` at off {br_array_end})",
874 )));
875 }
876
877 let a: i64 = 0;
881 let b: i64 = contents_hex_start as i64;
882 let c: i64 = (contents_hex_start + CONTENTS_HEX_LEN) as i64;
883 let d: i64 = out.len() as i64 - c;
884
885 if a > BYTE_RANGE_SLOT_MAX
887 || b > BYTE_RANGE_SLOT_MAX
888 || c > BYTE_RANGE_SLOT_MAX
889 || d > BYTE_RANGE_SLOT_MAX
890 {
891 return Err(PdfError::other(format!(
892 "sign_path: PDF too large for /ByteRange slot width {BYTE_RANGE_SLOT_WIDTH} \
893 (max value {BYTE_RANGE_SLOT_MAX})",
894 )));
895 }
896
897 let formatted = format!(
900 "{a:0w$} {b:0w$} {c:0w$} {d:0w$}",
901 a = a,
902 b = b,
903 c = c,
904 d = d,
905 w = BYTE_RANGE_SLOT_WIDTH
906 );
907 if formatted.len() != array_body_len {
908 return Err(PdfError::other(
909 "sign_path: byte-range formatter width drift",
910 ));
911 }
912 out[br_array_start..br_array_end].copy_from_slice(formatted.as_bytes());
913
914 let (signer_ref, identity) = match &form_fields[sig_idx] {
916 FormField::Signature(s) => (s.signer.as_ref(), &s.identity),
917 _ => unreachable!(),
918 };
919 let signed_bytes = concat_byte_ranges(&out, [a, b, c, d])?;
920 let content_hash = signer_ref.algorithm().hash().hash(&signed_bytes);
921 let md_attr = crate::pubsec::verify::build_message_digest_attribute_der(&content_hash);
922 let ct_attr =
923 crate::sig::writer::build_content_type_attribute_der(&crate::pubsec::cms::OID_DATA);
924 let attrs_body = crate::pubsec::verify::pack_signed_attrs_implicit(&[ct_attr, md_attr]);
925 let tbs = crate::pubsec::verify::signed_attrs_to_be_signed(&attrs_body);
926 let tbs_hash = signer_ref.algorithm().hash().hash(&tbs);
927 let signature_bytes = signer_ref.sign(&tbs_hash)?;
928
929 let cms_blob = crate::sig::pkcs7_wrap_signed_data(
930 signer_ref.algorithm(),
931 &identity.issuer_der,
932 &identity.serial,
933 &identity.cert_chain,
934 Some(&attrs_body),
935 &signature_bytes,
936 );
937
938 patch_contents(&mut out, contents_hex_start, &cms_blob)?;
939 Ok(out)
940}
941
942fn find_subslice(hay: &[u8], needle: &[u8]) -> Option<usize> {
943 hay.windows(needle.len()).position(|w| w == needle)
944}
945
946fn patch_contents(
947 pdf: &mut [u8],
948 contents_hex_offset: usize,
949 contents_der: &[u8],
950) -> Result<(), PdfError> {
951 let hex_len_needed = contents_der.len() * 2;
952 if hex_len_needed > CONTENTS_HEX_LEN {
953 return Err(PdfError::other(format!(
954 "write_pdf_with_form: CMS blob {hex_len_needed} hex chars exceeds /Contents budget {CONTENTS_HEX_LEN}",
955 )));
956 }
957 for (i, b) in contents_der.iter().enumerate() {
958 let hi = (b >> 4) & 0x0F;
959 let lo = b & 0x0F;
960 pdf[contents_hex_offset + 2 * i] = hex_digit(hi);
961 pdf[contents_hex_offset + 2 * i + 1] = hex_digit(lo);
962 }
963 for byte in pdf
964 .iter_mut()
965 .skip(contents_hex_offset + hex_len_needed)
966 .take(CONTENTS_HEX_LEN - hex_len_needed)
967 {
968 *byte = b'0';
969 }
970 Ok(())
971}
972
973fn hex_digit(n: u8) -> u8 {
974 match n {
975 0..=9 => b'0' + n,
976 10..=15 => b'A' + (n - 10),
977 _ => unreachable!(),
978 }
979}
980
981fn concat_byte_ranges(pdf: &[u8], byte_range: [i64; 4]) -> Result<Vec<u8>, PdfError> {
982 let [a, b, c, d] = byte_range;
983 if a < 0 || b < 0 || c < 0 || d < 0 {
984 return Err(PdfError::other("write_pdf_with_form: negative /ByteRange"));
985 }
986 let (a, b, c, d) = (a as usize, b as usize, c as usize, d as usize);
987 if a + b > pdf.len() || c + d > pdf.len() {
988 return Err(PdfError::other(
989 "write_pdf_with_form: /ByteRange extends past file length",
990 ));
991 }
992 let mut out = Vec::with_capacity(b + d);
993 out.extend_from_slice(&pdf[a..a + b]);
994 out.extend_from_slice(&pdf[c..c + d]);
995 Ok(out)
996}
997
998#[cfg(test)]
999mod tests {
1000 use super::*;
1001
1002 #[test]
1003 fn default_da_is_helvetica_12pt_black() {
1004 assert_eq!(DEFAULT_DA, "/Helv 12 Tf 0 g");
1006 }
1007
1008 #[test]
1009 fn rect_array_emits_four_reals() {
1010 let o = rect_array([1.0, 2.0, 3.0, 4.0]);
1011 match o {
1012 Object::Array(a) => assert_eq!(a.len(), 4),
1013 _ => panic!("expected array"),
1014 }
1015 }
1016
1017 #[test]
1018 fn justification_int_values_match_table_222() {
1019 assert_eq!(FieldJustification::Left.as_int(), 0);
1021 assert_eq!(FieldJustification::Center.as_int(), 1);
1022 assert_eq!(FieldJustification::Right.as_int(), 2);
1023 }
1024}