Files
vectorcraft/crates/testkit/src/pdf.rs
T
bflatasticandClaude Opus 5.5 8046b9be84 M4.62: PDF/.ai layers from optional content: named layers with visibility, print state and lock; art that is off as hidden layers
Optional content groups become layers (in paint order, shared by every page)
with their name, default visibility, print usage and lock; art outside them
goes to a layer per page. Art that is off isn't lost: an update appended to
the file drops its /OCProperties so the interpreter draws every group, and
the marked-content sequences found walking the content (checked by tag) route
the art to its group's layer, which comes in hidden. A form hidden by its own
/OC keeps the file as it is. document.open layers: false keeps one layer per
page of only what shows (Save then asks for a name).

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

212 lines
8.0 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> {
pdf_with_catalog(pages, extra, "", password)
}
/// [`pdf_with`] with more entries in the catalog (`/OCProperties << … >>`).
pub fn pdf_with_catalog(pages: &[PdfPage], extra: &[&str], catalog: &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(format!("<< /Type /Catalog /Pages 2 0 R {catalog}>>").into_bytes());
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");
}
}