use oxideav_core::vector::{FillRule, Group, ImageRef, Node, PathNode, VectorFrame};
use oxideav_scene::Scene;
use crate::encrypt::{EncryptionConfig, EncryptionState};
use crate::error::PdfError;
use crate::info::{build_info_dict, has_metadata};
use crate::objects::{Dict, Document, Object, Stream};
use crate::operators::{
concat_matrix, emit_clip_marker, emit_path, paint, restore, save, set_ext_gstate,
set_fill_paint, set_stroke_style, OpBuf, PaintMode,
};
use crate::page::{build_page, build_pages, PageInput};
use crate::reader::xref::{find_startxref_offset, parse_xref};
use crate::resources::ResourceCollector;
pub fn write_pdf(frame: &VectorFrame) -> Result<Vec<u8>, PdfError> {
let (content, resources) = render_frame(frame);
let mut doc = Document::new();
let _ = build_page(&mut doc, frame, content, &resources);
let mut out = Vec::with_capacity(2048);
doc.write_to(&mut out)?;
Ok(out)
}
pub fn write_pdf_from_scene(scene: &Scene) -> Result<Vec<u8>, PdfError> {
let pages = scene
.pages
.as_ref()
.filter(|p| !p.is_empty())
.ok_or_else(|| {
PdfError::other(
"write_pdf_from_scene: scene is not in pages mode (scene.pages is None or empty)",
)
})?;
struct Rendered<'a> {
frame: &'a VectorFrame,
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources: ResourceCollector,
}
let rendered: Vec<Rendered<'_>> = pages
.iter()
.map(|page| {
let (content_bytes, resources) = render_frame(&page.content);
Rendered {
frame: &page.content,
width: page.width,
height: page.height,
content_bytes,
resources,
}
})
.collect();
let inputs: Vec<PageInput<'_>> = rendered
.into_iter()
.map(|r| PageInput {
width: r.width,
height: r.height,
content_bytes: r.content_bytes,
resources: r.resources,
frame: r.frame,
})
.collect();
let mut doc = Document::new();
let _ = build_pages(&mut doc, inputs);
if has_metadata(&scene.metadata) {
let info_id = doc.add(Object::Dict(build_info_dict(&scene.metadata)));
doc.info = Some(info_id);
}
let mut out = Vec::with_capacity(4096);
doc.write_to(&mut out)?;
Ok(out)
}
pub fn write_pdf_from_scene_with_xmp(scene: &Scene, xmp_bytes: &[u8]) -> Result<Vec<u8>, PdfError> {
let pages = scene
.pages
.as_ref()
.filter(|p| !p.is_empty())
.ok_or_else(|| {
PdfError::other(
"write_pdf_from_scene_with_xmp: scene is not in pages mode (scene.pages is None or empty)",
)
})?;
struct Rendered<'a> {
frame: &'a VectorFrame,
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources: ResourceCollector,
}
let rendered: Vec<Rendered<'_>> = pages
.iter()
.map(|page| {
let (content_bytes, resources) = render_frame(&page.content);
Rendered {
frame: &page.content,
width: page.width,
height: page.height,
content_bytes,
resources,
}
})
.collect();
let inputs: Vec<PageInput<'_>> = rendered
.into_iter()
.map(|r| PageInput {
width: r.width,
height: r.height,
content_bytes: r.content_bytes,
resources: r.resources,
frame: r.frame,
})
.collect();
let mut doc = Document::new();
let pages_build = build_pages(&mut doc, inputs);
if has_metadata(&scene.metadata) {
let info_id = doc.add(Object::Dict(build_info_dict(&scene.metadata)));
doc.info = Some(info_id);
}
let metadata_dict = Dict::new()
.with("Type", Object::Name("Metadata".into()))
.with("Subtype", Object::Name("XML".into()));
let metadata_id = doc.add(Object::Stream(Stream::new(
metadata_dict,
xmp_bytes.to_vec(),
)));
let catalog = doc.object_mut(pages_build.catalog_id).ok_or_else(|| {
PdfError::other("write_pdf_from_scene_with_xmp: catalog id missing after build_pages")
})?;
if let Object::Dict(d) = catalog {
d.set("Metadata", Object::Reference(metadata_id));
} else {
return Err(PdfError::other(
"write_pdf_from_scene_with_xmp: catalog object is not a Dict",
));
}
let mut out = Vec::with_capacity(4096 + xmp_bytes.len());
doc.write_to(&mut out)?;
Ok(out)
}
pub fn write_pdf_from_scene_xref_stream(scene: &Scene) -> Result<Vec<u8>, PdfError> {
let pages = scene
.pages
.as_ref()
.filter(|p| !p.is_empty())
.ok_or_else(|| {
PdfError::other(
"write_pdf_from_scene_xref_stream: scene is not in pages mode (scene.pages is None or empty)",
)
})?;
struct Rendered<'a> {
frame: &'a VectorFrame,
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources: ResourceCollector,
}
let rendered: Vec<Rendered<'_>> = pages
.iter()
.map(|page| {
let (content_bytes, resources) = render_frame(&page.content);
Rendered {
frame: &page.content,
width: page.width,
height: page.height,
content_bytes,
resources,
}
})
.collect();
let inputs: Vec<PageInput<'_>> = rendered
.into_iter()
.map(|r| PageInput {
width: r.width,
height: r.height,
content_bytes: r.content_bytes,
resources: r.resources,
frame: r.frame,
})
.collect();
let mut doc = Document::new();
let _ = build_pages(&mut doc, inputs);
if has_metadata(&scene.metadata) {
let info_id = doc.add(Object::Dict(build_info_dict(&scene.metadata)));
doc.info = Some(info_id);
}
doc.xref_stream = true;
let mut out = Vec::with_capacity(4096);
doc.write_to(&mut out)?;
Ok(out)
}
pub fn write_pdf_from_scene_object_stream(scene: &Scene) -> Result<Vec<u8>, PdfError> {
let pages = scene
.pages
.as_ref()
.filter(|p| !p.is_empty())
.ok_or_else(|| {
PdfError::other(
"write_pdf_from_scene_object_stream: scene is not in pages mode (scene.pages is None or empty)",
)
})?;
struct Rendered<'a> {
frame: &'a VectorFrame,
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources: ResourceCollector,
}
let rendered: Vec<Rendered<'_>> = pages
.iter()
.map(|page| {
let (content_bytes, resources) = render_frame(&page.content);
Rendered {
frame: &page.content,
width: page.width,
height: page.height,
content_bytes,
resources,
}
})
.collect();
let inputs: Vec<PageInput<'_>> = rendered
.into_iter()
.map(|r| PageInput {
width: r.width,
height: r.height,
content_bytes: r.content_bytes,
resources: r.resources,
frame: r.frame,
})
.collect();
let mut doc = Document::new();
let _ = build_pages(&mut doc, inputs);
if has_metadata(&scene.metadata) {
let info_id = doc.add(Object::Dict(build_info_dict(&scene.metadata)));
doc.info = Some(info_id);
}
doc.xref_stream = true;
doc.object_stream = true;
let mut out = Vec::with_capacity(4096);
doc.write_to(&mut out)?;
Ok(out)
}
pub fn write_pdf_incremental_update(
prev_pdf: &[u8],
additional_pages: &[oxideav_scene::Page],
) -> Result<Vec<u8>, PdfError> {
let prev_xref_off = find_startxref_offset(prev_pdf)?;
let prev_table = parse_xref(prev_pdf)?;
let prev_root = prev_table.root()?;
let prev_size_from_trailer = prev_table
.trailer
.entries()
.iter()
.find(|(k, _)| k == "Size")
.and_then(|(_, v)| match v {
Object::Integer(n) if *n >= 0 => Some(*n as u32),
_ => None,
});
let prev_max_id = prev_table.entries.keys().copied().max().unwrap_or(0);
let prev_size = prev_size_from_trailer.unwrap_or(prev_max_id + 1);
let mut reader = crate::reader::document::DocumentReader::open(prev_pdf)?;
let catalog = reader.resolve(prev_root)?;
let Object::Dict(catalog_dict) = catalog else {
return Err(PdfError::other(
"write_pdf_incremental_update: previous /Root is not a Dict",
));
};
let pages_tree_id = match catalog_dict
.entries()
.iter()
.find(|(k, _)| k == "Pages")
.map(|(_, v)| v)
{
Some(Object::Reference(id)) => *id,
_ => {
return Err(PdfError::other(
"write_pdf_incremental_update: previous catalog missing /Pages reference",
))
}
};
let pages_tree = reader.resolve(pages_tree_id)?;
let Object::Dict(pages_tree_dict) = pages_tree else {
return Err(PdfError::other(
"write_pdf_incremental_update: previous /Pages tree is not a Dict",
));
};
let prev_kids: Vec<Object> = pages_tree_dict
.entries()
.iter()
.find(|(k, _)| k == "Kids")
.and_then(|(_, v)| match v {
Object::Array(items) => Some(items.clone()),
_ => None,
})
.unwrap_or_default();
let prev_count = match pages_tree_dict
.entries()
.iter()
.find(|(k, _)| k == "Count")
.map(|(_, v)| v)
{
Some(Object::Integer(n)) => *n,
_ => prev_kids.len() as i64,
};
let mut doc = Document::new();
doc.set_next_id(prev_max_id + 1);
struct Rendered {
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources: ResourceCollector,
}
let rendered: Vec<Rendered> = additional_pages
.iter()
.map(|page| {
let (content_bytes, resources) = render_frame(&page.content);
Rendered {
width: page.width,
height: page.height,
content_bytes,
resources,
}
})
.collect();
let mut new_kids = prev_kids.clone();
let mut changed_ids: Vec<u32> = Vec::new();
for r in rendered {
let page_id = doc.allocate_id();
let resources_id = doc.allocate_id();
let contents_id = doc.allocate_id();
let media_box = Object::Array(vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(r.width as f64),
Object::Real(r.height as f64),
]);
doc.add_object(
page_id,
Object::Dict(
Dict::new()
.with("Type", Object::Name("Page".into()))
.with("Parent", Object::Reference(pages_tree_id))
.with("MediaBox", media_box)
.with("Resources", Object::Reference(resources_id))
.with("Contents", Object::Reference(contents_id)),
),
);
let res_obj = r.resources.flatten_into_resources_dict(&mut doc);
doc.add_object(resources_id, res_obj);
doc.add_object(
contents_id,
Object::Stream(Stream::new(Dict::new(), r.content_bytes)),
);
new_kids.push(Object::Reference(page_id));
changed_ids.push(page_id.number);
changed_ids.push(resources_id.number);
changed_ids.push(contents_id.number);
}
doc.add_object(
pages_tree_id,
Object::Dict(
Dict::new()
.with("Type", Object::Name("Pages".into()))
.with("Kids", Object::Array(new_kids))
.with(
"Count",
Object::Integer(prev_count + additional_pages.len() as i64),
),
),
);
changed_ids.push(pages_tree_id.number);
doc.root = Some(prev_root);
doc.prev_xref_offset = Some(prev_xref_off);
doc.min_size = Some(prev_size);
let mut all_changed_ids = changed_ids.clone();
all_changed_ids.sort_unstable();
all_changed_ids.dedup();
doc.xref_only_ids = Some(all_changed_ids);
let mut out = prev_pdf.to_vec();
if !out.ends_with(b"\n") {
out.push(b'\n');
}
doc.write_to(&mut out)?;
Ok(out)
}
pub fn write_pdf_from_scene_object_stream_encrypted(
scene: &Scene,
config: &EncryptionConfig,
) -> Result<Vec<u8>, PdfError> {
let pages = scene
.pages
.as_ref()
.filter(|p| !p.is_empty())
.ok_or_else(|| {
PdfError::other(
"write_pdf_from_scene_object_stream_encrypted: scene is not in pages mode (scene.pages is None or empty)",
)
})?;
struct Rendered<'a> {
frame: &'a VectorFrame,
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources: ResourceCollector,
}
let rendered: Vec<Rendered<'_>> = pages
.iter()
.map(|page| {
let (content_bytes, resources) = render_frame(&page.content);
Rendered {
frame: &page.content,
width: page.width,
height: page.height,
content_bytes,
resources,
}
})
.collect();
let inputs: Vec<PageInput<'_>> = rendered
.into_iter()
.map(|r| PageInput {
width: r.width,
height: r.height,
content_bytes: r.content_bytes,
resources: r.resources,
frame: r.frame,
})
.collect();
let mut doc = Document::new();
let _ = build_pages(&mut doc, inputs);
if has_metadata(&scene.metadata) {
let info_id = doc.add(Object::Dict(build_info_dict(&scene.metadata)));
doc.info = Some(info_id);
}
doc.xref_stream = true;
doc.object_stream = true;
doc.encryption = Some(EncryptionState::build(config)?);
let mut out = Vec::with_capacity(4096);
doc.write_to(&mut out)?;
Ok(out)
}
pub fn write_pdf_from_scene_encrypted(
scene: &Scene,
config: &EncryptionConfig,
) -> Result<Vec<u8>, PdfError> {
let pages = scene
.pages
.as_ref()
.filter(|p| !p.is_empty())
.ok_or_else(|| {
PdfError::other(
"write_pdf_from_scene_encrypted: scene is not in pages mode (scene.pages is None or empty)",
)
})?;
struct Rendered<'a> {
frame: &'a VectorFrame,
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources: ResourceCollector,
}
let rendered: Vec<Rendered<'_>> = pages
.iter()
.map(|page| {
let (content_bytes, resources) = render_frame(&page.content);
Rendered {
frame: &page.content,
width: page.width,
height: page.height,
content_bytes,
resources,
}
})
.collect();
let inputs: Vec<PageInput<'_>> = rendered
.into_iter()
.map(|r| PageInput {
width: r.width,
height: r.height,
content_bytes: r.content_bytes,
resources: r.resources,
frame: r.frame,
})
.collect();
let mut doc = Document::new();
let _ = build_pages(&mut doc, inputs);
if has_metadata(&scene.metadata) {
let info_id = doc.add(Object::Dict(build_info_dict(&scene.metadata)));
doc.info = Some(info_id);
}
doc.encryption = Some(EncryptionState::build(config)?);
let mut out = Vec::with_capacity(4096);
doc.write_to(&mut out)?;
Ok(out)
}
pub fn write_pdf_from_scene_pubsec_encrypted(
scene: &Scene,
config: &crate::pubsec::PubSecEncoderConfig,
) -> Result<Vec<u8>, PdfError> {
let pages = scene
.pages
.as_ref()
.filter(|p| !p.is_empty())
.ok_or_else(|| {
PdfError::other(
"write_pdf_from_scene_pubsec_encrypted: scene is not in pages mode (scene.pages is None or empty)",
)
})?;
struct Rendered<'a> {
frame: &'a VectorFrame,
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources: ResourceCollector,
}
let rendered: Vec<Rendered<'_>> = pages
.iter()
.map(|page| {
let (content_bytes, resources) = render_frame(&page.content);
Rendered {
frame: &page.content,
width: page.width,
height: page.height,
content_bytes,
resources,
}
})
.collect();
let inputs: Vec<PageInput<'_>> = rendered
.into_iter()
.map(|r| PageInput {
width: r.width,
height: r.height,
content_bytes: r.content_bytes,
resources: r.resources,
frame: r.frame,
})
.collect();
let mut doc = Document::new();
let _ = build_pages(&mut doc, inputs);
if has_metadata(&scene.metadata) {
let info_id = doc.add(Object::Dict(build_info_dict(&scene.metadata)));
doc.info = Some(info_id);
}
let pubsec_state = crate::pubsec::PubSecEncryptionState::build(config)?;
doc.encryption = Some(pubsec_state.into_encryption_state());
let mut out = Vec::with_capacity(4096);
doc.write_to(&mut out)?;
Ok(out)
}
pub fn write_pdf_from_scene_pubsec_multi_cf(
scene: &Scene,
config: crate::pubsec::PubSecMultiCfConfig,
) -> Result<Vec<u8>, PdfError> {
let pages = scene
.pages
.as_ref()
.filter(|p| !p.is_empty())
.ok_or_else(|| {
PdfError::other(
"write_pdf_from_scene_pubsec_multi_cf: scene is not in pages mode (scene.pages is None or empty)",
)
})?;
struct Rendered<'a> {
frame: &'a VectorFrame,
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources: ResourceCollector,
}
let rendered: Vec<Rendered<'_>> = pages
.iter()
.map(|page| {
let (content_bytes, resources) = render_frame(&page.content);
Rendered {
frame: &page.content,
width: page.width,
height: page.height,
content_bytes,
resources,
}
})
.collect();
let inputs: Vec<PageInput<'_>> = rendered
.into_iter()
.map(|r| PageInput {
width: r.width,
height: r.height,
content_bytes: r.content_bytes,
resources: r.resources,
frame: r.frame,
})
.collect();
let mut doc = Document::new();
let _ = build_pages(&mut doc, inputs);
if has_metadata(&scene.metadata) {
let info_id = doc.add(Object::Dict(build_info_dict(&scene.metadata)));
doc.info = Some(info_id);
}
let pubsec_state = config.build()?;
doc.encryption = Some(pubsec_state.into_encryption_state());
let mut out = Vec::with_capacity(4096);
doc.write_to(&mut out)?;
Ok(out)
}
pub fn write_pdf_from_scene_pubsec_kari(
scene: &Scene,
config: &crate::pubsec::PubSecKariConfig,
) -> Result<Vec<u8>, PdfError> {
let pages = scene
.pages
.as_ref()
.filter(|p| !p.is_empty())
.ok_or_else(|| {
PdfError::other(
"write_pdf_from_scene_pubsec_kari: scene is not in pages mode (scene.pages is None or empty)",
)
})?;
struct Rendered<'a> {
frame: &'a VectorFrame,
width: f32,
height: f32,
content_bytes: Vec<u8>,
resources: ResourceCollector,
}
let rendered: Vec<Rendered<'_>> = pages
.iter()
.map(|page| {
let (content_bytes, resources) = render_frame(&page.content);
Rendered {
frame: &page.content,
width: page.width,
height: page.height,
content_bytes,
resources,
}
})
.collect();
let inputs: Vec<PageInput<'_>> = rendered
.into_iter()
.map(|r| PageInput {
width: r.width,
height: r.height,
content_bytes: r.content_bytes,
resources: r.resources,
frame: r.frame,
})
.collect();
let mut doc = Document::new();
let _ = build_pages(&mut doc, inputs);
if has_metadata(&scene.metadata) {
let info_id = doc.add(Object::Dict(build_info_dict(&scene.metadata)));
doc.info = Some(info_id);
}
let pubsec_state = crate::pubsec::PubSecEncryptionState::build_kari(config)?;
doc.encryption = Some(pubsec_state.into_encryption_state());
let mut out = Vec::with_capacity(4096);
doc.write_to(&mut out)?;
Ok(out)
}
fn render_frame(frame: &VectorFrame) -> (Vec<u8>, ResourceCollector) {
let mut op = OpBuf::new();
let mut resources = ResourceCollector::new();
emit_group(&mut op, &frame.root, &mut resources);
(op.into_bytes(), resources)
}
pub(crate) fn render_frame_for_linearize(frame: &VectorFrame) -> (Vec<u8>, ResourceCollector) {
render_frame(frame)
}
pub fn write_pdf_from_scene_linearized(scene: &Scene) -> Result<Vec<u8>, PdfError> {
crate::linearize::write_pdf_linearized(scene)
}
fn emit_group(op: &mut OpBuf, group: &Group, resources: &mut ResourceCollector) {
save(op);
if !group.transform.is_identity() {
concat_matrix(op, &group.transform);
}
if (group.opacity - 1.0).abs() > 1e-6 {
let name = resources.add_opacity(group.opacity);
set_ext_gstate(op, &name);
}
if let Some(clip) = &group.clip {
emit_path(op, clip);
emit_clip_marker(op, FillRule::NonZero);
}
for child in &group.children {
emit_node(op, child, resources);
}
restore(op);
}
fn emit_node(op: &mut OpBuf, node: &Node, resources: &mut ResourceCollector) {
match node {
Node::Group(g) => emit_group(op, g, resources),
Node::Path(p) => emit_path_node(op, p, resources),
Node::Image(img) => emit_image(op, img, resources),
_ => {}
}
}
fn emit_path_node(op: &mut OpBuf, node: &PathNode, resources: &mut ResourceCollector) {
if node.path.commands.is_empty() {
return;
}
if node.fill.is_none() && node.stroke.is_none() {
return;
}
save(op);
if let Some(stroke) = &node.stroke {
set_stroke_style(op, stroke, resources);
}
if let Some(fill) = &node.fill {
set_fill_paint(op, fill, resources);
}
emit_path(op, &node.path);
let mode = match (node.fill.is_some(), node.stroke.is_some()) {
(true, true) => PaintMode::FillStroke,
(true, false) => PaintMode::Fill,
(false, true) => PaintMode::Stroke,
(false, false) => PaintMode::None,
};
paint(op, mode, node.fill_rule);
restore(op);
}
fn emit_image(op: &mut OpBuf, image: &ImageRef, resources: &mut ResourceCollector) {
let width = image.bounds.width.round() as u32;
let height = image.bounds.height.round() as u32;
let Some(name) = resources.add_rgba_image(&image.frame, width, height) else {
return;
};
save(op);
if !image.transform.is_identity() {
concat_matrix(op, &image.transform);
}
let bx = image.bounds.x;
let by = image.bounds.y;
let bw = image.bounds.width;
let bh = image.bounds.height;
let ctm = oxideav_core::vector::Transform2D {
a: bw,
b: 0.0,
c: 0.0,
d: -bh,
e: bx,
f: by + bh,
};
concat_matrix(op, &ctm);
let mut bytes = Vec::with_capacity(name.len() + 5);
bytes.push(b'/');
bytes.extend_from_slice(name.as_bytes());
bytes.extend_from_slice(b" Do\n");
op.append_raw(&bytes);
restore(op);
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::time::TimeBase;
use oxideav_core::vector::{Group, Paint, Path, PathCommand, Point, Rgba};
fn rect_frame(width: f32, height: 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(width - 10.0, 10.0)));
p.commands
.push(PathCommand::LineTo(Point::new(width - 10.0, height - 10.0)));
p.commands
.push(PathCommand::LineTo(Point::new(10.0, height - 10.0)));
p.commands.push(PathCommand::Close);
VectorFrame {
width,
height,
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),
}
}
#[test]
fn write_pdf_starts_with_header_and_ends_with_eof() {
let frame = rect_frame(200.0, 100.0, Rgba::opaque(255, 128, 0));
let bytes = write_pdf(&frame).unwrap();
assert!(bytes.starts_with(b"%PDF-1.4\n"));
assert!(bytes.ends_with(b"%%EOF\n"));
}
#[test]
fn write_pdf_emits_basic_path_operators() {
let frame = rect_frame(200.0, 100.0, Rgba::opaque(0, 0, 0));
let bytes = write_pdf(&frame).unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains(" m\n"));
assert!(s.contains(" l\n"));
assert!(s.contains("h\n"));
assert!(s.contains("f\n"));
}
#[test]
fn empty_root_still_produces_valid_skeleton() {
let frame = VectorFrame {
width: 10.0,
height: 10.0,
view_box: None,
root: Group::default(),
pts: None,
time_base: TimeBase::new(1, 1),
};
let bytes = write_pdf(&frame).unwrap();
assert!(bytes.starts_with(b"%PDF-1.4\n"));
assert!(bytes.ends_with(b"%%EOF\n"));
}
}