use std::io::Write;
use oxideav_scene::Scene;
use crate::error::PdfError;
use crate::info::{build_info_dict, has_metadata};
use crate::objects::{Dict, Document, IndirectObject, Object, ObjectId, Stream};
use crate::resources::ResourceCollector;
use crate::writer::render_frame_for_linearize;
pub fn write_pdf_linearized(scene: &Scene) -> Result<Vec<u8>, PdfError> {
let pages = scene
.pages
.as_ref()
.filter(|p| !p.is_empty())
.ok_or_else(|| {
PdfError::other(
"write_pdf_linearized: scene is not in pages mode (scene.pages is None or empty)",
)
})?;
let rendered: Vec<RenderedOwned> = pages
.iter()
.map(|page| {
let (content_bytes, resources) = render_frame_for_linearize(&page.content);
RenderedOwned {
width: page.width,
height: page.height,
content_bytes,
resources,
}
})
.collect();
let n_pages = rendered.len();
let mut owned_pages: Vec<OwnedPage> = Vec::with_capacity(n_pages);
for r in &rendered {
let mut sub_doc = Document::new();
sub_doc.set_next_id(1);
let res_obj = r.resources.flatten_into_resources_dict(&mut sub_doc);
let extras = crate::objects::take_objects(&mut sub_doc);
owned_pages.push(OwnedPage {
width: r.width,
height: r.height,
content_bytes: r.content_bytes.clone(),
resources_dict: res_obj,
extra_objects: extras,
});
}
let mut next_id_n = 1u32;
let mut alloc = || {
let id = ObjectId::new(next_id_n);
next_id_n += 1;
id
};
let mut g2_page_ids = Vec::with_capacity(n_pages.saturating_sub(1));
let mut g2_resources_ids = Vec::with_capacity(n_pages.saturating_sub(1));
let mut g2_contents_ids = Vec::with_capacity(n_pages.saturating_sub(1));
let mut g2_extra_ids: Vec<Vec<ObjectId>> = Vec::with_capacity(n_pages.saturating_sub(1));
for owned in owned_pages.iter().skip(1) {
g2_page_ids.push(alloc());
g2_resources_ids.push(alloc());
g2_contents_ids.push(alloc());
let mut extras = Vec::with_capacity(owned.extra_objects.len());
for _ in 0..owned.extra_objects.len() {
extras.push(alloc());
}
g2_extra_ids.push(extras);
}
let catalog_id = alloc();
let pages_tree_id = alloc();
let info_id_opt = if has_metadata(&scene.metadata) {
Some(alloc())
} else {
None
};
let lin_param_id = alloc();
let first_page_id = alloc();
let first_page_resources_id = alloc();
let first_page_contents_id = alloc();
let mut first_page_extra_ids = Vec::with_capacity(owned_pages[0].extra_objects.len());
for _ in 0..owned_pages[0].extra_objects.len() {
first_page_extra_ids.push(alloc());
}
let hint_stream_id = alloc();
let total_ids = next_id_n;
let first_page_owned = remap_owned_page(&owned_pages[0], &first_page_extra_ids);
let mut g2_owned: Vec<OwnedPageRemapped> = Vec::with_capacity(n_pages.saturating_sub(1));
for (i, p) in owned_pages.iter().enumerate().skip(1) {
g2_owned.push(remap_owned_page(p, &g2_extra_ids[i - 1]));
}
let mut kids: Vec<Object> = Vec::with_capacity(n_pages);
kids.push(Object::Reference(first_page_id));
for id in &g2_page_ids {
kids.push(Object::Reference(*id));
}
let mut out = Vec::with_capacity(8192);
out.extend_from_slice(b"%PDF-1.5\n");
out.extend_from_slice(b"%\xE2\xE3\xCF\xD3\n");
let lin_param_off = out.len() as u64;
write!(
&mut out,
"{} 0 obj\n<< /Linearized 1 /L {:010} /H [ {:010} {:010} ] /O {} /E {:010} /N {} /T {:010} >>\nendobj\n",
lin_param_id.number,
0u64,
0u64,
0u64,
first_page_id.number,
0u64,
n_pages,
0u64,
)
.map_err(|e| PdfError::other(format!("linearize lin-dict format: {e}")))?;
let first_xref_off = out.len() as u64;
let first_group_start = catalog_id.number;
let first_group_count = total_ids - first_group_start;
out.extend_from_slice(b"xref\n");
writeln!(&mut out, "{} {}", first_group_start, first_group_count)
.map_err(|e| PdfError::other(format!("linearize first-xref header: {e}")))?;
let first_xref_entries_off = out.len() as u64;
for _ in 0..first_group_count {
out.extend_from_slice(b"0000000000 00000 n \n");
}
out.extend_from_slice(b"trailer\n");
write_first_page_trailer_dict(
&mut out,
total_ids,
catalog_id,
info_id_opt,
0,
)?;
let prev_patch_off = first_trailer_prev_offset(&out, first_xref_off as usize);
out.extend_from_slice(b"\nstartxref\n");
let first_startxref_value_off = out.len() as u64;
out.extend_from_slice(b"0000000000\n%%EOF\n");
let catalog_off = out.len() as u64;
write_indirect(
&mut out,
&IndirectObject {
id: catalog_id,
object: Object::Dict(
Dict::new()
.with("Type", Object::Name("Catalog".into()))
.with("Pages", Object::Reference(pages_tree_id)),
),
},
)?;
let pages_tree_off = out.len() as u64;
write_indirect(
&mut out,
&IndirectObject {
id: pages_tree_id,
object: Object::Dict(
Dict::new()
.with("Type", Object::Name("Pages".into()))
.with("Kids", Object::Array(kids))
.with("Count", Object::Integer(n_pages as i64)),
),
},
)?;
let info_off_opt = if let Some(info_id) = info_id_opt {
let off = out.len() as u64;
write_indirect(
&mut out,
&IndirectObject {
id: info_id,
object: Object::Dict(build_info_dict(&scene.metadata)),
},
)?;
Some(off)
} else {
None
};
let mut objects_per_page: Vec<u32> = Vec::with_capacity(n_pages);
objects_per_page.push(3 + first_page_owned.extra_objects.len() as u32);
for owned in &g2_owned {
objects_per_page.push(3 + owned.extra_objects.len() as u32);
}
let least_obj_per_page = *objects_per_page.iter().min().unwrap_or(&0);
let hint_stream_off = out.len() as u64;
let mut page_offset_table = build_page_offset_hint_table(least_obj_per_page);
let per_page_section_off = page_offset_table.len();
let per_page_section_size = n_pages * 16;
page_offset_table.resize(per_page_section_off + per_page_section_size, 0);
for (i, &count) in objects_per_page.iter().enumerate() {
let delta = count - least_obj_per_page;
let off = per_page_section_off + i * 4;
page_offset_table[off..off + 4].copy_from_slice(&delta.to_be_bytes());
}
let shared_object_table = build_shared_object_hint_table();
let thumbnail_table = build_thumbnail_hint_table();
let outline_table = build_outline_hint_table();
let s_off = page_offset_table.len();
let t_off = s_off + shared_object_table.len();
let o_off = t_off + thumbnail_table.len();
let mut hint_data = Vec::with_capacity(o_off + outline_table.len());
hint_data.extend_from_slice(&page_offset_table);
hint_data.extend_from_slice(&shared_object_table);
hint_data.extend_from_slice(&thumbnail_table);
hint_data.extend_from_slice(&outline_table);
let hint_dict = Dict::new()
.with("S", Object::Integer(s_off as i64))
.with("T", Object::Integer(t_off as i64))
.with("O", Object::Integer(o_off as i64));
let hint_obj_start = out.len();
write_indirect(
&mut out,
&IndirectObject {
id: hint_stream_id,
object: Object::Stream(Stream::new(hint_dict, hint_data)),
},
)?;
let hint_stream_end = out.len() as u64;
let hint_stream_total_len = hint_stream_end - hint_stream_off;
let hint_payload_off = {
let needle = b"\nstream\n";
let pos = out[hint_obj_start..]
.windows(needle.len())
.position(|w| w == needle)
.ok_or_else(|| PdfError::other("linearize: hint stream payload not found"))?;
hint_obj_start + pos + needle.len()
};
let first_page_off = out.len() as u64;
write_indirect(
&mut out,
&IndirectObject {
id: first_page_id,
object: Object::Dict(
Dict::new()
.with("Type", Object::Name("Page".into()))
.with("Parent", Object::Reference(pages_tree_id))
.with(
"MediaBox",
Object::Array(vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(first_page_owned.width as f64),
Object::Real(first_page_owned.height as f64),
]),
)
.with("Resources", Object::Reference(first_page_resources_id))
.with("Contents", Object::Reference(first_page_contents_id)),
),
},
)?;
let first_resources_off = out.len() as u64;
write_indirect(
&mut out,
&IndirectObject {
id: first_page_resources_id,
object: first_page_owned.resources_dict.clone(),
},
)?;
let mut first_extra_offs: Vec<u64> = Vec::with_capacity(first_page_owned.extra_objects.len());
for (i, body) in first_page_owned.extra_objects.iter().enumerate() {
first_extra_offs.push(out.len() as u64);
write_indirect(
&mut out,
&IndirectObject {
id: first_page_extra_ids[i],
object: body.clone(),
},
)?;
}
let first_contents_off = out.len() as u64;
write_indirect(
&mut out,
&IndirectObject {
id: first_page_contents_id,
object: Object::Stream(Stream::new(
Dict::new(),
first_page_owned.content_bytes.clone(),
)),
},
)?;
let end_of_first_page = out.len() as u64;
let mut g2_page_offs = Vec::with_capacity(g2_owned.len());
let mut g2_resources_offs = Vec::with_capacity(g2_owned.len());
let mut g2_contents_offs = Vec::with_capacity(g2_owned.len());
let mut g2_extra_offs: Vec<Vec<u64>> = Vec::with_capacity(g2_owned.len());
for (idx, owned) in g2_owned.iter().enumerate() {
g2_page_offs.push(out.len() as u64);
write_indirect(
&mut out,
&IndirectObject {
id: g2_page_ids[idx],
object: Object::Dict(
Dict::new()
.with("Type", Object::Name("Page".into()))
.with("Parent", Object::Reference(pages_tree_id))
.with(
"MediaBox",
Object::Array(vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(owned.width as f64),
Object::Real(owned.height as f64),
]),
)
.with("Resources", Object::Reference(g2_resources_ids[idx]))
.with("Contents", Object::Reference(g2_contents_ids[idx])),
),
},
)?;
g2_resources_offs.push(out.len() as u64);
write_indirect(
&mut out,
&IndirectObject {
id: g2_resources_ids[idx],
object: owned.resources_dict.clone(),
},
)?;
let mut extras_offs = Vec::with_capacity(owned.extra_objects.len());
for (i, body) in owned.extra_objects.iter().enumerate() {
extras_offs.push(out.len() as u64);
write_indirect(
&mut out,
&IndirectObject {
id: g2_extra_ids[idx][i],
object: body.clone(),
},
)?;
}
g2_extra_offs.push(extras_offs);
g2_contents_offs.push(out.len() as u64);
write_indirect(
&mut out,
&IndirectObject {
id: g2_contents_ids[idx],
object: Object::Stream(Stream::new(Dict::new(), owned.content_bytes.clone())),
},
)?;
}
let mut page_starts: Vec<u64> = Vec::with_capacity(n_pages);
let mut content_starts: Vec<u64> = Vec::with_capacity(n_pages);
page_starts.push(first_page_off);
content_starts.push(first_contents_off);
for (i, _) in g2_owned.iter().enumerate() {
page_starts.push(g2_page_offs[i]);
content_starts.push(g2_contents_offs[i]);
}
let mut page_ends: Vec<u64> = Vec::with_capacity(n_pages);
page_ends.push(end_of_first_page);
for i in 1..n_pages {
let end = if i + 1 < n_pages {
page_starts[i + 1]
} else {
out.len() as u64
};
page_ends.push(end);
}
let content_ends: Vec<u64> = page_ends.clone();
let page_lengths: Vec<u32> = (0..n_pages)
.map(|i| (page_ends[i] - page_starts[i]) as u32)
.collect();
let content_offsets: Vec<u32> = (0..n_pages)
.map(|i| (content_starts[i] - page_starts[i]) as u32)
.collect();
let content_lengths: Vec<u32> = (0..n_pages)
.map(|i| (content_ends[i] - content_starts[i]) as u32)
.collect();
let least_page_length = *page_lengths.iter().min().unwrap_or(&0);
let least_content_off = *content_offsets.iter().min().unwrap_or(&0);
let least_content_len = *content_lengths.iter().min().unwrap_or(&0);
let hdr = hint_payload_off;
out[hdr + 4..hdr + 8].copy_from_slice(&(first_page_off as u32).to_be_bytes());
out[hdr + 10..hdr + 14].copy_from_slice(&least_page_length.to_be_bytes());
out[hdr + 16..hdr + 20].copy_from_slice(&least_content_off.to_be_bytes());
out[hdr + 22..hdr + 26].copy_from_slice(&least_content_len.to_be_bytes());
let pp_off = hdr + per_page_section_off;
let block_size = n_pages * 4;
for (i, len) in page_lengths.iter().enumerate() {
let delta = len - least_page_length;
let off = pp_off + block_size + i * 4;
out[off..off + 4].copy_from_slice(&delta.to_be_bytes());
}
for (i, co) in content_offsets.iter().enumerate() {
let delta = co - least_content_off;
let off = pp_off + 2 * block_size + i * 4;
out[off..off + 4].copy_from_slice(&delta.to_be_bytes());
}
for (i, cl) in content_lengths.iter().enumerate() {
let delta = cl - least_content_len;
let off = pp_off + 3 * block_size + i * 4;
out[off..off + 4].copy_from_slice(&delta.to_be_bytes());
}
let main_xref_off = out.len() as u64;
out.extend_from_slice(b"xref\n");
writeln!(&mut out, "0 {}", total_ids)
.map_err(|e| PdfError::other(format!("linearize main-xref header: {e}")))?;
out.extend_from_slice(b"0000000000 65535 f \n");
let mut all_offs: Vec<u64> = vec![0; total_ids as usize];
for (i, page_id) in g2_page_ids.iter().enumerate() {
all_offs[page_id.number as usize] = g2_page_offs[i];
all_offs[g2_resources_ids[i].number as usize] = g2_resources_offs[i];
all_offs[g2_contents_ids[i].number as usize] = g2_contents_offs[i];
for (j, off) in g2_extra_offs[i].iter().enumerate() {
all_offs[g2_extra_ids[i][j].number as usize] = *off;
}
}
all_offs[catalog_id.number as usize] = catalog_off;
all_offs[pages_tree_id.number as usize] = pages_tree_off;
if let (Some(info_id), Some(off)) = (info_id_opt, info_off_opt) {
all_offs[info_id.number as usize] = off;
}
all_offs[lin_param_id.number as usize] = lin_param_off;
all_offs[first_page_id.number as usize] = first_page_off;
all_offs[first_page_resources_id.number as usize] = first_resources_off;
all_offs[first_page_contents_id.number as usize] = first_contents_off;
for (i, off) in first_extra_offs.iter().enumerate() {
all_offs[first_page_extra_ids[i].number as usize] = *off;
}
all_offs[hint_stream_id.number as usize] = hint_stream_off;
for id in 1..total_ids {
let off = all_offs[id as usize];
writeln!(&mut out, "{:010} {:05} n ", off, 0)
.map_err(|e| PdfError::other(format!("linearize main-xref entry: {e}")))?;
}
out.extend_from_slice(b"trailer\n");
let mut main_trailer = Dict::new()
.with("Size", Object::Integer(total_ids as i64))
.with("Root", Object::Reference(catalog_id));
if let Some(info_id) = info_id_opt {
main_trailer.set("Info", Object::Reference(info_id));
}
let mut main_trailer_bytes = Vec::new();
write_object_to_vec(&mut main_trailer_bytes, &Object::Dict(main_trailer))?;
out.extend_from_slice(&main_trailer_bytes);
out.extend_from_slice(b"\nstartxref\n");
writeln!(&mut out, "{}", first_xref_off)
.map_err(|e| PdfError::other(format!("linearize startxref: {e}")))?;
out.extend_from_slice(b"%%EOF\n");
let total_file_length = out.len() as u64;
patch_padded_int(&mut out, lin_param_off as usize, b"/L ", total_file_length)?;
patch_padded_int(&mut out, lin_param_off as usize, b"/H [ ", hint_stream_off)?;
{
let anchor = b"/H [ ";
let pos = find_anchor(&out, lin_param_off as usize, anchor)?;
let after_first = pos + anchor.len() + 11; write_padded_at(&mut out, after_first, hint_stream_total_len)?;
}
patch_padded_int(&mut out, lin_param_off as usize, b"/E ", end_of_first_page)?;
patch_padded_int(&mut out, lin_param_off as usize, b"/T ", main_xref_off)?;
write_padded_at(&mut out, prev_patch_off, main_xref_off)?;
write_padded_at(&mut out, first_startxref_value_off as usize, first_xref_off)?;
{
let mut entry_off = first_xref_entries_off as usize;
for id in first_group_start..(first_group_start + first_group_count) {
let off = all_offs[id as usize];
let line = format!("{:010} {:05} n \n", off, 0);
debug_assert_eq!(line.len(), 20);
out[entry_off..entry_off + 20].copy_from_slice(line.as_bytes());
entry_off += 20;
}
}
Ok(out)
}
struct RenderedOwned {
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources: ResourceCollector,
}
struct OwnedPage {
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources_dict: Object,
extra_objects: Vec<IndirectObject>,
}
struct OwnedPageRemapped {
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources_dict: Object,
extra_objects: Vec<Object>,
}
fn remap_owned_page(page: &OwnedPage, final_ids: &[ObjectId]) -> OwnedPageRemapped {
use std::collections::HashMap;
let placeholder_to_final: HashMap<u32, ObjectId> = page
.extra_objects
.iter()
.enumerate()
.map(|(i, ind)| (ind.id.number, final_ids[i]))
.collect();
let mut resources_dict = page.resources_dict.clone();
remap_object(&mut resources_dict, &placeholder_to_final);
let extras: Vec<Object> = page
.extra_objects
.iter()
.map(|ind| {
let mut body = ind.object.clone();
remap_object(&mut body, &placeholder_to_final);
body
})
.collect();
OwnedPageRemapped {
width: page.width,
height: page.height,
content_bytes: page.content_bytes.clone(),
resources_dict,
extra_objects: extras,
}
}
fn remap_object(obj: &mut Object, map: &std::collections::HashMap<u32, ObjectId>) {
match obj {
Object::Reference(id) => {
if let Some(&new_id) = map.get(&id.number) {
*id = new_id;
}
}
Object::Array(items) => {
for it in items {
remap_object(it, map);
}
}
Object::Dict(d) => {
let entries = d.entries().to_vec();
*d = Dict::new();
for (k, mut v) in entries {
remap_object(&mut v, map);
d.set(&k, v);
}
}
Object::Stream(s) => {
let entries = s.dict.entries().to_vec();
s.dict = Dict::new();
for (k, mut v) in entries {
remap_object(&mut v, map);
s.dict.set(&k, v);
}
}
_ => {}
}
}
fn write_indirect(out: &mut Vec<u8>, ind: &IndirectObject) -> Result<(), PdfError> {
writeln!(out, "{} {} obj", ind.id.number, ind.id.generation)
.map_err(|e| PdfError::other(format!("write_indirect: {e}")))?;
write_object_to_vec(out, &ind.object)?;
out.extend_from_slice(b"\nendobj\n");
Ok(())
}
fn write_object_to_vec(out: &mut Vec<u8>, obj: &Object) -> Result<(), PdfError> {
crate::objects::write_object_to(out, obj).map_err(PdfError::Io)
}
fn write_first_page_trailer_dict(
out: &mut Vec<u8>,
size: u32,
root: ObjectId,
info: Option<ObjectId>,
prev_placeholder: u64,
) -> Result<(), PdfError> {
write!(out, "<< /Size {} /Root {} 0 R", size, root.number)
.map_err(|e| PdfError::other(format!("first-trailer dict: {e}")))?;
if let Some(info_id) = info {
write!(out, " /Info {} 0 R", info_id.number)
.map_err(|e| PdfError::other(format!("first-trailer dict: {e}")))?;
}
write!(out, " /Prev {:010} >>", prev_placeholder)
.map_err(|e| PdfError::other(format!("first-trailer dict: {e}")))?;
Ok(())
}
fn first_trailer_prev_offset(out: &[u8], first_xref_section_off: usize) -> usize {
let needle = b"/Prev ";
let pos = out[first_xref_section_off..]
.windows(needle.len())
.position(|w| w == needle)
.expect("first-page trailer must carry /Prev");
first_xref_section_off + pos + needle.len()
}
fn find_anchor(hay: &[u8], start: usize, anchor: &[u8]) -> Result<usize, PdfError> {
hay[start..]
.windows(anchor.len())
.position(|w| w == anchor)
.map(|p| start + p)
.ok_or_else(|| {
PdfError::other(format!(
"linearize patch: anchor `{}` not found",
String::from_utf8_lossy(anchor)
))
})
}
fn patch_padded_int(
out: &mut [u8],
start: usize,
anchor: &[u8],
value: u64,
) -> Result<(), PdfError> {
let anchor_pos = out[start..]
.windows(anchor.len())
.position(|w| w == anchor)
.ok_or_else(|| {
PdfError::other(format!(
"linearize patch: anchor `{}` not found",
String::from_utf8_lossy(anchor)
))
})?;
write_padded_at(out, start + anchor_pos + anchor.len(), value)
}
fn write_padded_at(out: &mut [u8], at: usize, value: u64) -> Result<(), PdfError> {
if value > 9_999_999_999 {
return Err(PdfError::other(format!(
"linearize patch: value {} exceeds 10-digit width",
value
)));
}
let s = format!("{:010}", value);
if at + s.len() > out.len() {
return Err(PdfError::other("linearize patch: write past EOF"));
}
out[at..at + s.len()].copy_from_slice(s.as_bytes());
Ok(())
}
fn build_shared_object_hint_table() -> Vec<u8> {
let mut buf = Vec::with_capacity(24);
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
debug_assert_eq!(buf.len(), 24);
buf
}
fn build_thumbnail_hint_table() -> Vec<u8> {
let mut buf = Vec::with_capacity(28);
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
debug_assert_eq!(buf.len(), 28);
buf
}
fn build_outline_hint_table() -> Vec<u8> {
let mut buf = Vec::with_capacity(14);
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
debug_assert_eq!(buf.len(), 14);
buf
}
fn build_page_offset_hint_table(least_obj_per_page: u32) -> Vec<u8> {
let mut buf = Vec::with_capacity(36);
buf.extend_from_slice(&least_obj_per_page.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&32u16.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&32u16.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&32u16.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&32u16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&4u16.to_be_bytes());
debug_assert_eq!(buf.len(), 36);
buf
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::time::TimeBase;
use oxideav_core::vector::{
FillRule, Group, Node, Paint, Path, PathCommand, PathNode, Point, Rgba, VectorFrame,
};
use oxideav_scene::Page;
fn rect_frame(w: f32, h: f32, color: Rgba) -> VectorFrame {
let mut p = Path::new();
p.commands.push(PathCommand::MoveTo(Point::new(10.0, 10.0)));
p.commands
.push(PathCommand::LineTo(Point::new(w - 10.0, 10.0)));
p.commands
.push(PathCommand::LineTo(Point::new(w - 10.0, h - 10.0)));
p.commands
.push(PathCommand::LineTo(Point::new(10.0, h - 10.0)));
p.commands.push(PathCommand::Close);
VectorFrame {
width: w,
height: h,
view_box: None,
root: Group {
children: vec![Node::Path(PathNode {
path: p,
fill: Some(Paint::Solid(color)),
stroke: None,
fill_rule: FillRule::NonZero,
})],
..Group::default()
},
pts: None,
time_base: TimeBase::new(1, 1),
}
}
fn page_with(w: f32, h: f32, color: Rgba) -> Page {
let mut page = Page::new(w, h);
page.content = rect_frame(w, h, color);
page
}
fn single_page_scene() -> Scene {
Scene {
pages: Some(vec![page_with(100.0, 100.0, Rgba::opaque(255, 0, 0))]),
..Scene::default()
}
}
fn multi_page_scene() -> Scene {
Scene {
pages: Some(vec![
page_with(100.0, 100.0, Rgba::opaque(255, 0, 0)),
page_with(200.0, 150.0, Rgba::opaque(0, 255, 0)),
page_with(300.0, 200.0, Rgba::opaque(0, 0, 255)),
]),
..Scene::default()
}
}
#[test]
fn linearized_emits_pdf_1_5_header_and_marker() {
let pdf = write_pdf_linearized(&single_page_scene()).expect("linearize");
assert!(pdf.starts_with(b"%PDF-1.5\n"));
assert_eq!(&pdf[9..14], &[0x25, 0xE2, 0xE3, 0xCF, 0xD3]);
assert!(pdf.ends_with(b"%%EOF\n"));
}
fn pdf_lossy(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).into_owned()
}
#[test]
fn linearization_dict_is_within_first_1024_bytes() {
let pdf = write_pdf_linearized(&multi_page_scene()).expect("linearize");
let head = &pdf[..1024.min(pdf.len())];
let s = pdf_lossy(head);
assert!(s.contains("/Linearized 1"), "head must carry /Linearized 1");
let lin_idx = s.find("/Linearized 1").unwrap();
let close_idx = s[lin_idx..].find(">>").expect("lin-dict close");
assert!(
lin_idx + close_idx < 1024,
"lin-dict must close within first 1024 bytes"
);
}
#[test]
fn linearization_dict_carries_required_keys() {
let pdf = write_pdf_linearized(&multi_page_scene()).expect("linearize");
let s = pdf_lossy(&pdf);
for key in ["/Linearized", "/L ", "/H [", "/O ", "/E ", "/N ", "/T "] {
assert!(s.contains(key), "lin-dict missing {key:?}");
}
}
#[test]
fn linearized_l_matches_actual_file_length() {
let pdf = write_pdf_linearized(&multi_page_scene()).expect("linearize");
let actual = pdf.len();
let head = pdf_lossy(&pdf[..1024.min(pdf.len())]);
let l_idx = head.find("/L ").unwrap();
let after = &head[l_idx + 3..];
let value: u64 = after
.split_ascii_whitespace()
.next()
.unwrap()
.parse()
.unwrap();
assert_eq!(value as usize, actual, "/L must equal actual file length");
}
#[test]
fn n_matches_page_count() {
let pdf = write_pdf_linearized(&multi_page_scene()).expect("linearize");
let head = pdf_lossy(&pdf[..1024.min(pdf.len())]);
let n_idx = head.find("/N ").unwrap();
let after = &head[n_idx + 3..];
let value: u64 = after
.split_ascii_whitespace()
.next()
.unwrap()
.parse()
.unwrap();
assert_eq!(value, 3);
}
#[test]
fn startxref_points_at_first_page_xref() {
let pdf = write_pdf_linearized(&single_page_scene()).expect("linearize");
let s = pdf_lossy(&pdf);
let start_off = s.rfind("startxref\n").unwrap() + "startxref\n".len();
let line: &str = s[start_off..].split('\n').next().unwrap();
let off: usize = line.trim().parse().unwrap();
let first_xref_off = pdf
.windows(b"xref\n".len())
.position(|w| w == b"xref\n")
.unwrap();
assert_eq!(off, first_xref_off);
}
#[test]
fn first_page_trailer_carries_prev() {
let pdf = write_pdf_linearized(&single_page_scene()).expect("linearize");
let s = pdf_lossy(&pdf);
let first_trailer_off = s.find("trailer\n").unwrap();
let after = &s[first_trailer_off..];
let close_off = after.find(">>").unwrap();
let prev_off = after
.find("/Prev ")
.expect("first trailer must carry /Prev");
assert!(prev_off < close_off);
}
#[test]
fn round_trips_through_reader() {
let pdf = write_pdf_linearized(&multi_page_scene()).expect("linearize");
let scene = crate::reader::read_pdf_to_scene(&pdf).expect("reader accepts linearized");
assert_eq!(scene.pages.as_ref().unwrap().len(), 3);
}
#[test]
fn single_page_round_trips() {
let pdf = write_pdf_linearized(&single_page_scene()).expect("linearize");
let scene = crate::reader::read_pdf_to_scene(&pdf).expect("reader accepts");
assert_eq!(scene.pages.as_ref().unwrap().len(), 1);
}
#[test]
fn page_offset_hint_table_header_is_36_bytes() {
let table = build_page_offset_hint_table(3);
assert_eq!(table.len(), 36);
assert_eq!(&table[0..4], &3u32.to_be_bytes());
assert_eq!(&table[8..10], &32u16.to_be_bytes());
assert_eq!(&table[14..16], &32u16.to_be_bytes());
assert_eq!(&table[20..22], &32u16.to_be_bytes());
assert_eq!(&table[26..28], &32u16.to_be_bytes());
assert_eq!(&table[34..36], &4u16.to_be_bytes());
}
#[test]
fn page_offset_per_page_section_size_is_16_bytes_per_page() {
let pdf = write_pdf_linearized(&multi_page_scene()).expect("linearize");
let s = pdf_lossy(&pdf);
let hint_dict_start = s.find("/S ").expect("hint dict /S");
let dict_open = s[..hint_dict_start].rfind("<<").unwrap();
let dict_close = s[dict_open..].find(">>").unwrap() + dict_open;
let dict_str = &s[dict_open..dict_close];
let len_idx = dict_str.find("/Length ").expect("hint dict /Length");
let after = &dict_str[len_idx + "/Length ".len()..];
let value: usize = after
.split_ascii_whitespace()
.next()
.unwrap()
.parse()
.unwrap();
let n = 3usize;
assert_eq!(value, 36 + n * 16 + 24 + 28 + 14);
}
#[test]
fn page_offset_per_page_first_page_object_count_matches() {
let pdf = write_pdf_linearized(&single_page_scene()).expect("linearize");
let after_stream = pdf
.windows(b"\nstream\n".len())
.position(|w| w == b"\nstream\n")
.unwrap()
+ b"\nstream\n".len();
let item1_p0 = u32::from_be_bytes(
pdf[after_stream + 36..after_stream + 40]
.try_into()
.unwrap(),
);
assert_eq!(item1_p0, 0, "single-page item-1 delta = count - least = 0");
}
#[test]
fn shared_object_hint_table_is_24_bytes_zero() {
let table = build_shared_object_hint_table();
assert_eq!(table.len(), 24);
assert!(table.iter().all(|&b| b == 0));
}
#[test]
fn thumbnail_hint_table_is_28_bytes_zero() {
let table = build_thumbnail_hint_table();
assert_eq!(table.len(), 28);
assert!(table.iter().all(|&b| b == 0));
}
#[test]
fn outline_hint_table_is_14_bytes_zero() {
let table = build_outline_hint_table();
assert_eq!(table.len(), 14);
assert!(table.iter().all(|&b| b == 0));
}
#[test]
fn hint_stream_dict_carries_s_t_o_offsets() {
let pdf = write_pdf_linearized(&single_page_scene()).expect("linearize");
let s = pdf_lossy(&pdf);
assert!(s.contains("/S "), "hint dict must carry /S");
assert!(s.contains("/T "), "hint dict must carry /T (thumbnail)");
assert!(s.contains("/O "), "hint dict must carry /O (outline)");
}
}