Files
ElyOffice/crates/ely-pdf-engine/src/sample.rs
T

270 lines
13 KiB
Rust

//! Small PDFs written by hand, for tests.
/// A bookmark: title, page, and those under it.
pub type Mark<'a> = (&'a str, usize, &'a [(&'a str, usize)]);
/// Links, form fields and page labels to add.
#[derive(Default)]
pub struct Extras<'a> {
/// Link annotations: from page, to page.
pub links: &'a [(usize, usize)],
/// A text field on each of these pages.
pub fields: &'a [usize],
/// Page labels: first page, style name.
pub labels: &'a [(usize, &'a str)],
/// Fields leave out their optional /P.
pub loose: bool,
/// Every page's /UserUnit, if not one.
pub unit: Option<f32>,
/// Field names start with this, else "field".
pub prefix: Option<&'a str>,
/// The form's look: red 24 pt; font key, DA token.
pub look: Option<(&'a str, &'a str)>,
/// The form's default alignment, /Q.
pub align: Option<u8>,
/// Page one: a form of this text at 300, 400.
pub stamp: Option<&'a str>,
/// Page one: a 2x2 image, 100x50 at 72, 500.
pub picture: bool,
/// With `picture`: page one draws it full-page first.
pub backdrop: bool,
/// Fields hang under one parent of this name.
pub parent: Option<&'a str>,
/// A parent and child field with no widget.
pub bare: Option<(&'a str, &'a str)>,
/// Page one: a file annotation, name and bytes, no /Size.
pub attached: Option<(&'a str, &'a [u8])>,
}
/// A PDF of `pages` (width, height, lines), with bookmarks.
pub fn pdf(pages: &[(f32, f32, &[&str])], marks: &[Mark]) -> Vec<u8> {
pdf_with(pages, marks, &Extras::default())
}
/// A PDF as `pdf` makes, with `extras`.
pub fn pdf_with(pages: &[(f32, f32, &[&str])], marks: &[Mark], extras: &Extras) -> Vec<u8> {
let mut objs: Vec<Vec<u8>> = Vec::new();
let page_ids: Vec<usize> = (0..pages.len()).map(|n| 4 + n * 2).collect();
// Links, then fields, after the outline objects.
let outline_objs: usize = marks.iter().map(|(_, _, down)| 1 + down.len()).sum();
let first_extra = 4 + pages.len() * 2 + 1 + outline_objs;
let field_ids: Vec<usize> = (0..extras.fields.len()).map(|n| first_extra + extras.links.len() + n).collect();
let mut catalog = String::from("<< /Type /Catalog /Pages 2 0 R /Outlines 3 0 R");
// The parent field comes last of all.
let parent_id = first_extra + extras.links.len() + extras.fields.len() + usize::from(extras.stamp.is_some()) + usize::from(extras.picture);
let bare_id = parent_id + usize::from(extras.parent.is_some());
let attach_id = bare_id + 2 * usize::from(extras.bare.is_some());
if !field_ids.is_empty() || extras.bare.is_some() {
let mut refs: Vec<String> = match extras.parent {
Some(_) => vec![format!("{parent_id} 0 R")],
None => field_ids.iter().map(|id| format!("{id} 0 R")).collect(),
};
refs.extend(extras.bare.map(|_| format!("{bare_id} 0 R")));
let font = 4 + pages.len() * 2;
let look = extras.look.map_or_else(String::new, |(key, token)| format!(" /DA (/{token} 24 Tf 1 0 0 rg) /DR << /Font << /{key} {font} 0 R >> >>"));
let align = extras.align.map_or_else(String::new, |q| format!(" /Q {q}"));
catalog.push_str(&format!(" /AcroForm << /Fields [{}]{look}{align} >>", refs.join(" ")));
}
if !extras.labels.is_empty() {
let nums: Vec<String> = extras.labels.iter().map(|(at, style)| format!("{at} << /S /{style} >>")).collect();
catalog.push_str(&format!(" /PageLabels << /Nums [{}] >>", nums.join(" ")));
}
objs.push(format!("{catalog} >>").into_bytes());
let kids: Vec<String> = page_ids.iter().map(|id| format!("{id} 0 R")).collect();
objs.push(
format!(
"<< /Type /Pages /Kids [{}] /Count {} >>",
kids.join(" "),
pages.len()
)
.into_bytes(),
);
// Outline objects follow the pages.
let mut id = 4 + pages.len() * 2 + 1;
let tops: Vec<usize> = (marks.iter())
.map(|(_, _, down)| {
let top = id;
id += 1 + down.len();
top
})
.collect();
objs.push(match (tops.first(), tops.last()) {
(Some(first), Some(last)) => format!(
"<< /Type /Outlines /First {first} 0 R /Last {last} 0 R /Count {} >>",
tops.len()
)
.into_bytes(),
_ => b"<< /Type /Outlines /Count 0 >>".to_vec(),
});
for (n, (w, h, lines)) in pages.iter().enumerate() {
let mut stream = match n == 0 && extras.backdrop {
true => format!("q {w} 0 0 {h} 0 0 cm /Im1 Do Q\n"),
false => String::new(),
};
stream.push_str("BT /F1 18 Tf 72 0 Td\n");
for (at, line) in lines.iter().enumerate() {
let y = h - 72.0 - at as f32 * 24.0;
stream.push_str(&format!("1 0 0 1 72 {y} Tm ({line}) Tj\n"));
}
stream.push_str("ET");
let font = 4 + pages.len() * 2;
let (drawn, xobjects) = match n {
0 => xobjects(extras, first_extra + extras.links.len() + extras.fields.len()),
_ => (String::new(), String::new()),
};
stream.push_str(&drawn);
let links = (extras.links.iter().enumerate()).filter(|(_, (from, _))| *from == n).map(|(at, _)| first_extra + at);
let fields = (extras.fields.iter().enumerate()).filter(|(_, page)| **page == n).map(|(at, _)| field_ids[at]);
let attached = extras.attached.filter(|_| n == 0).map(|_| attach_id);
let annots: Vec<String> = links.chain(fields).chain(attached).map(|id| format!("{id} 0 R")).collect();
let unit = extras.unit.map_or_else(String::new, |unit| format!(" /UserUnit {unit}"));
objs.push(format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {w} {h}]{unit} /Contents {} 0 R /Resources << /Font << /F1 {font} 0 R >>{xobjects} >> /Annots [{}] >>", page_ids[n] + 1, annots.join(" ")).into_bytes());
objs.push(
format!(
"<< /Length {} >>\nstream\n{stream}\nendstream",
stream.len()
)
.into_bytes(),
);
}
objs.push(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec());
// Top marks link to each other; children hang under them.
for (at, (title, page, down)) in marks.iter().enumerate() {
let top = tops[at];
let mut dict = format!(
"<< /Title ({title}) /Parent 3 0 R /Dest [{} 0 R /XYZ 0 700 0]",
page_ids[*page]
);
if at > 0 {
dict.push_str(&format!(" /Prev {} 0 R", tops[at - 1]));
}
if at + 1 < tops.len() {
dict.push_str(&format!(" /Next {} 0 R", tops[at + 1]));
}
if !down.is_empty() {
dict.push_str(&format!(
" /First {} 0 R /Last {} 0 R /Count {}",
top + 1,
top + down.len(),
down.len()
));
}
objs.push(format!("{dict} >>").into_bytes());
for (n, (title, page)) in down.iter().enumerate() {
let mut dict = format!(
"<< /Title ({title}) /Parent {top} 0 R /Dest [{} 0 R /XYZ 0 500 0]",
page_ids[*page]
);
if n > 0 {
dict.push_str(&format!(" /Prev {} 0 R", top + n));
}
if n + 1 < down.len() {
dict.push_str(&format!(" /Next {} 0 R", top + n + 2));
}
objs.push(format!("{dict} >>").into_bytes());
}
}
for (_, to) in extras.links {
objs.push(format!("<< /Type /Annot /Subtype /Link /Rect [72 72 200 100] /Border [0 0 0] /Dest [{} 0 R /Fit] >>", page_ids[*to]).into_bytes());
}
for (n, page) in extras.fields.iter().enumerate() {
let owner = if extras.loose { String::new() } else { format!(" /P {} 0 R", page_ids[*page]) };
let name = extras.prefix.unwrap_or("field");
let parent = extras.parent.map_or_else(String::new, |_| format!(" /Parent {parent_id} 0 R"));
objs.push(format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T ({name}{n}) /V (value{n}) /Rect [72 120 300 140]{owner}{parent} >>").into_bytes());
}
if let Some(text) = extras.stamp {
let font = 4 + pages.len() * 2;
let content = format!("BT /F1 12 Tf 0 10 Td ({text}) Tj ET");
objs.push(format!("<< /Type /XObject /Subtype /Form /BBox [0 0 200 40] /Resources << /Font << /F1 {font} 0 R >> >> /Length {} >>\nstream\n{content}\nendstream", content.len()).into_bytes());
}
if extras.picture {
let mut image = b"<< /Type /XObject /Subtype /Image /Width 2 /Height 2 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Length 12 >>\nstream\n".to_vec();
image.extend_from_slice(&[255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255]);
image.extend_from_slice(b"\nendstream");
objs.push(image);
}
if let Some(name) = extras.parent {
let kids: Vec<String> = field_ids.iter().map(|id| format!("{id} 0 R")).collect();
objs.push(format!("<< /T ({name}) /Kids [{}] >>", kids.join(" ")).into_bytes());
}
if let Some((parent, child)) = extras.bare {
objs.push(format!("<< /T ({parent}) /Kids [{} 0 R] >>", bare_id + 1).into_bytes());
objs.push(format!("<< /T ({child}) /FT /Tx /V (Alice) /Parent {bare_id} 0 R >>").into_bytes());
}
if let Some((name, bytes)) = extras.attached {
objs.push(format!("<< /Type /Annot /Subtype /FileAttachment /Rect [72 72 90 90] /FS {} 0 R >>", attach_id + 1).into_bytes());
objs.push(format!("<< /Type /Filespec /F ({name}) /UF ({name}) /EF << /F {} 0 R >> >>", attach_id + 2).into_bytes());
let mut file = format!("<< /Type /EmbeddedFile /Length {} >>\nstream\n", bytes.len()).into_bytes();
file.extend_from_slice(bytes);
file.extend_from_slice(b"\nendstream");
objs.push(file);
}
file(&objs)
}
/// Page one's form and image draws, and their resources.
fn xobjects(extras: &Extras, first: usize) -> (String, String) {
let (mut drawn, mut names, mut id) = (String::new(), String::new(), first);
if extras.stamp.is_some() {
drawn.push_str("\nq 1 0 0 1 300 400 cm /Fm1 Do Q");
names.push_str(&format!(" /Fm1 {id} 0 R"));
id += 1;
}
if extras.picture {
drawn.push_str("\nq 100 0 0 50 72 500 cm /Im1 Do Q");
names.push_str(&format!(" /Im1 {id} 0 R"));
}
match names.is_empty() {
true => (drawn, names),
false => (drawn, format!(" /XObject <<{names} >>")),
}
}
/// One page of `size` points: a gray picture only.
pub fn scan((w, h): (f32, f32), (pw, ph): (u32, u32), gray: &[u8]) -> Vec<u8> {
let content = format!("q {w} 0 0 {h} 0 0 cm /Im1 Do Q");
let mut image = format!("<< /Type /XObject /Subtype /Image /Width {pw} /Height {ph} /ColorSpace /DeviceGray /BitsPerComponent 8 /Length {} >>\nstream\n", gray.len()).into_bytes();
image.extend_from_slice(gray);
image.extend_from_slice(b"\nendstream");
let objs: Vec<Vec<u8>> = vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {w} {h}] /Contents 4 0 R /Resources << /XObject << /Im1 5 0 R >> >> >>").into_bytes(),
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()).into_bytes(),
image,
];
file(&objs)
}
/// Numbered objects as a whole PDF file.
fn file(objs: &[Vec<u8>]) -> Vec<u8> {
let mut body = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (n, obj) in objs.iter().enumerate() {
offsets.push(body.len());
body.extend_from_slice(format!("{} 0 obj\n", n + 1).as_bytes());
body.extend_from_slice(obj);
body.extend_from_slice(b"\nendobj\n");
}
let xref = body.len();
body.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
for offset in offsets {
body.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
body.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
body
}
/// One Letter page drawing `content`; `objects` are numbered from 5.
pub fn one_page(catalog: &str, page: &str, content: &str, objects: &[String]) -> Vec<u8> {
let mut objs: Vec<Vec<u8>> = vec![
format!("<< /Type /Catalog /Pages 2 0 R{catalog} >>").into_bytes(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R{page} >>").into_bytes(),
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()).into_bytes(),
];
objs.extend(objects.iter().map(|obj| obj.clone().into_bytes()));
file(&objs)
}