Files
vectorcraft/crates/testkit/src/pdf.rs
T
bflatasticandClaude Opus 5.5 e0895fd4ba M4.47: PostScript .ai and placeholder-only .ai files say why they can't open
A PostScript file (a .ai saved in an older format or without PDF
compatibility, or an .eps, plain or with a binary header) is refused by any
name with a clear message instead of a parse error (there is no PostScript
importer yet). An .ai whose pages are only placeholder text over the editor's
private data (PieceInfo AIPrivateData) is detected and refused with advice to
save it with PDF compatibility, instead of opening the placeholder.
vectorcraft-pdf: is_postscript(), PdfError::PostScript / PlaceholderOnly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:37:11 +07:00

207 lines
7.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Hand-written PDF files for import tests: pages with given boxes, rotation, resources and
//! content, optionally encrypted with a user password (the standard security handler, revision 2:
//! 40-bit RC4), which the PDF writer can't produce.
use std::fmt::Write as _;
/// One page: its boxes as `[x0, y0, x1, y1]` in PDF user space.
#[derive(Clone, Debug, Default)]
pub struct PdfPage {
pub media: [f64; 4],
pub crop: Option<[f64; 4]>,
pub bleed: Option<[f64; 4]>,
pub trim: Option<[f64; 4]>,
pub art: Option<[f64; 4]>,
pub rotate: i32,
/// The entries of the page's resource dictionary (`/ColorSpace << … >>`).
pub resources: String,
/// The content stream (operators, uncompressed).
pub content: String,
/// More entries of the page dictionary (`/PieceInfo << … >>`).
pub entries: String,
}
impl PdfPage {
/// A `w` × `h` pt page drawing `content`.
pub fn new(w: f64, h: f64, content: &str) -> Self {
Self { media: [0.0, 0.0, w, h], content: content.into(), ..Default::default() }
}
}
/// Padding of passwords (PDF 1.7, 7.6.3.3).
const PAD: [u8; 32] = [
0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A,
];
fn padded(pw: &str) -> Vec<u8> {
let mut v: Vec<u8> = pw.bytes().take(32).collect();
v.extend_from_slice(&PAD[..32 - v.len()]);
v
}
fn hex(b: &[u8]) -> String {
b.iter().fold(String::new(), |mut s, x| {
let _ = write!(s, "{x:02X}");
s
})
}
fn rect(r: [f64; 4]) -> String {
format!("[{} {} {} {}]", r[0], r[1], r[2], r[3])
}
/// A PDF of `pages`; with a `password` it is encrypted and opens only with it (its owner password
/// is the same followed by `-owner`).
pub fn pdf(pages: &[PdfPage], password: Option<&str>) -> Vec<u8> {
pdf_with(pages, &[], password)
}
/// The object number of the first `extra` object of [`pdf_with`] for a file of `pages` pages.
pub fn first_extra(pages: usize) -> usize {
3 + 2 * pages
}
/// [`pdf`] with `extra` objects (written as given, not encrypted: streams such as functions and
/// ICC profiles that resources refer to by number, from [`first_extra`] on).
pub fn pdf_with(pages: &[PdfPage], extra: &[&str], password: Option<&str>) -> Vec<u8> {
let id = md5(b"vectorcraft test file");
// (O, U, file key) of the standard security handler, revision 2, permissions -4.
let crypt = password.map(|pw| {
let owner_key = md5(&padded(&format!("{pw}-owner")));
let o = rc4(&owner_key[..5], &padded(pw));
let mut k = padded(pw);
k.extend_from_slice(&o);
k.extend_from_slice(&(-4i32).to_le_bytes());
k.extend_from_slice(&id);
let key = md5(&k)[..5].to_vec();
let u = rc4(&key, &PAD);
(o, u, key)
});
let mut objects: Vec<Vec<u8>> = vec![];
let kids: Vec<String> = (0..pages.len()).map(|i| format!("{} 0 R", 3 + 2 * i)).collect();
objects.push(b"<< /Type /Catalog /Pages 2 0 R >>".to_vec());
objects.push(format!("<< /Type /Pages /Kids [{}] /Count {} >>", kids.join(" "), pages.len()).into_bytes());
for (i, p) in pages.iter().enumerate() {
let mut d = format!("<< /Type /Page /Parent 2 0 R /MediaBox {} /Contents {} 0 R /Resources << {} >>", rect(p.media), 4 + 2 * i, p.resources);
for (k, r) in [("CropBox", p.crop), ("BleedBox", p.bleed), ("TrimBox", p.trim), ("ArtBox", p.art)] {
if let Some(r) = r {
let _ = write!(d, " /{k} {}", rect(r));
}
}
if p.rotate != 0 {
let _ = write!(d, " /Rotate {}", p.rotate);
}
if !p.entries.is_empty() {
let _ = write!(d, " {}", p.entries);
}
d.push_str(" >>");
objects.push(d.into_bytes());
let num = objects.len() + 1;
let data = match &crypt {
Some((_, _, key)) => {
let mut k = key.clone();
k.extend_from_slice(&(num as u32).to_le_bytes()[..3]);
k.extend_from_slice(&[0, 0]);
rc4(&md5(&k)[..10], p.content.as_bytes())
}
None => p.content.as_bytes().to_vec(),
};
let mut s = format!("<< /Length {} >>\nstream\n", data.len()).into_bytes();
s.extend_from_slice(&data);
s.extend_from_slice(b"\nendstream");
objects.push(s);
}
objects.extend(extra.iter().map(|o| o.as_bytes().to_vec()));
if let Some((o, u, _)) = &crypt {
objects.push(format!("<< /Filter /Standard /V 1 /R 2 /O <{}> /U <{}> /P -4 >>", hex(o), hex(u)).into_bytes());
}
let mut out = b"%PDF-1.4\n".to_vec();
let mut offsets = vec![];
for (i, o) in objects.iter().enumerate() {
offsets.push(out.len());
out.extend(format!("{} 0 obj\n", i + 1).bytes());
out.extend_from_slice(o);
out.extend_from_slice(b"\nendobj\n");
}
let xref = out.len();
out.extend(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).bytes());
for off in offsets {
out.extend(format!("{off:010} 00000 n \n").bytes());
}
let encrypt = if crypt.is_some() { format!(" /Encrypt {} 0 R", objects.len()) } else { String::new() };
let (size, id) = (objects.len() + 1, hex(&id));
out.extend(format!("trailer\n<< /Size {size} /Root 1 0 R /ID [<{id}> <{id}>]{encrypt} >>\nstartxref\n{xref}\n%%EOF\n").bytes());
out
}
/// RC4 of `data` with `key`.
fn rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
let mut s: Vec<u8> = (0..=255).collect();
let mut j = 0u8;
for i in 0..256 {
j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
s.swap(i, j as usize);
}
let (mut i, mut j) = (0u8, 0u8);
data.iter()
.map(|b| {
i = i.wrapping_add(1);
j = j.wrapping_add(s[i as usize]);
s.swap(i as usize, j as usize);
b ^ s[s[i as usize].wrapping_add(s[j as usize]) as usize]
})
.collect()
}
/// MD5 of `data` (RFC 1321).
fn md5(data: &[u8]) -> [u8; 16] {
const SHIFT: [u32; 16] = [7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21];
let k: Vec<u32> = (0..64).map(|i| ((i as f64 + 1.0).sin().abs() * 4_294_967_296.0) as u32).collect();
let mut h = [0x6745_2301u32, 0xefcd_ab89, 0x98ba_dcfe, 0x1032_5476];
let mut msg = data.to_vec();
msg.push(0x80);
while msg.len() % 64 != 56 {
msg.push(0);
}
msg.extend_from_slice(&((data.len() as u64).wrapping_mul(8)).to_le_bytes());
for chunk in msg.chunks(64) {
let m: Vec<u32> = chunk.chunks(4).map(|w| u32::from_le_bytes([w[0], w[1], w[2], w[3]])).collect();
let [mut a, mut b, mut c, mut d] = h;
for i in 0..64 {
let (f, g) = match i / 16 {
0 => ((b & c) | (!b & d), i),
1 => ((d & b) | (!d & c), (5 * i + 1) % 16),
2 => (b ^ c ^ d, (3 * i + 5) % 16),
_ => (c ^ (b | !d), (7 * i) % 16),
};
let f = f.wrapping_add(a).wrapping_add(k[i]).wrapping_add(m[g]);
a = d;
d = c;
c = b;
b = b.wrapping_add(f.rotate_left(SHIFT[(i / 16) * 4 + i % 4]));
}
for (x, y) in h.iter_mut().zip([a, b, c, d]) {
*x = x.wrapping_add(y);
}
}
let mut out = [0u8; 16];
for (o, w) in out.chunks_mut(4).zip(h) {
o.copy_from_slice(&w.to_le_bytes());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn md5_and_rc4_match_their_reference_vectors() {
assert_eq!(hex(&md5(b"")), "D41D8CD98F00B204E9800998ECF8427E");
assert_eq!(hex(&md5(b"abc")), "900150983CD24FB0D6963F7D28E17F72");
assert_eq!(hex(&md5(&[b'a'; 100])), "36A92CC94A9E0FA21F625F8BFB007ADF");
assert_eq!(hex(&rc4(b"Key", b"Plaintext")), "BBF316E8D940AF0AD3");
}
}