use oxideav_scene::Scene;
use crate::error::PdfError;
use crate::info::{build_info_dict, has_metadata};
use crate::objects::{Dict, Document, Object, ObjectId};
use crate::page::{build_pages, PageInput};
use crate::resources::ResourceCollector;
use crate::sig::Signer;
use crate::writer::render_frame_for_linearize as render_frame;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FieldJustification {
#[default]
Left,
Center,
Right,
}
impl FieldJustification {
fn as_int(self) -> i64 {
match self {
Self::Left => 0,
Self::Center => 1,
Self::Right => 2,
}
}
}
#[derive(Debug, Clone)]
pub struct FormFieldText {
pub name: String,
pub rect: [f32; 4],
pub page_index: usize,
pub value: Option<String>,
pub max_length: Option<u32>,
pub multi_line: bool,
pub justification: FieldJustification,
pub default_appearance: Option<String>,
}
#[derive(Debug, Clone)]
pub struct FormFieldCheckbox {
pub name: String,
pub rect: [f32; 4],
pub page_index: usize,
pub checked: bool,
pub default_appearance: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RadioOption {
pub export_value: String,
pub rect: [f32; 4],
pub page_index: usize,
}
#[derive(Debug, Clone)]
pub struct FormFieldRadioGroup {
pub name: String,
pub options: Vec<RadioOption>,
pub value: Option<String>,
}
#[derive(Debug, Clone)]
pub struct FormFieldChoice {
pub name: String,
pub rect: [f32; 4],
pub page_index: usize,
pub options: Vec<String>,
pub value: Option<String>,
pub combo_box: bool,
pub default_appearance: Option<String>,
}
pub struct FormFieldSignature {
pub name: String,
pub rect: [f32; 4],
pub page_index: usize,
pub signer: Box<dyn Signer>,
pub identity: crate::sig::SignerIdentity,
}
impl std::fmt::Debug for FormFieldSignature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FormFieldSignature")
.field("name", &self.name)
.field("rect", &self.rect)
.field("page_index", &self.page_index)
.field("signer", &"<dyn Signer>")
.finish()
}
}
#[allow(missing_docs)]
pub enum FormField {
Text(FormFieldText),
Checkbox(FormFieldCheckbox),
RadioGroup(FormFieldRadioGroup),
Choice(FormFieldChoice),
Signature(FormFieldSignature),
}
impl std::fmt::Debug for FormField {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Text(t) => f.debug_tuple("Text").field(t).finish(),
Self::Checkbox(c) => f.debug_tuple("Checkbox").field(c).finish(),
Self::RadioGroup(r) => f.debug_tuple("RadioGroup").field(r).finish(),
Self::Choice(c) => f.debug_tuple("Choice").field(c).finish(),
Self::Signature(s) => f.debug_tuple("Signature").field(s).finish(),
}
}
}
const DEFAULT_DA: &str = "/Helv 12 Tf 0 g";
const CONTENTS_HEX_LEN: usize = 8192;
const BYTE_RANGE_SLOT_MAX: i64 = 99_999_999;
const BYTE_RANGE_SLOT_WIDTH: usize = 8;
pub fn write_pdf_with_form(scene: &Scene, form_fields: &[FormField]) -> Result<Vec<u8>, PdfError> {
let pages = scene
.pages
.as_ref()
.filter(|p| !p.is_empty())
.ok_or_else(|| {
PdfError::other(
"write_pdf_with_form: scene is not in pages mode (scene.pages is None or empty)",
)
})?;
let n_pages = pages.len();
let signature_count = form_fields
.iter()
.filter(|f| matches!(f, FormField::Signature(_)))
.count();
if signature_count > 1 {
return Err(PdfError::other(
"write_pdf_with_form: only one /FT /Sig field per call is supported (round 31)",
));
}
validate_pages(form_fields, n_pages)?;
struct Rendered<'a> {
frame: &'a oxideav_core::vector::VectorFrame,
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources: ResourceCollector,
}
let rendered: Vec<Rendered<'_>> = pages
.iter()
.map(|page| {
let (content_bytes, resources) = render_frame(&page.content);
Rendered {
frame: &page.content,
width: page.width,
height: page.height,
content_bytes,
resources,
}
})
.collect();
let inputs: Vec<PageInput<'_>> = rendered
.into_iter()
.map(|r| PageInput {
width: r.width,
height: r.height,
content_bytes: r.content_bytes,
resources: r.resources,
frame: r.frame,
})
.collect();
let mut doc = Document::new();
let pages_build = build_pages(&mut doc, inputs);
if has_metadata(&scene.metadata) {
let info_id = doc.add(Object::Dict(build_info_dict(&scene.metadata)));
doc.info = Some(info_id);
}
let mut top_field_ids: Vec<ObjectId> = Vec::with_capacity(form_fields.len());
let mut widgets_per_page: Vec<Vec<ObjectId>> = (0..n_pages).map(|_| Vec::new()).collect();
let mut sig_field_idx: Option<usize> = None;
let mut sig_dict_id: Option<ObjectId> = None;
let mut radio_kid_ids: Vec<Vec<ObjectId>> = Vec::with_capacity(form_fields.len());
for (i, field) in form_fields.iter().enumerate() {
let id = doc.allocate_id();
top_field_ids.push(id);
match field {
FormField::Text(t) => {
widgets_per_page[t.page_index].push(id);
radio_kid_ids.push(Vec::new());
}
FormField::Checkbox(c) => {
widgets_per_page[c.page_index].push(id);
radio_kid_ids.push(Vec::new());
}
FormField::RadioGroup(r) => {
let mut kids = Vec::with_capacity(r.options.len());
for opt in &r.options {
let kid_id = doc.allocate_id();
kids.push(kid_id);
widgets_per_page[opt.page_index].push(kid_id);
}
radio_kid_ids.push(kids);
}
FormField::Choice(c) => {
widgets_per_page[c.page_index].push(id);
radio_kid_ids.push(Vec::new());
}
FormField::Signature(s) => {
sig_field_idx = Some(i);
let sdid = doc.allocate_id();
sig_dict_id = Some(sdid);
widgets_per_page[s.page_index].push(id);
radio_kid_ids.push(Vec::new());
}
}
}
let mut contents_hex_offset_marker: Option<u32> = None;
for (i, field) in form_fields.iter().enumerate() {
let id = top_field_ids[i];
match field {
FormField::Text(t) => {
let dict = build_text_field_dict(t);
doc.add_object(id, Object::Dict(dict));
}
FormField::Checkbox(c) => {
let mut dict = build_checkbox_dict(c);
let ap = button_appearance_dict(
&mut doc,
c.rect,
"Yes",
checkbox_appearance_content(c.rect, true),
checkbox_appearance_content(c.rect, false),
);
dict.set("AP", ap);
doc.add_object(id, Object::Dict(dict));
}
FormField::RadioGroup(r) => {
let kid_ids = &radio_kid_ids[i];
let aggregate = build_radio_aggregate_dict(r, id, kid_ids);
doc.add_object(id, Object::Dict(aggregate));
for (opt, kid_id) in r.options.iter().zip(kid_ids.iter()) {
let active = matches!(&r.value, Some(v) if v == &opt.export_value);
let mut kid = build_radio_kid_dict(opt, id, active);
let ap = button_appearance_dict(
&mut doc,
opt.rect,
&opt.export_value,
radio_appearance_content(opt.rect, true),
radio_appearance_content(opt.rect, false),
);
kid.set("AP", ap);
doc.add_object(*kid_id, Object::Dict(kid));
}
}
FormField::Choice(c) => {
let dict = build_choice_field_dict(c);
doc.add_object(id, Object::Dict(dict));
}
FormField::Signature(s) => {
let dict = Dict::new()
.with("Type", Object::Name("Annot".into()))
.with("Subtype", Object::Name("Widget".into()))
.with("FT", Object::Name("Sig".into()))
.with("T", text_string(&s.name))
.with("Rect", rect_array(s.rect))
.with("F", Object::Integer(4))
.with(
"V",
Object::Reference(
sig_dict_id.expect("sig_dict_id allocated for signature field"),
),
)
.with("P", Object::Reference(pages_build.page_ids[s.page_index]));
doc.add_object(id, Object::Dict(dict));
let contents_placeholder = vec![0u8; CONTENTS_HEX_LEN / 2];
let signer_cert_hex = {
let cert_bytes = s
.identity
.cert_chain
.first()
.map(|v| v.as_slice())
.unwrap_or(&[]);
cert_bytes.to_vec()
};
let sig_dict = Dict::new()
.with("Type", Object::Name("Sig".into()))
.with("Filter", Object::Name("Adobe.PPKLite".into()))
.with("SubFilter", Object::Name("adbe.pkcs7.detached".into()))
.with(
"ByteRange",
Object::Array(vec![
Object::Integer(BYTE_RANGE_SLOT_MAX),
Object::Integer(BYTE_RANGE_SLOT_MAX),
Object::Integer(BYTE_RANGE_SLOT_MAX),
Object::Integer(BYTE_RANGE_SLOT_MAX),
]),
)
.with("Contents", Object::HexString(contents_placeholder))
.with("Cert", Object::HexString(signer_cert_hex));
doc.add_object(sig_dict_id.unwrap(), Object::Dict(sig_dict));
contents_hex_offset_marker = Some(sig_dict_id.unwrap().number);
}
}
}
let acroform_id = doc.allocate_id();
let mut acroform_dict = Dict::new()
.with(
"Fields",
Object::Array(
top_field_ids
.iter()
.map(|id| Object::Reference(*id))
.collect(),
),
)
.with("DA", Object::LiteralString(DEFAULT_DA.as_bytes().to_vec()));
if sig_field_idx.is_some() {
acroform_dict.set("SigFlags", Object::Integer(3));
}
acroform_dict.set("NeedAppearances", Object::Bool(true));
doc.add_object(acroform_id, Object::Dict(acroform_dict));
let catalog = doc
.object_mut(pages_build.catalog_id)
.ok_or_else(|| PdfError::other("write_pdf_with_form: catalog id missing"))?;
if let Object::Dict(d) = catalog {
d.set("AcroForm", Object::Reference(acroform_id));
}
for (page_idx, widgets) in widgets_per_page.iter().enumerate() {
if widgets.is_empty() {
continue;
}
let page_id = pages_build.page_ids[page_idx];
let page_obj = doc
.object_mut(page_id)
.ok_or_else(|| PdfError::other("write_pdf_with_form: page id missing"))?;
if let Object::Dict(d) = page_obj {
d.set(
"Annots",
Object::Array(widgets.iter().map(|w| Object::Reference(*w)).collect()),
);
}
}
if let Some(sig_idx) = sig_field_idx {
sign_path(
&mut doc,
form_fields,
sig_idx,
sig_dict_id.expect("sig dict id"),
contents_hex_offset_marker,
)
} else {
let mut out = Vec::with_capacity(4096);
doc.write_to(&mut out)?;
Ok(out)
}
}
fn rect_array(rect: [f32; 4]) -> Object {
Object::Array(rect.iter().map(|v| Object::Real(*v as f64)).collect())
}
fn emit_widget_appearance(doc: &mut Document, rect: [f32; 4], content: String) -> ObjectId {
let dict = Dict::new()
.with("Type", Object::Name("XObject".into()))
.with("Subtype", Object::Name("Form".into()))
.with("BBox", rect_array(rect));
doc.add(Object::Stream(crate::objects::Stream::new(
dict,
content.into_bytes(),
)))
}
fn checkbox_appearance_content(rect: [f32; 4], checked: bool) -> String {
use crate::operators::format_real;
let fr = |v: f32| format_real(f64::from(v));
let (x0, y0) = (rect[0] + 0.5, rect[1] + 0.5);
let (x1, y1) = (rect[2] - 0.5, rect[3] - 0.5);
let (w, h) = (x1 - x0, y1 - y0);
let mut ops = format!("0 G 1 w\n{} {} {} {} re\nS\n", fr(x0), fr(y0), fr(w), fr(h));
if checked && w > 0.0 && h > 0.0 {
let lw = (w.min(h) * 0.12).max(0.4);
ops.push_str(&format!(
"{} w\n1 J 1 j\n{} {} m\n{} {} l\n{} {} l\nS\n",
fr(lw),
fr(x0 + 0.20 * w),
fr(y0 + 0.50 * h),
fr(x0 + 0.45 * w),
fr(y0 + 0.25 * h),
fr(x0 + 0.80 * w),
fr(y0 + 0.75 * h),
));
}
ops
}
fn radio_appearance_content(rect: [f32; 4], on: bool) -> String {
use crate::operators::format_real;
let fr = |v: f32| format_real(f64::from(v));
let (x0, y0) = (rect[0] + 0.5, rect[1] + 0.5);
let (x1, y1) = (rect[2] - 0.5, rect[3] - 0.5);
let (cx, cy) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0);
let (rx, ry) = (((x1 - x0) / 2.0).max(0.0), ((y1 - y0) / 2.0).max(0.0));
let ellipse = |ops: &mut String, rx: f32, ry: f32| {
let k = crate::annotations::ARC_KAPPA;
let (kx, ky) = (rx * k, ry * k);
ops.push_str(&format!("{} {} m\n", fr(cx + rx), fr(cy)));
for (c1, c2, end) in [
((cx + rx, cy + ky), (cx + kx, cy + ry), (cx, cy + ry)),
((cx - kx, cy + ry), (cx - rx, cy + ky), (cx - rx, cy)),
((cx - rx, cy - ky), (cx - kx, cy - ry), (cx, cy - ry)),
((cx + kx, cy - ry), (cx + rx, cy - ky), (cx + rx, cy)),
] {
ops.push_str(&format!(
"{} {} {} {} {} {} c\n",
fr(c1.0),
fr(c1.1),
fr(c2.0),
fr(c2.1),
fr(end.0),
fr(end.1)
));
}
ops.push_str("h\n");
};
let mut ops = String::from("0 G 1 w\n");
ellipse(&mut ops, rx, ry);
ops.push_str("S\n");
if on {
ops.push_str("0 g\n");
ellipse(&mut ops, rx * 0.5, ry * 0.5);
ops.push_str("f\n");
}
ops
}
fn button_appearance_dict(
doc: &mut Document,
rect: [f32; 4],
on_state: &str,
on_content: String,
off_content: String,
) -> Object {
let on_id = emit_widget_appearance(doc, rect, on_content);
let off_id = emit_widget_appearance(doc, rect, off_content);
let states = Dict::new()
.with(on_state, Object::Reference(on_id))
.with("Off", Object::Reference(off_id));
Object::Dict(Dict::new().with("N", Object::Dict(states)))
}
fn text_string(s: &str) -> Object {
if s.bytes().all(|b| b.is_ascii() && b != 0) {
Object::LiteralString(s.as_bytes().to_vec())
} else {
let mut bytes = vec![0xFE, 0xFF];
for cp in s.encode_utf16() {
bytes.push((cp >> 8) as u8);
bytes.push((cp & 0xFF) as u8);
}
Object::HexString(bytes)
}
}
fn build_text_field_dict(t: &FormFieldText) -> Dict {
let mut d = Dict::new()
.with("Type", Object::Name("Annot".into()))
.with("Subtype", Object::Name("Widget".into()))
.with("FT", Object::Name("Tx".into()))
.with("T", text_string(&t.name))
.with("Rect", rect_array(t.rect))
.with("F", Object::Integer(4)); if let Some(v) = &t.value {
d.set("V", text_string(v));
d.set("DV", text_string(v));
}
if let Some(m) = t.max_length {
d.set("MaxLen", Object::Integer(m as i64));
}
if t.multi_line {
d.set("Ff", Object::Integer(0x1000));
}
d.set("Q", Object::Integer(t.justification.as_int()));
let da = t.default_appearance.as_deref().unwrap_or(DEFAULT_DA);
d.set("DA", Object::LiteralString(da.as_bytes().to_vec()));
d
}
fn build_checkbox_dict(c: &FormFieldCheckbox) -> Dict {
let mut d = Dict::new()
.with("Type", Object::Name("Annot".into()))
.with("Subtype", Object::Name("Widget".into()))
.with("FT", Object::Name("Btn".into()))
.with("T", text_string(&c.name))
.with("Rect", rect_array(c.rect))
.with("F", Object::Integer(4));
if c.checked {
d.set("V", Object::Name("Yes".into()));
d.set("AS", Object::Name("Yes".into()));
d.set("DV", Object::Name("Yes".into()));
} else {
d.set("V", Object::Name("Off".into()));
d.set("AS", Object::Name("Off".into()));
d.set("DV", Object::Name("Off".into()));
}
let da = c.default_appearance.as_deref().unwrap_or(DEFAULT_DA);
d.set("DA", Object::LiteralString(da.as_bytes().to_vec()));
d
}
fn build_radio_aggregate_dict(
r: &FormFieldRadioGroup,
_self_id: ObjectId,
kid_ids: &[ObjectId],
) -> Dict {
let ff: i64 = 0x8000 | 0x4000;
let mut d = Dict::new()
.with("FT", Object::Name("Btn".into()))
.with("T", text_string(&r.name))
.with("Ff", Object::Integer(ff))
.with(
"Kids",
Object::Array(kid_ids.iter().map(|id| Object::Reference(*id)).collect()),
);
if let Some(v) = &r.value {
d.set("V", Object::Name(v.clone()));
d.set("DV", Object::Name(v.clone()));
} else {
d.set("V", Object::Name("Off".into()));
d.set("DV", Object::Name("Off".into()));
}
d
}
fn build_radio_kid_dict(opt: &RadioOption, parent_id: ObjectId, active: bool) -> Dict {
let mut d = Dict::new()
.with("Type", Object::Name("Annot".into()))
.with("Subtype", Object::Name("Widget".into()))
.with("Parent", Object::Reference(parent_id))
.with("Rect", rect_array(opt.rect))
.with("F", Object::Integer(4));
let as_name = if active {
Object::Name(opt.export_value.clone())
} else {
Object::Name("Off".into())
};
d.set("AS", as_name);
d
}
fn build_choice_field_dict(c: &FormFieldChoice) -> Dict {
let mut d = Dict::new()
.with("Type", Object::Name("Annot".into()))
.with("Subtype", Object::Name("Widget".into()))
.with("FT", Object::Name("Ch".into()))
.with("T", text_string(&c.name))
.with("Rect", rect_array(c.rect))
.with("F", Object::Integer(4));
let opt_array: Vec<Object> = c.options.iter().map(|s| text_string(s)).collect();
d.set("Opt", Object::Array(opt_array));
if let Some(v) = &c.value {
d.set("V", text_string(v));
d.set("DV", text_string(v));
}
if c.combo_box {
d.set("Ff", Object::Integer(0x20000));
}
let da = c.default_appearance.as_deref().unwrap_or(DEFAULT_DA);
d.set("DA", Object::LiteralString(da.as_bytes().to_vec()));
d
}
fn validate_pages(form_fields: &[FormField], n_pages: usize) -> Result<(), PdfError> {
for field in form_fields {
match field {
FormField::Text(t) => check_page(t.page_index, n_pages)?,
FormField::Checkbox(c) => check_page(c.page_index, n_pages)?,
FormField::Choice(c) => check_page(c.page_index, n_pages)?,
FormField::Signature(s) => check_page(s.page_index, n_pages)?,
FormField::RadioGroup(r) => {
if r.options.is_empty() {
return Err(PdfError::other(
"write_pdf_with_form: radio group has no options",
));
}
for opt in &r.options {
check_page(opt.page_index, n_pages)?;
}
}
}
}
Ok(())
}
fn check_page(page_index: usize, n_pages: usize) -> Result<(), PdfError> {
if page_index >= n_pages {
Err(PdfError::other(format!(
"write_pdf_with_form: form field page_index {page_index} \
out of range (scene has {n_pages} page(s))",
)))
} else {
Ok(())
}
}
fn sign_path(
doc: &mut Document,
form_fields: &[FormField],
sig_idx: usize,
sig_dict_id: ObjectId,
_contents_hex_offset_marker: Option<u32>,
) -> Result<Vec<u8>, PdfError> {
let mut out = Vec::with_capacity(4096);
doc.write_to(&mut out)?;
let id_prefix = format!("{} 0 obj\n", sig_dict_id.number);
let obj_start = out
.windows(id_prefix.len())
.position(|w| w == id_prefix.as_bytes())
.ok_or_else(|| PdfError::other("sign_path: sig dict missing in serialised PDF"))?;
let body_start = obj_start + id_prefix.len();
let endobj_off = find_subslice(&out[body_start..], b"\nendobj\n")
.ok_or_else(|| PdfError::other("sign_path: endobj missing after sig dict"))?;
let body_end = body_start + endobj_off;
let body = &out[body_start..body_end];
let contents_marker = b"/Contents <";
let contents_in_body = find_subslice(body, contents_marker)
.ok_or_else(|| PdfError::other("sign_path: /Contents <…> marker missing"))?;
let contents_hex_start = body_start + contents_in_body + contents_marker.len();
let br_marker = b"/ByteRange [";
let br_in_body = find_subslice(body, br_marker)
.ok_or_else(|| PdfError::other("sign_path: /ByteRange marker missing"))?;
let br_array_start = body_start + br_in_body + br_marker.len();
let array_body_len = BYTE_RANGE_SLOT_WIDTH * 4 + 3;
let br_array_end = br_array_start + array_body_len;
if out.get(br_array_end) != Some(&b']') {
return Err(PdfError::other(format!(
"sign_path: /ByteRange array width drift (expected `]` at off {br_array_end})",
)));
}
let a: i64 = 0;
let b: i64 = contents_hex_start as i64;
let c: i64 = (contents_hex_start + CONTENTS_HEX_LEN) as i64;
let d: i64 = out.len() as i64 - c;
if a > BYTE_RANGE_SLOT_MAX
|| b > BYTE_RANGE_SLOT_MAX
|| c > BYTE_RANGE_SLOT_MAX
|| d > BYTE_RANGE_SLOT_MAX
{
return Err(PdfError::other(format!(
"sign_path: PDF too large for /ByteRange slot width {BYTE_RANGE_SLOT_WIDTH} \
(max value {BYTE_RANGE_SLOT_MAX})",
)));
}
let formatted = format!(
"{a:0w$} {b:0w$} {c:0w$} {d:0w$}",
a = a,
b = b,
c = c,
d = d,
w = BYTE_RANGE_SLOT_WIDTH
);
if formatted.len() != array_body_len {
return Err(PdfError::other(
"sign_path: byte-range formatter width drift",
));
}
out[br_array_start..br_array_end].copy_from_slice(formatted.as_bytes());
let (signer_ref, identity) = match &form_fields[sig_idx] {
FormField::Signature(s) => (s.signer.as_ref(), &s.identity),
_ => unreachable!(),
};
let signed_bytes = concat_byte_ranges(&out, [a, b, c, d])?;
let content_hash = signer_ref.algorithm().hash().hash(&signed_bytes);
let md_attr = crate::pubsec::verify::build_message_digest_attribute_der(&content_hash);
let ct_attr =
crate::sig::writer::build_content_type_attribute_der(&crate::pubsec::cms::OID_DATA);
let attrs_body = crate::pubsec::verify::pack_signed_attrs_implicit(&[ct_attr, md_attr]);
let tbs = crate::pubsec::verify::signed_attrs_to_be_signed(&attrs_body);
let tbs_hash = signer_ref.algorithm().hash().hash(&tbs);
let signature_bytes = signer_ref.sign(&tbs_hash)?;
let cms_blob = crate::sig::pkcs7_wrap_signed_data(
signer_ref.algorithm(),
&identity.issuer_der,
&identity.serial,
&identity.cert_chain,
Some(&attrs_body),
&signature_bytes,
);
patch_contents(&mut out, contents_hex_start, &cms_blob)?;
Ok(out)
}
fn find_subslice(hay: &[u8], needle: &[u8]) -> Option<usize> {
hay.windows(needle.len()).position(|w| w == needle)
}
fn patch_contents(
pdf: &mut [u8],
contents_hex_offset: usize,
contents_der: &[u8],
) -> Result<(), PdfError> {
let hex_len_needed = contents_der.len() * 2;
if hex_len_needed > CONTENTS_HEX_LEN {
return Err(PdfError::other(format!(
"write_pdf_with_form: CMS blob {hex_len_needed} hex chars exceeds /Contents budget {CONTENTS_HEX_LEN}",
)));
}
for (i, b) in contents_der.iter().enumerate() {
let hi = (b >> 4) & 0x0F;
let lo = b & 0x0F;
pdf[contents_hex_offset + 2 * i] = hex_digit(hi);
pdf[contents_hex_offset + 2 * i + 1] = hex_digit(lo);
}
for byte in pdf
.iter_mut()
.skip(contents_hex_offset + hex_len_needed)
.take(CONTENTS_HEX_LEN - hex_len_needed)
{
*byte = b'0';
}
Ok(())
}
fn hex_digit(n: u8) -> u8 {
match n {
0..=9 => b'0' + n,
10..=15 => b'A' + (n - 10),
_ => unreachable!(),
}
}
fn concat_byte_ranges(pdf: &[u8], byte_range: [i64; 4]) -> Result<Vec<u8>, PdfError> {
let [a, b, c, d] = byte_range;
if a < 0 || b < 0 || c < 0 || d < 0 {
return Err(PdfError::other("write_pdf_with_form: negative /ByteRange"));
}
let (a, b, c, d) = (a as usize, b as usize, c as usize, d as usize);
if a + b > pdf.len() || c + d > pdf.len() {
return Err(PdfError::other(
"write_pdf_with_form: /ByteRange extends past file length",
));
}
let mut out = Vec::with_capacity(b + d);
out.extend_from_slice(&pdf[a..a + b]);
out.extend_from_slice(&pdf[c..c + d]);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_da_is_helvetica_12pt_black() {
assert_eq!(DEFAULT_DA, "/Helv 12 Tf 0 g");
}
#[test]
fn rect_array_emits_four_reals() {
let o = rect_array([1.0, 2.0, 3.0, 4.0]);
match o {
Object::Array(a) => assert_eq!(a.len(), 4),
_ => panic!("expected array"),
}
}
#[test]
fn justification_int_values_match_table_222() {
assert_eq!(FieldJustification::Left.as_int(), 0);
assert_eq!(FieldJustification::Center.as_int(), 1);
assert_eq!(FieldJustification::Right.as_int(), 2);
}
}