use oxideav_pdf::reader::DocumentReader;
use oxideav_pdf::{read_pdf_annotations, AnnotationKind, FixedPrint};
fn synth_pdf_with_objects(extra_obj_bodies: &[&str], annot_refs: &[u32]) -> Vec<u8> {
let mut body: Vec<u8> = Vec::new();
body.extend_from_slice(b"%PDF-1.7\n%");
body.extend_from_slice(&[0xe2, 0xe3, 0xcf, 0xd3]);
body.push(b'\n');
let mut offsets: Vec<usize> = Vec::new();
let push_obj = |body: &mut Vec<u8>, offsets: &mut Vec<usize>, n: u32, content: &str| {
offsets.push(body.len());
body.extend_from_slice(format!("{} 0 obj\n{}\nendobj\n", n, content).as_bytes());
};
push_obj(
&mut body,
&mut offsets,
1,
"<< /Type /Catalog /Pages 2 0 R >>",
);
push_obj(
&mut body,
&mut offsets,
2,
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
);
let annot_arr = annot_refs
.iter()
.map(|n| format!("{} 0 R", n))
.collect::<Vec<_>>()
.join(" ");
let page_dict = format!(
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
/Contents 4 0 R /Resources << >> /Annots [{}] >>",
annot_arr
);
push_obj(&mut body, &mut offsets, 3, &page_dict);
push_obj(
&mut body,
&mut offsets,
4,
"<< /Length 0 >>\nstream\n\nendstream",
);
for (i, ob) in extra_obj_bodies.iter().enumerate() {
push_obj(&mut body, &mut offsets, 5 + i as u32, ob);
}
let xref_off = body.len();
let n_objs = 5 + extra_obj_bodies.len();
body.extend_from_slice(format!("xref\n0 {}\n", n_objs).as_bytes());
body.extend_from_slice(b"0000000000 65535 f \n");
for off in &offsets {
body.extend_from_slice(format!("{:010} 00000 n \n", off).as_bytes());
}
body.extend_from_slice(
format!(
"trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{}\n%%EOF\n",
n_objs, xref_off
)
.as_bytes(),
);
body
}
#[test]
fn watermark_annotation_without_fixed_print() {
let pdf = synth_pdf_with_objects(
&["<< /Type /Annot /Subtype /Watermark /Rect [10 10 110 60] >>"],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
assert_eq!(anns.len(), 1);
match &anns[0].kind {
AnnotationKind::Watermark { fixed_print } => {
assert!(fixed_print.is_none(), "absent /FixedPrint ⇒ None");
}
other => panic!("expected Watermark, got {:?}", other),
}
}
#[test]
fn watermark_annotation_with_indirect_fixed_print_full() {
let pdf = synth_pdf_with_objects(
&[
"<< /Type /Annot /Subtype /Watermark /Rect [0 0 100 50] \
/FixedPrint 6 0 R >>",
"<< /Type /FixedPrint /Matrix [1 0 0 1 72 -72] /H 0 /V 1.0 >>",
],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
assert_eq!(anns.len(), 1);
match &anns[0].kind {
AnnotationKind::Watermark { fixed_print } => {
let fp = fixed_print.as_ref().expect("present /FixedPrint");
assert_eq!(fp.matrix, [1.0, 0.0, 0.0, 1.0, 72.0, -72.0]);
assert_eq!(fp.h, 0.0);
assert_eq!(fp.v, 1.0);
}
other => panic!("expected Watermark, got {:?}", other),
}
}
#[test]
fn watermark_fixed_print_defaults_apply_when_entries_absent() {
let pdf = synth_pdf_with_objects(
&[
"<< /Type /Annot /Subtype /Watermark /Rect [0 0 100 50] \
/FixedPrint 6 0 R >>",
"<< /Type /FixedPrint >>",
],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
match &anns[0].kind {
AnnotationKind::Watermark { fixed_print } => {
let fp = fixed_print.as_ref().unwrap();
assert_eq!(fp, &FixedPrint::default());
assert_eq!(fp.matrix, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
assert_eq!(fp.h, 0.0);
assert_eq!(fp.v, 0.0);
}
other => panic!("expected Watermark, got {:?}", other),
}
}
#[test]
fn watermark_fixed_print_partial_entries_keep_defaults() {
let pdf = synth_pdf_with_objects(
&[
"<< /Type /Annot /Subtype /Watermark /Rect [0 0 100 50] \
/FixedPrint 6 0 R >>",
"<< /Type /FixedPrint /H 0.5 >>",
],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
match &anns[0].kind {
AnnotationKind::Watermark { fixed_print } => {
let fp = fixed_print.as_ref().unwrap();
assert_eq!(fp.matrix, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
assert_eq!(fp.h, 0.5);
assert_eq!(fp.v, 0.0);
}
other => panic!("expected Watermark, got {:?}", other),
}
}
#[test]
fn watermark_inline_fixed_print_dict_is_decoded() {
let pdf = synth_pdf_with_objects(
&["<< /Type /Annot /Subtype /Watermark /Rect [0 0 50 50] \
/FixedPrint << /Type /FixedPrint /H 0.25 /V 0.75 >> >>"],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
match &anns[0].kind {
AnnotationKind::Watermark { fixed_print } => {
let fp = fixed_print.as_ref().unwrap();
assert_eq!(fp.h, 0.25);
assert_eq!(fp.v, 0.75);
}
other => panic!("expected Watermark, got {:?}", other),
}
}
#[test]
fn watermark_fixed_print_malformed_matrix_falls_back_to_identity() {
let pdf = synth_pdf_with_objects(
&[
"<< /Type /Annot /Subtype /Watermark /Rect [0 0 50 50] \
/FixedPrint 6 0 R >>",
"<< /Type /FixedPrint /Matrix [1 0 0 1 72] /H 0.1 >>",
],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
match &anns[0].kind {
AnnotationKind::Watermark { fixed_print } => {
let fp = fixed_print.as_ref().unwrap();
assert_eq!(fp.matrix, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
assert_eq!(fp.h, 0.1);
}
other => panic!("expected Watermark, got {:?}", other),
}
}
#[test]
fn redact_annotation_with_quadpoints_and_overlay_text() {
let pdf = synth_pdf_with_objects(
&["<< /Type /Annot /Subtype /Redact /Rect [10 20 110 60] \
/QuadPoints [10 60 110 60 10 20 110 20] \
/IC [1.0 0.0 0.0] \
/OverlayText (REDACTED) \
/Repeat true \
/DA (/Helv 12 Tf 0 g) \
/Q 1 >>"],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
assert_eq!(anns.len(), 1);
match &anns[0].kind {
AnnotationKind::Redact {
quad_points,
interior_colour,
overlay_form,
overlay_text,
repeat,
default_appearance,
quadding,
} => {
assert_eq!(
quad_points.as_deref(),
Some(&[10.0, 60.0, 110.0, 60.0, 10.0, 20.0, 110.0, 20.0][..])
);
assert_eq!(*interior_colour, Some([1.0, 0.0, 0.0]));
assert!(overlay_form.is_none(), "absent /RO ⇒ None");
assert_eq!(overlay_text.as_deref(), Some("REDACTED"));
assert!(*repeat);
assert_eq!(default_appearance.as_deref(), Some("/Helv 12 Tf 0 g"));
assert_eq!(*quadding, 1);
}
other => panic!("expected Redact, got {:?}", other),
}
}
#[test]
fn redact_annotation_falls_back_to_rect_when_quadpoints_absent() {
let pdf = synth_pdf_with_objects(
&["<< /Type /Annot /Subtype /Redact /Rect [0 0 100 100] >>"],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
assert_eq!(anns.len(), 1);
match &anns[0].kind {
AnnotationKind::Redact {
quad_points,
interior_colour,
overlay_form,
overlay_text,
repeat,
default_appearance,
quadding,
} => {
assert!(quad_points.is_none(), "absent ⇒ caller falls back to Rect");
assert!(interior_colour.is_none());
assert!(overlay_form.is_none());
assert!(overlay_text.is_none());
assert!(!*repeat, "default per Table 192");
assert!(default_appearance.is_none());
assert_eq!(*quadding, 0, "default per Table 192");
}
other => panic!("expected Redact, got {:?}", other),
}
}
#[test]
fn redact_annotation_carries_ro_reference_to_form_xobject() {
let pdf = synth_pdf_with_objects(
&[
"<< /Type /Annot /Subtype /Redact /Rect [0 0 100 100] \
/RO 6 0 R \
/IC [0.5 0.5 0.5] \
/OverlayText (ignored by spec when RO is present) >>",
"<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] \
/Length 0 >>\nstream\n\nendstream",
],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
match &anns[0].kind {
AnnotationKind::Redact {
overlay_form,
interior_colour,
overlay_text,
..
} => {
let oid = overlay_form.expect("/RO present");
assert_eq!(oid.number, 6);
assert_eq!(interior_colour.as_ref().unwrap()[0], 0.5);
assert!(overlay_text.is_some());
}
other => panic!("expected Redact, got {:?}", other),
}
}
#[test]
fn redact_interior_colour_rejects_non_devicergb_shapes() {
let pdf = synth_pdf_with_objects(
&["<< /Type /Annot /Subtype /Redact /Rect [0 0 50 50] \
/IC [0.1 0.2 0.3 0.4] >>"],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
match &anns[0].kind {
AnnotationKind::Redact {
interior_colour, ..
} => {
assert!(
interior_colour.is_none(),
"4-component /IC violates DeviceRGB-only rule"
);
}
other => panic!("expected Redact, got {:?}", other),
}
}
#[test]
fn redact_multi_quad_region_is_preserved_in_order() {
let pdf = synth_pdf_with_objects(
&["<< /Type /Annot /Subtype /Redact /Rect [0 0 200 100] \
/QuadPoints [0 100 100 100 0 0 100 0 \
100 100 200 100 100 0 200 0] >>"],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
match &anns[0].kind {
AnnotationKind::Redact { quad_points, .. } => {
let qp = quad_points.as_deref().unwrap();
assert_eq!(qp.len(), 16);
assert_eq!(
qp,
&[
0.0, 100.0, 100.0, 100.0, 0.0, 0.0, 100.0, 0.0, 100.0, 100.0, 200.0, 100.0,
100.0, 0.0, 200.0, 0.0,
][..]
);
}
other => panic!("expected Redact, got {:?}", other),
}
}
#[test]
fn redact_quadding_out_of_range_clamps_per_table_192() {
let pdf = synth_pdf_with_objects(
&["<< /Type /Annot /Subtype /Redact /Rect [0 0 50 50] \
/OverlayText (X) /DA (/F 12 Tf) /Q 9 >>"],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
match &anns[0].kind {
AnnotationKind::Redact { quadding, .. } => {
assert_eq!(
*quadding, 2,
"9 clamps to the table's max (right-justified)"
);
}
other => panic!("expected Redact, got {:?}", other),
}
}
#[test]
fn redact_repeat_defaults_to_false_when_omitted() {
let pdf = synth_pdf_with_objects(
&["<< /Type /Annot /Subtype /Redact /Rect [0 0 50 50] \
/OverlayText (R) /DA (/F 8 Tf) >>"],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
match &anns[0].kind {
AnnotationKind::Redact { repeat, .. } => {
assert!(!*repeat);
}
other => panic!("expected Redact, got {:?}", other),
}
}
#[test]
fn redact_default_appearance_decodes_utf16be_overlay_text() {
let pdf = synth_pdf_with_objects(
&["<< /Type /Annot /Subtype /Redact /Rect [0 0 50 50] \
/OverlayText <FEFF4E2D6587> /DA (/F 12 Tf) >>"],
&[5],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
match &anns[0].kind {
AnnotationKind::Redact { overlay_text, .. } => {
assert_eq!(overlay_text.as_deref(), Some("中文"));
}
other => panic!("expected Redact, got {:?}", other),
}
}
#[test]
fn watermark_and_redact_alongside_round197_subtypes() {
let pdf = synth_pdf_with_objects(
&[
"<< /Type /Annot /Subtype /Watermark /Rect [0 0 100 50] \
/FixedPrint 9 0 R >>",
"<< /Type /Annot /Subtype /Redact /Rect [10 10 90 90] \
/OverlayText (X) /DA (/F 12 Tf) >>",
"<< /Type /Annot /Subtype /Line /Rect [0 0 100 100] \
/L [0 0 100 100] >>",
"<< /Type /Annot /Subtype /Text /Rect [0 0 20 20] \
/Contents (Hello) >>",
"<< /Type /FixedPrint /H 0.5 >>",
],
&[5, 6, 7, 8],
);
let mut r = DocumentReader::open(&pdf).unwrap();
let anns = read_pdf_annotations(&mut r).unwrap();
assert_eq!(anns.len(), 4);
assert!(matches!(anns[0].kind, AnnotationKind::Watermark { .. }));
assert!(matches!(anns[1].kind, AnnotationKind::Redact { .. }));
assert!(matches!(anns[2].kind, AnnotationKind::Line { .. }));
assert!(matches!(anns[3].kind, AnnotationKind::Text { .. }));
}