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, ObjectId, 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::outline::{LinkAnnotationSpec, LinkTarget, OutlineDestination, OutlineSpec};
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_with_outlines(
scene: &Scene,
outlines: &[OutlineSpec],
) -> Result<Vec<u8>, PdfError> {
write_pdf_from_scene_with_outlines_and_links(scene, outlines, &[])
}
pub fn write_pdf_from_scene_with_outlines_and_links(
scene: &Scene,
outlines: &[OutlineSpec],
links: &[LinkAnnotationSpec],
) -> 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_outlines: 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 n_pages = pages_build.page_ids.len();
validate_outlines(outlines, n_pages)?;
for link in links {
if link.source_page_index >= n_pages {
return Err(PdfError::other(format!(
"write_pdf_from_scene_with_outlines: link annotation source_page_index {} \
out of range (scene has {} page(s))",
link.source_page_index, n_pages
)));
}
if let LinkTarget::Internal(dest) = &link.target {
if dest.page_index() >= n_pages {
return Err(PdfError::other(format!(
"write_pdf_from_scene_with_outlines: link annotation destination page_index {} \
out of range (scene has {} page(s))",
dest.page_index(),
n_pages
)));
}
}
}
if !outlines.is_empty() {
let outline_root_id = doc.allocate_id();
let (first_id, last_id, top_visible) =
emit_outline_level(&mut doc, outlines, outline_root_id, &pages_build.page_ids);
let root_dict = Dict::new()
.with("Type", Object::Name("Outlines".into()))
.with("First", Object::Reference(first_id))
.with("Last", Object::Reference(last_id))
.with("Count", Object::Integer(top_visible as i64));
doc.add_object(outline_root_id, Object::Dict(root_dict));
let catalog = doc.object_mut(pages_build.catalog_id).ok_or_else(|| {
PdfError::other(
"write_pdf_from_scene_with_outlines: catalog id missing after build_pages",
)
})?;
if let Object::Dict(d) = catalog {
d.set("Outlines", Object::Reference(outline_root_id));
} else {
return Err(PdfError::other(
"write_pdf_from_scene_with_outlines: catalog object is not a Dict",
));
}
}
if !links.is_empty() {
let mut by_page: Vec<Vec<&LinkAnnotationSpec>> = (0..n_pages).map(|_| Vec::new()).collect();
for link in links {
by_page[link.source_page_index].push(link);
}
for (page_idx, page_links) in by_page.iter().enumerate() {
if page_links.is_empty() {
continue;
}
let page_id = pages_build.page_ids[page_idx];
let annot_refs: Vec<Object> = page_links
.iter()
.map(|link| {
let dict = build_link_annot_dict(link, &pages_build.page_ids);
let id = doc.add(Object::Dict(dict));
Object::Reference(id)
})
.collect();
let page_obj = doc.object_mut(page_id).ok_or_else(|| {
PdfError::other(
"write_pdf_from_scene_with_outlines: page id missing after build_pages",
)
})?;
if let Object::Dict(d) = page_obj {
d.set("Annots", Object::Array(annot_refs));
} else {
return Err(PdfError::other(
"write_pdf_from_scene_with_outlines: page object is not a Dict",
));
}
}
}
let mut out = Vec::with_capacity(4096);
doc.write_to(&mut out)?;
Ok(out)
}
fn validate_outlines(outlines: &[OutlineSpec], n_pages: usize) -> Result<(), PdfError> {
for o in outlines {
if o.destination.page_index() >= n_pages {
return Err(PdfError::other(format!(
"write_pdf_from_scene_with_outlines: outline `{}` destination page_index {} \
out of range (scene has {} page(s))",
o.title,
o.destination.page_index(),
n_pages
)));
}
validate_outlines(&o.children, n_pages)?;
}
Ok(())
}
fn emit_outline_level(
doc: &mut Document,
siblings: &[OutlineSpec],
parent_id: ObjectId,
page_ids: &[ObjectId],
) -> (ObjectId, ObjectId, usize) {
let sibling_ids: Vec<ObjectId> = siblings.iter().map(|_| doc.allocate_id()).collect();
let mut visible_at_this_level = siblings.len();
for (i, spec) in siblings.iter().enumerate() {
let my_id = sibling_ids[i];
let mut item = Dict::new()
.with("Title", outline_text_string(&spec.title))
.with("Parent", Object::Reference(parent_id))
.with("Dest", build_destination_array(&spec.destination, page_ids));
if i > 0 {
item.set("Prev", Object::Reference(sibling_ids[i - 1]));
}
if i + 1 < sibling_ids.len() {
item.set("Next", Object::Reference(sibling_ids[i + 1]));
}
if !spec.children.is_empty() {
let (child_first, child_last, child_visible) =
emit_outline_level(doc, &spec.children, my_id, page_ids);
item.set("First", Object::Reference(child_first));
item.set("Last", Object::Reference(child_last));
let signed = if spec.open {
child_visible as i64
} else {
-(child_visible as i64)
};
item.set("Count", Object::Integer(signed));
if spec.open {
visible_at_this_level += child_visible;
}
}
doc.add_object(my_id, Object::Dict(item));
}
(
sibling_ids[0],
*sibling_ids.last().unwrap(),
visible_at_this_level,
)
}
fn build_destination_array(dest: &OutlineDestination, page_ids: &[ObjectId]) -> Object {
let page_ref = Object::Reference(page_ids[dest.page_index()]);
let opt_num = |v: Option<f32>| match v {
Some(n) => Object::Real(n as f64),
None => Object::Null,
};
match *dest {
OutlineDestination::Xyz {
left, top, zoom, ..
} => Object::Array(vec![
page_ref,
Object::Name("XYZ".into()),
opt_num(left),
opt_num(top),
opt_num(zoom),
]),
OutlineDestination::Fit { .. } => Object::Array(vec![page_ref, Object::Name("Fit".into())]),
OutlineDestination::FitH { top, .. } => {
Object::Array(vec![page_ref, Object::Name("FitH".into()), opt_num(top)])
}
OutlineDestination::FitV { left, .. } => {
Object::Array(vec![page_ref, Object::Name("FitV".into()), opt_num(left)])
}
OutlineDestination::FitR {
left,
bottom,
right,
top,
..
} => Object::Array(vec![
page_ref,
Object::Name("FitR".into()),
Object::Real(left as f64),
Object::Real(bottom as f64),
Object::Real(right as f64),
Object::Real(top as f64),
]),
OutlineDestination::FitB { .. } => {
Object::Array(vec![page_ref, Object::Name("FitB".into())])
}
OutlineDestination::FitBH { top, .. } => {
Object::Array(vec![page_ref, Object::Name("FitBH".into()), opt_num(top)])
}
OutlineDestination::FitBV { left, .. } => {
Object::Array(vec![page_ref, Object::Name("FitBV".into()), opt_num(left)])
}
}
}
fn build_link_annot_dict(link: &LinkAnnotationSpec, page_ids: &[ObjectId]) -> Dict {
let rect = Object::Array(link.rect.iter().map(|v| Object::Real(*v as f64)).collect());
let mut dict = Dict::new()
.with("Type", Object::Name("Annot".into()))
.with("Subtype", Object::Name("Link".into()))
.with("Rect", rect)
.with(
"Border",
Object::Array(vec![
Object::Integer(0),
Object::Integer(0),
Object::Integer(0),
]),
);
match &link.target {
LinkTarget::Internal(dest) => {
dict.set("Dest", build_destination_array(dest, page_ids));
}
LinkTarget::Uri(uri) => {
let action = Dict::new()
.with("Type", Object::Name("Action".into()))
.with("S", Object::Name("URI".into()))
.with("URI", Object::LiteralString(uri.as_bytes().to_vec()));
dict.set("A", Object::Dict(action));
}
}
dict
}
fn outline_text_string(s: &str) -> Object {
if s.bytes().all(|b| b.is_ascii() && b != 0) {
Object::LiteralString(s.as_bytes().to_vec())
} else {
let mut bytes = vec![0xFE, 0xFF];
for cp in s.encode_utf16() {
bytes.push((cp >> 8) as u8);
bytes.push((cp & 0xFF) as u8);
}
Object::HexString(bytes)
}
}
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"));
}
}