use oxideav_pdf::reader::DocumentReader;
use oxideav_pdf::{read_pdf_annotations, AnnotationKind, ThreeDActivation, ThreeDViewSelector};
fn read_annots(pdf: &[u8]) -> Vec<oxideav_pdf::PdfAnnotation> {
let mut r = DocumentReader::open(pdf).unwrap();
read_pdf_annotations(&mut r).unwrap()
}
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 three_d_minimal_3dd_only() {
let annot = "<< /Type /Annot /Subtype /3D /Rect [10 20 110 120] \
/3DD 6 0 R >>";
let stream_stub = "<< /Type /3D /Subtype /U3D /Length 0 >>\nstream\n\nendstream";
let pdf = synth_pdf_with_objects(&[annot, stream_stub], &[5]);
let annots = read_annots(&pdf);
assert_eq!(annots.len(), 1);
match &annots[0].kind {
AnnotationKind::ThreeD {
artwork,
view,
activation,
interactive,
view_box,
} => {
assert_eq!(artwork.map(|id| id.number), Some(6));
assert!(view.is_none(), "no /3DV ⇒ None");
assert_eq!(activation, &ThreeDActivation::default());
assert!(*interactive);
assert!(view_box.is_none());
}
other => panic!("expected ThreeD, got {:?}", other),
}
assert_eq!(annots[0].rect, [10.0, 20.0, 110.0, 120.0]);
}
#[test]
fn three_d_view_selector_indirect_view_ref() {
let annot = "<< /Type /Annot /Subtype /3D /Rect [0 0 100 100] \
/3DD 7 0 R /3DV 6 0 R >>";
let view_stub = "<< /Type /3DView /XN (default) >>";
let stream_stub = "<< /Type /3D /Subtype /U3D /Length 0 >>\nstream\n\nendstream";
let pdf = synth_pdf_with_objects(&[annot, view_stub, stream_stub], &[5]);
let annots = read_annots(&pdf);
match &annots[0].kind {
AnnotationKind::ThreeD { view, .. } => match view {
Some(ThreeDViewSelector::View(id)) => assert_eq!(id.number, 6),
other => panic!("expected View, got {:?}", other),
},
other => panic!("expected ThreeD, got {:?}", other),
}
}
#[test]
fn three_d_view_selector_integer_va_index() {
let annot = "<< /Type /Annot /Subtype /3D /Rect [0 0 100 100] \
/3DD 6 0 R /3DV 2 >>";
let stream_stub = "<< /Type /3D /Subtype /U3D /Length 0 >>\nstream\n\nendstream";
let pdf = synth_pdf_with_objects(&[annot, stream_stub], &[5]);
let annots = read_annots(&pdf);
match &annots[0].kind {
AnnotationKind::ThreeD { view, .. } => match view {
Some(ThreeDViewSelector::Index(n)) => assert_eq!(*n, 2),
other => panic!("expected Index, got {:?}", other),
},
other => panic!("expected ThreeD, got {:?}", other),
}
}
#[test]
fn three_d_view_selector_text_string_matches_in() {
let annot = "<< /Type /Annot /Subtype /3D /Rect [0 0 100 100] \
/3DD 6 0 R /3DV (Top View) >>";
let stream_stub = "<< /Type /3D /Subtype /U3D /Length 0 >>\nstream\n\nendstream";
let pdf = synth_pdf_with_objects(&[annot, stream_stub], &[5]);
let annots = read_annots(&pdf);
match &annots[0].kind {
AnnotationKind::ThreeD { view, .. } => match view {
Some(ThreeDViewSelector::Name(s)) => assert_eq!(s, "Top View"),
other => panic!("expected Name, got {:?}", other),
},
other => panic!("expected ThreeD, got {:?}", other),
}
}
#[test]
fn three_d_view_selector_symbolic_f_l_d() {
let annot = "<< /Type /Annot /Subtype /3D /Rect [0 0 100 100] \
/3DD 6 0 R /3DV /F >>";
let stream_stub = "<< /Type /3D /Subtype /U3D /Length 0 >>\nstream\n\nendstream";
let pdf = synth_pdf_with_objects(&[annot, stream_stub], &[5]);
let annots = read_annots(&pdf);
match &annots[0].kind {
AnnotationKind::ThreeD { view, .. } => match view {
Some(ThreeDViewSelector::Symbolic(s)) => assert_eq!(s, "F"),
other => panic!("expected Symbolic, got {:?}", other),
},
other => panic!("expected ThreeD, got {:?}", other),
}
}
#[test]
fn three_d_full_activation_dict_inline() {
let annot = "<< /Type /Annot /Subtype /3D /Rect [0 0 100 100] \
/3DD 6 0 R \
/3DA << /A /PO /AIS /I /D /XD /DIS /L /TB false /NP true >> \
/3DI false /3DB [-50 -50 50 50] >>";
let stream_stub = "<< /Type /3D /Subtype /U3D /Length 0 >>\nstream\n\nendstream";
let pdf = synth_pdf_with_objects(&[annot, stream_stub], &[5]);
let annots = read_annots(&pdf);
match &annots[0].kind {
AnnotationKind::ThreeD {
activation,
interactive,
view_box,
..
} => {
assert_eq!(activation.activation_when.as_deref(), Some("PO"));
assert_eq!(activation.artwork_state_on_activation.as_deref(), Some("I"));
assert_eq!(activation.deactivation_when.as_deref(), Some("XD"));
assert_eq!(
activation.artwork_state_on_deactivation.as_deref(),
Some("L")
);
assert_eq!(activation.toolbar, Some(false));
assert_eq!(activation.navigation_panel, Some(true));
assert!(!*interactive);
assert_eq!(*view_box, Some([-50.0, -50.0, 50.0, 50.0]));
}
other => panic!("expected ThreeD, got {:?}", other),
}
}
#[test]
fn three_d_activation_via_indirect_reference() {
let annot = "<< /Type /Annot /Subtype /3D /Rect [0 0 100 100] \
/3DD 6 0 R /3DA 7 0 R >>";
let stream_stub = "<< /Type /3D /Subtype /U3D /Length 0 >>\nstream\n\nendstream";
let activation_dict = "<< /A /PV /D /PC >>";
let pdf = synth_pdf_with_objects(&[annot, stream_stub, activation_dict], &[5]);
let annots = read_annots(&pdf);
match &annots[0].kind {
AnnotationKind::ThreeD { activation, .. } => {
assert_eq!(activation.activation_when.as_deref(), Some("PV"));
assert_eq!(activation.deactivation_when.as_deref(), Some("PC"));
assert!(activation.artwork_state_on_activation.is_none());
assert!(activation.toolbar.is_none());
}
other => panic!("expected ThreeD, got {:?}", other),
}
}
#[test]
fn three_d_without_3dd_still_surfaces() {
let annot = "<< /Type /Annot /Subtype /3D /Rect [0 0 100 100] >>";
let pdf = synth_pdf_with_objects(&[annot], &[5]);
let annots = read_annots(&pdf);
assert_eq!(annots.len(), 1);
match &annots[0].kind {
AnnotationKind::ThreeD {
artwork,
interactive,
..
} => {
assert!(artwork.is_none());
assert!(*interactive);
}
other => panic!("expected ThreeD, got {:?}", other),
}
}
#[test]
fn three_d_view_box_with_non_numeric_entry_drops_to_none() {
let annot = "<< /Type /Annot /Subtype /3D /Rect [0 0 100 100] \
/3DD 6 0 R /3DB [-50 -50 50 /Bogus] >>";
let stream_stub = "<< /Type /3D /Subtype /U3D /Length 0 >>\nstream\n\nendstream";
let pdf = synth_pdf_with_objects(&[annot, stream_stub], &[5]);
let annots = read_annots(&pdf);
match &annots[0].kind {
AnnotationKind::ThreeD { view_box, .. } => {
assert!(view_box.is_none());
}
other => panic!("expected ThreeD, got {:?}", other),
}
}
#[test]
fn three_d_unknown_activation_name_preserved_verbatim() {
let annot = "<< /Type /Annot /Subtype /3D /Rect [0 0 100 100] \
/3DD 6 0 R /3DA << /A /CustomTrigger >> >>";
let stream_stub = "<< /Type /3D /Subtype /U3D /Length 0 >>\nstream\n\nendstream";
let pdf = synth_pdf_with_objects(&[annot, stream_stub], &[5]);
let annots = read_annots(&pdf);
match &annots[0].kind {
AnnotationKind::ThreeD { activation, .. } => {
assert_eq!(activation.activation_when.as_deref(), Some("CustomTrigger"));
}
other => panic!("expected ThreeD, got {:?}", other),
}
}