Files
pdfcraft/crates/annot/src/model3d/tests.rs
T
KenM76andClaude Opus 5.5 fb05b733f9 M12: model_render, and a 3D reference's stream under /3D
A 3D reference dictionary holds the shared stream under /3D (ISO
32000-2 §13.6.3), not /3DD; it is followed once, and the caller refuses
anything that is not a stream, so a chain or cycle of references ends.
The tool is renamed model_render, since MCP tool names here are lowercase
letters and underscores only, and to match model_add.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 22:54:28 -05:00

89 lines
3.9 KiB
Rust

use std::sync::Arc;
use super::*;
/// A one-page document whose page has `annots` as its `/Annots`, plus `extra` objects from 4 on.
fn doc(annots: &str, extra: &[Vec<u8>]) -> Document {
let mut objs: Vec<Vec<u8>> = vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 612 792] >>".to_vec(),
format!("<< /Type /Page /Parent 2 0 R /Annots {annots} >>").into_bytes(),
];
objs.extend(extra.iter().cloned());
let mut out = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, o) in objs.iter().enumerate() {
offsets.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
out.extend_from_slice(o);
out.extend_from_slice(b"\nendobj\n");
}
let xref = out.len();
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
for o in offsets {
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
Document::open(Arc::new(out)).expect("opens")
}
fn stream(dict: &str, body: &[u8]) -> Vec<u8> {
let mut s = format!("<< {dict} /Length {} >>\nstream\n", body.len()).into_bytes();
s.extend_from_slice(body);
s.extend_from_slice(b"\nendstream");
s
}
fn annot(dd: &str) -> Vec<u8> {
format!("<< /Type /Annot /Subtype /3D /Rect [0 0 100 100] /3DD {dd} >>").into_bytes()
}
#[test]
fn a_plain_3d_stream_is_read_through_its_filters() {
let d = doc("[4 0 R]", &[annot("5 0 R"), stream("/Type /3D /Subtype /PRC /Filter /ASCIIHexDecode", b"505243>")]);
assert_eq!(model_3d(&d, 0, 0), Ok(Model3d { kind: Model3dKind::Prc, data: b"PRC".to_vec() }));
}
#[test]
fn a_3dref_leads_to_the_stream_it_names() {
let d = doc("[4 0 R]", &[annot("<< /Type /3DRef /3D 5 0 R >>"), stream("/Type /3D /Subtype /PRC", b"PRC")]);
assert_eq!(model_3d(&d, 0, 0).map(|m| m.data), Ok(b"PRC".to_vec()));
}
#[test]
fn a_u3d_stream_is_read_and_says_so() {
let d = doc("[4 0 R]", &[annot("5 0 R"), stream("/Type /3D /Subtype /U3D", b"U3D")]);
assert_eq!(model_3d(&d, 0, 0).map(|m| m.kind), Ok(Model3dKind::U3d));
}
#[test]
fn a_3d_annotation_without_a_stream_is_invalid() {
let d = doc("[<< /Type /Annot /Subtype /3D /Rect [0 0 1 1] >>]", &[]);
assert!(matches!(model_3d(&d, 0, 0), Err(AnnotError::Invalid(m)) if m.contains("no 3D stream")));
}
#[test]
fn an_unknown_format_is_invalid() {
let d = doc("[4 0 R]", &[annot("5 0 R"), stream("/Type /3D /Subtype /STL", b"x")]);
assert!(matches!(model_3d(&d, 0, 0), Err(AnnotError::Invalid(m)) if m.contains("unknown format (STL)")));
}
#[test]
fn a_3dref_is_followed_once_and_only_to_a_stream() {
// A reference naming another reference, or one that names itself, is damage: no stream.
let d = doc("[4 0 R]", &[annot("5 0 R"), b"<< /Type /3DRef /3D 6 0 R >>".to_vec(), b"<< /Type /3DRef /3D 5 0 R >>".to_vec()]);
assert!(matches!(model_3d(&d, 0, 0), Err(AnnotError::Invalid(m)) if m.contains("no 3D stream")));
// The stream is under `/3D`; a `/3DD` inside the reference is not the standard's key.
let d = doc("[4 0 R]", &[annot("<< /Type /3DRef /3DD 5 0 R >>"), stream("/Type /3D /Subtype /PRC", b"PRC")]);
assert!(matches!(model_3d(&d, 0, 0), Err(AnnotError::Invalid(m)) if m.contains("no 3D stream")));
}
#[test]
fn another_annotation_is_not_a_3d_one() {
let d = doc("[<< /Type /Annot /Subtype /Square /Rect [0 0 1 1] >> 4 0 R]", &[annot("5 0 R"), stream("/Subtype /PRC", b"PRC")]);
assert!(matches!(model_3d(&d, 0, 0), Err(AnnotError::Invalid(m)) if m.contains("not a 3D annotation")));
assert_eq!(model_3d(&d, 0, 2), Err(AnnotError::NoSuchAnnotation { page: 0, index: 2 }));
assert_eq!(model_3d_annotations(&d, 0), Ok(vec![1]));
assert_eq!(model_3d_annotations(&d, 1), Err(AnnotError::NoSuchPage(1)));
}