use std::collections::HashMap;
use oxideav_core::vector::{
FillRule, Group, Node, Paint, Path, PathCommand, PathNode, Rgba, VectorFrame,
};
use oxideav_core::TimeBase;
use oxideav_scene::{Metadata, Page, Scene};
use crate::decrypt::{open_with_password, StandardHandler};
use crate::error::PdfError;
use crate::objects::{Dict, Object, ObjectId, Stream};
use crate::pubsec::{
open_with_certificate, open_with_certificate_and_trust_store, PubSecCredential, TrustStore,
};
use crate::reader::content::parse_content_stream;
use crate::reader::parse::Parser;
use crate::reader::xref::{parse_xref, XrefEntry, XrefTable};
pub struct DocumentReader<'a> {
input: &'a [u8],
xref: XrefTable,
cache: HashMap<ObjectId, Object>,
crypt: Option<StandardHandler>,
}
impl<'a> DocumentReader<'a> {
pub fn open(input: &'a [u8]) -> Result<Self, PdfError> {
Self::open_with_password(input, b"")
}
pub fn open_with_password(input: &'a [u8], password: &[u8]) -> Result<Self, PdfError> {
let xref = parse_xref(input)?;
let crypt = build_crypt(&xref, input, password)?;
Ok(Self {
input,
xref,
cache: HashMap::new(),
crypt,
})
}
pub fn open_with_certificate(
input: &'a [u8],
credential: &PubSecCredential,
) -> Result<Self, PdfError> {
let xref = parse_xref(input)?;
let crypt = build_crypt_pubsec(&xref, input, credential, None)?;
Ok(Self {
input,
xref,
cache: HashMap::new(),
crypt,
})
}
pub fn open_with_certificate_and_trust_store(
input: &'a [u8],
credential: &PubSecCredential,
trust_store: &TrustStore,
) -> Result<Self, PdfError> {
let xref = parse_xref(input)?;
let crypt = build_crypt_pubsec(&xref, input, credential, Some(trust_store))?;
Ok(Self {
input,
xref,
cache: HashMap::new(),
crypt,
})
}
pub fn xref(&self) -> &XrefTable {
&self.xref
}
pub fn is_encrypted(&self) -> bool {
self.crypt.is_some()
}
pub fn signatures(&mut self) -> Result<Vec<crate::reader::sig::PdfSignature>, PdfError> {
crate::reader::sig::signatures(self)
}
pub fn doc_timestamps(&mut self) -> Result<Vec<crate::reader::sig::PdfDocTimestamp>, PdfError> {
crate::reader::sig::doc_timestamps(self)
}
pub fn annotations(
&mut self,
) -> Result<Vec<crate::reader::annotation::PdfAnnotation>, PdfError> {
crate::reader::annotation::annotations(self)
}
pub fn actions(&mut self) -> Result<Vec<crate::reader::actions::PdfAction>, PdfError> {
crate::reader::actions::actions(self)
}
pub fn optional_content(
&mut self,
) -> Result<Option<crate::reader::ocg::OptionalContent>, PdfError> {
crate::reader::ocg::optional_content(self)
}
pub fn linearization(
&self,
) -> Result<Option<crate::reader::linearize::LinearizationParams>, PdfError> {
crate::reader::linearize::LinearizationParams::parse(self.input)
}
pub fn verify_hierarchy(
&mut self,
) -> Result<crate::reader::hierarchy::HierarchyReport, PdfError> {
crate::reader::hierarchy::verify_hierarchy(self)
}
pub fn pdfa_signals(&mut self) -> Result<crate::reader::pdfa::PdfACatalogSignals, PdfError> {
crate::reader::pdfa::pdfa_signals(self)
}
pub fn pdfa_conformance(&mut self) -> Result<crate::reader::pdfa::PdfAConformance, PdfError> {
let signals = self.pdfa_signals()?;
let xmp = self.xmp_packet()?;
Ok(crate::reader::pdfa::PdfAConformance::from_signals_and_xmp(
&signals,
xmp.as_ref(),
))
}
pub fn xmp_packet(&mut self) -> Result<Option<crate::reader::xmp::XmpPacket>, PdfError> {
Ok(self
.xmp_metadata()?
.as_deref()
.map(crate::reader::xmp::XmpPacket::parse))
}
pub fn xmp_metadata(&mut self) -> Result<Option<Vec<u8>>, PdfError> {
let root_id = self.xref.root()?;
let catalog = self.resolve(root_id)?;
let Object::Dict(catalog) = catalog else {
return Err(PdfError::other(format!(
"PDF reader: /Root must be a dictionary (got {catalog:?})"
)));
};
let metadata_obj = catalog
.entries()
.iter()
.find(|(k, _)| k == "Metadata")
.map(|(_, v)| v.clone());
let Some(metadata_obj) = metadata_obj else {
return Ok(None);
};
let stream_obj = match metadata_obj {
Object::Reference(id) => self.resolve(id)?,
other => other,
};
let Object::Stream(s) = stream_obj else {
return Err(PdfError::other(format!(
"PDF reader: catalog /Metadata must resolve to a Stream (got {stream_obj:?})"
)));
};
Ok(Some(decode_stream(&s)?))
}
pub fn resolve(&mut self, id: ObjectId) -> Result<Object, PdfError> {
if let Some(o) = self.cache.get(&id) {
return Ok(o.clone());
}
if let Some(XrefEntry::Compressed {
obj_stream_id,
index_within_stream,
}) = self.xref.entries.get(&id.number).copied()
{
let body = self.resolve_compressed(id, obj_stream_id, index_within_stream)?;
self.cache.insert(id, body.clone());
return Ok(body);
}
let off = self
.xref
.offset_of(id)
.ok_or_else(|| PdfError::other(format!("PDF reader: object {id:?} not in xref")))?;
let mut p = Parser::new(self.input);
p.lexer_mut().seek(off as usize);
let input = self.input;
let xref_snapshot = &self.xref;
let id_being_parsed = id;
let mut resolver = move |length_ref_id: ObjectId| -> Result<i64, PdfError> {
resolve_indirect_length(input, xref_snapshot, length_ref_id, id_being_parsed)
};
let (parsed_id, mut body) = p.parse_indirect_with_length_resolver(&mut resolver)?;
if parsed_id != id {
return Err(PdfError::other(format!(
"PDF reader: xref points to wrong object — wanted {id:?}, got {parsed_id:?}"
)));
}
if let Some(crypt) = &self.crypt {
decrypt_object_in_place(&mut body, id, crypt)?;
}
self.cache.insert(id, body.clone());
Ok(body)
}
fn resolve_compressed(
&mut self,
wanted: ObjectId,
obj_stream_num: u32,
index_within_stream: u32,
) -> Result<Object, PdfError> {
let container_id = ObjectId::new(obj_stream_num);
let container = self.resolve(container_id)?;
let Object::Stream(s) = container else {
return Err(PdfError::other(format!(
"PDF reader: object {wanted:?} expected its container {container_id:?} to be a Stream"
)));
};
let dict = &s.dict;
let lookup = |k: &str| {
dict.entries()
.iter()
.find(|(kk, _)| kk == k)
.map(|(_, v)| v.clone())
};
if !matches!(lookup("Type"), Some(Object::Name(ref n)) if n == "ObjStm") {
return Err(PdfError::other(format!(
"PDF reader: container {container_id:?} is not /Type /ObjStm"
)));
}
let n = match lookup("N") {
Some(Object::Integer(v)) if v >= 0 => v as u32,
other => {
return Err(PdfError::other(format!(
"PDF reader: ObjStm /N must be a non-negative integer (got {other:?})"
)))
}
};
let first = match lookup("First") {
Some(Object::Integer(v)) if v >= 0 => v as usize,
other => {
return Err(PdfError::other(format!(
"PDF reader: ObjStm /First must be a non-negative integer (got {other:?})"
)))
}
};
if index_within_stream >= n {
return Err(PdfError::other(format!(
"PDF reader: ObjStm index {index_within_stream} out of range (N={n})"
)));
}
let payload = decode_stream(&s)?;
if first > payload.len() {
return Err(PdfError::other(format!(
"PDF reader: ObjStm /First {first} exceeds payload length {}",
payload.len()
)));
}
let header_bytes = &payload[..first];
let mut hp = Parser::new(header_bytes);
let mut pairs: Vec<(u32, usize)> = Vec::with_capacity(n as usize);
for i in 0..n {
let on = hp.parse_object()?.ok_or_else(|| {
PdfError::other(format!(
"PDF reader: ObjStm header truncated at pair {i} (obj_num)"
))
})?;
let off = hp.parse_object()?.ok_or_else(|| {
PdfError::other(format!(
"PDF reader: ObjStm header truncated at pair {i} (offset)"
))
})?;
let (Object::Integer(num), Object::Integer(o)) = (on, off) else {
return Err(PdfError::other(format!(
"PDF reader: ObjStm header pair {i} must be two integers"
)));
};
if num < 1 || o < 0 {
return Err(PdfError::other(format!(
"PDF reader: ObjStm header pair {i} out of range ({num}, {o})"
)));
}
pairs.push((num as u32, o as usize));
}
let (header_num, body_off) = pairs[index_within_stream as usize];
if header_num != wanted.number {
return Err(PdfError::other(format!(
"PDF reader: ObjStm slot {index_within_stream} declares object {header_num},\
but xref expected {}",
wanted.number
)));
}
let abs_off = first
.checked_add(body_off)
.ok_or_else(|| PdfError::other("PDF reader: ObjStm offset overflow"))?;
if abs_off > payload.len() {
return Err(PdfError::other(format!(
"PDF reader: ObjStm body offset {abs_off} past payload length {}",
payload.len()
)));
}
let mut bp = Parser::new(&payload[abs_off..]);
let body = bp
.parse_object()?
.ok_or_else(|| PdfError::other("PDF reader: ObjStm body parse returned EOF"))?;
Ok(body)
}
pub fn deref(&mut self, obj: Object) -> Result<Object, PdfError> {
let mut cur = obj;
let mut hops = 0;
while let Object::Reference(id) = cur {
cur = self.resolve(id)?;
hops += 1;
if hops > 16 {
return Err(PdfError::other(
"PDF reader: indirect-reference chain too deep (>16 hops)",
));
}
}
Ok(cur)
}
}
fn resolve_indirect_length(
input: &[u8],
xref: &XrefTable,
length_ref: ObjectId,
containing_stream: ObjectId,
) -> Result<i64, PdfError> {
if length_ref == containing_stream {
return Err(PdfError::other(format!(
"PDF reader: stream {containing_stream:?} /Length refers to itself"
)));
}
if let Some(XrefEntry::Compressed { .. }) = xref.entries.get(&length_ref.number) {
return Err(PdfError::other(format!(
"PDF reader: indirect /Length {length_ref:?} lives in an object stream \
— not supported (ISO 32000-1 §7.5.7); the length object should be a \
direct uncompressed integer"
)));
}
let off = xref.offset_of(length_ref).ok_or_else(|| {
PdfError::other(format!(
"PDF reader: indirect /Length {length_ref:?} (for stream \
{containing_stream:?}) is not in the xref table"
))
})?;
let mut p = Parser::new(input);
p.lexer_mut().seek(off as usize);
let (parsed_id, body) = p.parse_indirect()?;
if parsed_id != length_ref {
return Err(PdfError::other(format!(
"PDF reader: xref points to wrong object for indirect /Length — \
wanted {length_ref:?}, got {parsed_id:?}"
)));
}
match body {
Object::Integer(n) => Ok(n),
other => Err(PdfError::other(format!(
"PDF reader: indirect /Length {length_ref:?} target must be an \
integer (got {other:?})"
))),
}
}
fn build_crypt(
xref: &XrefTable,
input: &[u8],
password: &[u8],
) -> Result<Option<StandardHandler>, PdfError> {
let encrypt_ref = xref.trailer.entries().iter().find(|(k, _)| k == "Encrypt");
let Some((_, encrypt_obj)) = encrypt_ref else {
return Ok(None);
};
let encrypt_dict = match encrypt_obj {
Object::Dict(d) => d.clone(),
Object::Reference(id) => {
let off = xref
.offset_of(*id)
.ok_or_else(|| PdfError::other("PDF reader: /Encrypt refers to missing object"))?;
let mut p = Parser::new(input);
p.lexer_mut().seek(off as usize);
let (_, body) = p.parse_indirect()?;
match body {
Object::Dict(d) => d,
other => {
return Err(PdfError::other(format!(
"PDF reader: /Encrypt must resolve to a dict (got {other:?})"
)))
}
}
}
other => {
return Err(PdfError::other(format!(
"PDF reader: /Encrypt must be a dict or reference (got {other:?})"
)))
}
};
let id_obj = xref
.trailer
.entries()
.iter()
.find(|(k, _)| k == "ID")
.map(|(_, v)| v.clone())
.ok_or_else(|| PdfError::other("PDF reader: encrypted PDF missing /ID in trailer"))?;
let Object::Array(id_items) = id_obj else {
return Err(PdfError::other("PDF reader: /ID must be an array"));
};
if id_items.is_empty() {
return Err(PdfError::other("PDF reader: trailer /ID array is empty"));
}
let file_id = match &id_items[0] {
Object::LiteralString(s) | Object::HexString(s) => s.clone(),
other => {
return Err(PdfError::other(format!(
"PDF reader: /ID[0] must be a string (got {other:?})"
)))
}
};
let handler = open_with_password(&encrypt_dict, &file_id, password)?;
handler
.ok_or_else(|| {
PdfError::other("PDF reader: wrong password (or PDF requires owner password)")
})
.map(Some)
}
fn build_crypt_pubsec(
xref: &XrefTable,
input: &[u8],
credential: &PubSecCredential,
trust_store: Option<&TrustStore>,
) -> Result<Option<StandardHandler>, PdfError> {
let encrypt_ref = xref.trailer.entries().iter().find(|(k, _)| k == "Encrypt");
let Some((_, encrypt_obj)) = encrypt_ref else {
return Ok(None);
};
let encrypt_dict = match encrypt_obj {
Object::Dict(d) => d.clone(),
Object::Reference(id) => {
let off = xref
.offset_of(*id)
.ok_or_else(|| PdfError::other("PDF reader: /Encrypt refers to missing object"))?;
let mut p = Parser::new(input);
p.lexer_mut().seek(off as usize);
let (_, body) = p.parse_indirect()?;
match body {
Object::Dict(d) => d,
other => {
return Err(PdfError::other(format!(
"PDF reader: /Encrypt must resolve to a dict (got {other:?})"
)))
}
}
}
other => {
return Err(PdfError::other(format!(
"PDF reader: /Encrypt must be a dict or reference (got {other:?})"
)))
}
};
let handler = match trust_store {
Some(store) => open_with_certificate_and_trust_store(&encrypt_dict, credential, store)?,
None => open_with_certificate(&encrypt_dict, credential)?,
};
handler
.ok_or_else(|| {
PdfError::other(
"PDF reader: certificate did not match any recipient in /Recipients (round-10)",
)
})
.map(Some)
}
fn decrypt_object_in_place(
obj: &mut Object,
id: ObjectId,
crypt: &StandardHandler,
) -> Result<(), PdfError> {
match obj {
Object::LiteralString(s) | Object::HexString(s) => {
*s = crypt.decrypt_object(id, s)?;
}
Object::Array(items) => {
for item in items {
decrypt_object_in_place(item, id, crypt)?;
}
}
Object::Dict(d) => {
decrypt_dict_in_place(d, id, crypt)?;
}
Object::Stream(s) => {
decrypt_dict_in_place(&mut s.dict, id, crypt)?;
if has_identity_crypt_filter(&s.dict) {
return Ok(());
}
s.data = crypt.decrypt_object(id, &s.data)?;
}
_ => {}
}
Ok(())
}
fn has_identity_crypt_filter(dict: &Dict) -> bool {
let filter = dict
.entries()
.iter()
.find(|(k, _)| k == "Filter")
.map(|(_, v)| v);
let parms = dict
.entries()
.iter()
.find(|(k, _)| k == "DecodeParms")
.map(|(_, v)| v);
let crypt_pos: Option<usize> = match filter {
Some(Object::Name(s)) if s == "Crypt" => Some(0),
Some(Object::Array(items)) => items
.iter()
.position(|f| matches!(f, Object::Name(n) if n == "Crypt")),
_ => None,
};
let Some(idx) = crypt_pos else {
return false;
};
let parms_dict = match parms {
Some(Object::Dict(d)) if idx == 0 => Some(d.clone()),
Some(Object::Array(items)) => match items.get(idx) {
Some(Object::Dict(d)) => Some(d.clone()),
_ => None,
},
_ => None,
};
let Some(d) = parms_dict else {
return true;
};
match d
.entries()
.iter()
.find(|(k, _)| k == "Name")
.map(|(_, v)| v)
{
Some(Object::Name(s)) => s == "Identity",
None => true,
_ => false,
}
}
fn decrypt_dict_in_place(
d: &mut Dict,
id: ObjectId,
crypt: &StandardHandler,
) -> Result<(), PdfError> {
let mut new_entries: Vec<(String, Object)> = Vec::with_capacity(d.entries().len());
for (k, v) in d.entries() {
let mut v = v.clone();
decrypt_object_in_place(&mut v, id, crypt)?;
new_entries.push((k.clone(), v));
}
*d = Dict::default();
for (k, v) in new_entries {
d.set(&k, v);
}
Ok(())
}
pub fn read_pdf_to_scene(input: &[u8]) -> Result<Scene, PdfError> {
read_pdf_to_scene_with_password(input, b"")
}
pub fn read_pdf_to_scene_with_password(input: &[u8], password: &[u8]) -> Result<Scene, PdfError> {
let reader = DocumentReader::open_with_password(input, password)?;
decode_to_scene(reader)
}
pub fn read_pdf_to_scene_with_certificate(
input: &[u8],
credential: &PubSecCredential,
) -> Result<Scene, PdfError> {
let reader = DocumentReader::open_with_certificate(input, credential)?;
decode_to_scene(reader)
}
pub fn read_pdf_to_scene_with_certificate_and_trust_store(
input: &[u8],
credential: &PubSecCredential,
trust_store: &TrustStore,
) -> Result<Scene, PdfError> {
let reader =
DocumentReader::open_with_certificate_and_trust_store(input, credential, trust_store)?;
decode_to_scene(reader)
}
fn decode_to_scene(mut reader: DocumentReader<'_>) -> Result<Scene, PdfError> {
let root_id = reader.xref.root()?;
let catalog = reader.resolve(root_id)?;
let Object::Dict(catalog) = catalog else {
return Err(PdfError::other(format!(
"PDF reader: /Root must be a dictionary (got {catalog:?})"
)));
};
let pages_ref = catalog
.entries()
.iter()
.find(|(k, _)| k == "Pages")
.map(|(_, v)| v.clone())
.ok_or_else(|| PdfError::other("PDF reader: catalog missing /Pages"))?;
let Object::Reference(pages_root_id) = pages_ref else {
return Err(PdfError::other(format!(
"PDF reader: catalog /Pages must be an indirect reference (got {pages_ref:?})"
)));
};
let mut leaves = Vec::new();
walk_pages_tree(&mut reader, pages_root_id, &mut leaves)?;
if leaves.is_empty() {
return Err(PdfError::other(
"PDF reader: /Pages tree contained no Page leaves",
));
}
let mut scene_pages = Vec::with_capacity(leaves.len());
for leaf_id in leaves {
scene_pages.push(decode_page(&mut reader, leaf_id)?);
}
let metadata = if let Some(info_id) = reader.xref.info() {
let info = reader.resolve(info_id)?;
decode_metadata(info)?
} else {
Metadata::default()
};
Ok(Scene {
pages: Some(scene_pages),
metadata,
..Scene::default()
})
}
fn walk_pages_tree(
reader: &mut DocumentReader<'_>,
node_id: ObjectId,
out: &mut Vec<ObjectId>,
) -> Result<(), PdfError> {
let node = reader.resolve(node_id)?;
let Object::Dict(d) = node else {
return Err(PdfError::other(format!(
"PDF reader: pages-tree node {node_id:?} is not a dict"
)));
};
let kind = d
.entries()
.iter()
.find(|(k, _)| k == "Type")
.and_then(|(_, v)| match v {
Object::Name(s) => Some(s.as_str()),
_ => None,
});
match kind {
Some("Page") => {
out.push(node_id);
Ok(())
}
Some("Pages") => {
let kids = d
.entries()
.iter()
.find(|(k, _)| k == "Kids")
.map(|(_, v)| v.clone())
.ok_or_else(|| {
PdfError::other(format!("PDF reader: /Pages node {node_id:?} missing /Kids"))
})?;
let Object::Array(items) = kids else {
return Err(PdfError::other(format!(
"PDF reader: /Kids must be an array on {node_id:?}"
)));
};
for item in items {
if let Object::Reference(id) = item {
walk_pages_tree(reader, id, out)?;
}
}
Ok(())
}
_ => Err(PdfError::other(format!(
"PDF reader: pages-tree node {node_id:?} has unknown /Type {kind:?}"
))),
}
}
fn decode_page(reader: &mut DocumentReader<'_>, page_id: ObjectId) -> Result<Page, PdfError> {
let page_obj = reader.resolve(page_id)?;
let Object::Dict(page_dict) = page_obj else {
return Err(PdfError::other(format!(
"PDF reader: page {page_id:?} is not a dict"
)));
};
let media_box = page_dict
.entries()
.iter()
.find(|(k, _)| k == "MediaBox")
.map(|(_, v)| v.clone());
let (width, height) = match media_box {
Some(Object::Array(items)) if items.len() == 4 => {
let llx = number_to_f32(&items[0])?;
let lly = number_to_f32(&items[1])?;
let urx = number_to_f32(&items[2])?;
let ury = number_to_f32(&items[3])?;
((urx - llx).abs(), (ury - lly).abs())
}
Some(other) => {
return Err(PdfError::other(format!(
"PDF reader: /MediaBox must be a 4-array (got {other:?})"
)));
}
None => {
(595.0, 842.0)
}
};
let contents_obj = page_dict
.entries()
.iter()
.find(|(k, _)| k == "Contents")
.map(|(_, v)| v.clone());
let content_bytes = match contents_obj {
Some(Object::Reference(id)) => extract_stream_data(reader, id)?,
Some(Object::Array(items)) => {
let mut all = Vec::new();
for item in items {
if let Object::Reference(id) = item {
all.extend_from_slice(&extract_stream_data(reader, id)?);
all.push(b'\n');
}
}
all
}
Some(other) => {
return Err(PdfError::other(format!(
"PDF reader: /Contents must be a Stream or array (got {other:?})"
)));
}
None => Vec::new(),
};
let root = parse_content_stream(&content_bytes)?;
let mut page = Page::new(width, height);
page.content = VectorFrame {
width,
height,
view_box: None,
root,
pts: None,
time_base: TimeBase::new(1, 1),
};
Ok(page)
}
fn extract_stream_data(reader: &mut DocumentReader<'_>, id: ObjectId) -> Result<Vec<u8>, PdfError> {
let obj = reader.resolve(id)?;
let Object::Stream(s) = obj else {
return Err(PdfError::other(format!(
"PDF reader: object {id:?} expected to be a Stream (got {obj:?})"
)));
};
decode_stream(&s)
}
pub fn decode_stream(stream: &Stream) -> Result<Vec<u8>, PdfError> {
let filter = stream
.dict
.entries()
.iter()
.find(|(k, _)| k == "Filter")
.map(|(_, v)| v.clone());
let parms = stream
.dict
.entries()
.iter()
.find(|(k, _)| k == "DecodeParms" || k == "DP")
.map(|(_, v)| v.clone());
match filter {
None => Ok(stream.data.clone()),
Some(Object::Name(name)) => apply_filter(&name, &stream.data, parms_for_index(&parms, 0)),
Some(Object::Array(items)) => {
let mut data = stream.data.clone();
for (idx, item) in items.iter().enumerate() {
let Object::Name(name) = item else {
return Err(PdfError::other(format!(
"PDF reader: /Filter chain item must be a Name (got {item:?})"
)));
};
data = apply_filter(name, &data, parms_for_index(&parms, idx))?;
}
Ok(data)
}
Some(other) => Err(PdfError::other(format!(
"PDF reader: /Filter must be a Name or array of Names (got {other:?})"
))),
}
}
fn parms_for_index(parms: &Option<Object>, idx: usize) -> Option<&Dict> {
match parms {
Some(Object::Dict(d)) if idx == 0 => Some(d),
Some(Object::Array(items)) => match items.get(idx) {
Some(Object::Dict(d)) => Some(d),
_ => None,
},
_ => None,
}
}
fn apply_filter(name: &str, data: &[u8], parms: Option<&Dict>) -> Result<Vec<u8>, PdfError> {
use crate::reader::filters;
match name {
"FlateDecode" | "Fl" => apply_predictor_post(filters::flate_decompress(data)?, parms),
"LZWDecode" | "LZW" => {
let early = parms
.and_then(|d| d.entries().iter().find(|(k, _)| k == "EarlyChange"))
.and_then(|(_, v)| match v {
Object::Integer(n) => Some(*n != 0),
_ => None,
})
.unwrap_or(true);
apply_predictor_post(filters::lzw_decode_with_early_change(data, early)?, parms)
}
"ASCII85Decode" | "A85" => filters::ascii85_decode(data),
"ASCIIHexDecode" | "AHx" => filters::ascii_hex_decode(data),
"RunLengthDecode" | "RL" => filters::run_length_decode(data),
other => Err(PdfError::other(format!(
"PDF reader: filter `{other}` not yet supported by decode_stream \
(image codec filters DCT/JPX/JBIG2/CCITTFax route through the image walkers)"
))),
}
}
fn apply_predictor_post(data: Vec<u8>, parms: Option<&Dict>) -> Result<Vec<u8>, PdfError> {
use crate::reader::filters::{apply_predictor, PredictorParams};
let Some(parms) = parms else {
return Ok(data);
};
let int = |key: &str| -> Option<i64> {
parms
.entries()
.iter()
.find(|(k, _)| k == key)
.and_then(|(_, v)| match v {
Object::Integer(n) => Some(*n),
_ => None,
})
};
let predictor = int("Predictor").unwrap_or(1);
if predictor <= 1 {
return Ok(data);
}
let def = PredictorParams::default();
let params = PredictorParams {
predictor,
colors: int("Colors")
.map(|n| n.max(0) as usize)
.unwrap_or(def.colors),
bits_per_component: int("BitsPerComponent")
.map(|n| n.max(0) as usize)
.unwrap_or(def.bits_per_component),
columns: int("Columns")
.map(|n| n.max(0) as usize)
.unwrap_or(def.columns),
};
apply_predictor(&data, ¶ms)
}
fn decode_metadata(info: Object) -> Result<Metadata, PdfError> {
let Object::Dict(d) = info else {
return Err(PdfError::other(format!(
"PDF reader: /Info must be a dict (got {info:?})"
)));
};
let mut m = Metadata::default();
for (k, v) in d.entries() {
match k.as_str() {
"Title" => m.title = decode_text(v),
"Author" => m.author = decode_text(v),
"Subject" => m.subject = decode_text(v),
"Keywords" => {
if let Some(s) = decode_text(v) {
m.keywords = s.split(',').map(|p| p.trim().to_owned()).collect();
}
}
"Creator" => m.creator = decode_text(v),
"Producer" => m.producer = decode_text(v),
"CreationDate" => m.created_at = decode_text(v).map(pdf_date_to_iso8601),
"ModDate" => m.modified_at = decode_text(v).map(pdf_date_to_iso8601),
other => {
if let Some(s) = decode_text(v) {
m.custom.insert(other.to_owned(), s);
}
}
}
}
Ok(m)
}
pub fn pdf_date_to_iso8601(s: String) -> String {
let bytes = s.as_bytes();
if !bytes.starts_with(b"D:") {
return s;
}
let rest = &bytes[2..];
if rest.len() < 4 {
return s.clone();
}
let mut out = String::with_capacity(25);
let year = &rest[0..4.min(rest.len())];
out.push_str(&String::from_utf8_lossy(year));
if rest.len() >= 6 {
out.push('-');
out.push_str(&String::from_utf8_lossy(&rest[4..6]));
}
if rest.len() >= 8 {
out.push('-');
out.push_str(&String::from_utf8_lossy(&rest[6..8]));
}
if rest.len() >= 10 {
out.push('T');
out.push_str(&String::from_utf8_lossy(&rest[8..10]));
}
if rest.len() >= 12 {
out.push(':');
out.push_str(&String::from_utf8_lossy(&rest[10..12]));
}
if rest.len() >= 14 {
out.push(':');
out.push_str(&String::from_utf8_lossy(&rest[12..14]));
}
if rest.len() == 15 && rest[14] == b'Z' {
out.push('Z');
} else if rest.len() >= 17 && (rest[14] == b'+' || rest[14] == b'-') {
out.push(rest[14] as char);
out.push_str(&String::from_utf8_lossy(&rest[15..17]));
if rest.len() >= 20 && rest[17] == b'\'' {
out.push(':');
out.push_str(&String::from_utf8_lossy(&rest[18..20]));
}
}
out
}
fn decode_text(v: &Object) -> Option<String> {
match v {
Object::LiteralString(b) => Some(String::from_utf8_lossy(b).into_owned()),
Object::HexString(b) => {
if b.len() >= 2 && b[0] == 0xFE && b[1] == 0xFF {
let utf16: Vec<u16> = b[2..]
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
Some(String::from_utf16_lossy(&utf16))
} else {
Some(String::from_utf8_lossy(b).into_owned())
}
}
_ => None,
}
}
fn number_to_f32(o: &Object) -> Result<f32, PdfError> {
match o {
Object::Integer(n) => Ok(*n as f32),
Object::Real(f) => Ok(*f as f32),
other => Err(PdfError::other(format!(
"PDF reader: expected number, got {other:?}"
))),
}
}
#[allow(dead_code)]
fn empty_root() -> Group {
Group::default()
}
#[allow(dead_code)]
fn empty_path_node() -> PathNode {
PathNode {
path: Path {
commands: vec![PathCommand::Close],
},
fill: Some(Paint::Solid(Rgba::opaque(0, 0, 0))),
stroke: None,
fill_rule: FillRule::NonZero,
}
}
#[allow(dead_code)]
fn _node_imported(_: Node) {}
#[cfg(test)]
mod tests {
use super::*;
use crate::write_pdf_from_scene;
fn make_scene_with_one_red_rect() -> Scene {
use oxideav_core::vector::{
FillRule, Group, Node, Paint, Path, PathCommand, PathNode, Point, 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(90.0, 10.0)));
p.commands.push(PathCommand::LineTo(Point::new(90.0, 90.0)));
p.commands.push(PathCommand::LineTo(Point::new(10.0, 90.0)));
p.commands.push(PathCommand::Close);
let frame = VectorFrame {
width: 100.0,
height: 100.0,
view_box: None,
root: Group {
children: vec![Node::Path(PathNode {
path: p,
fill: Some(Paint::Solid(Rgba::opaque(255, 0, 0))),
stroke: None,
fill_rule: FillRule::NonZero,
})],
..Group::default()
},
pts: None,
time_base: TimeBase::new(1, 1),
};
let mut page = Page::new(100.0, 100.0);
page.content = frame;
Scene {
pages: Some(vec![page]),
..Scene::default()
}
}
#[test]
fn read_pdf_to_scene_roundtrip_single_page() {
let scene = make_scene_with_one_red_rect();
let pdf = write_pdf_from_scene(&scene).unwrap();
let parsed = read_pdf_to_scene(&pdf).unwrap();
let pages = parsed.pages.expect("scene has pages");
assert_eq!(pages.len(), 1);
assert_eq!(pages[0].width, 100.0);
assert_eq!(pages[0].height, 100.0);
let root = &pages[0].content.root;
let path_node = find_first_path(root).expect("at least one PathNode in the tree");
match &path_node.fill {
Some(Paint::Solid(rgba)) => assert_eq!((rgba.r, rgba.g, rgba.b), (255, 0, 0)),
other => panic!("expected solid red, got {other:?}"),
}
}
fn find_first_path(group: &Group) -> Option<&PathNode> {
for child in &group.children {
match child {
Node::Path(p) => return Some(p),
Node::Group(g) => {
if let Some(p) = find_first_path(g) {
return Some(p);
}
}
_ => {}
}
}
None
}
#[test]
fn read_pdf_to_scene_roundtrip_multi_page() {
use oxideav_core::vector::Rgba;
let mut scene = make_scene_with_one_red_rect();
let mut p2 = Page::new(200.0, 100.0);
p2.content.width = 200.0;
p2.content.height = 100.0;
use oxideav_core::vector::{Group, Node, Paint, Path, PathCommand, PathNode, Point};
let mut path = Path::new();
path.commands
.push(PathCommand::MoveTo(Point::new(0.0, 0.0)));
path.commands
.push(PathCommand::LineTo(Point::new(50.0, 0.0)));
path.commands
.push(PathCommand::LineTo(Point::new(50.0, 50.0)));
path.commands.push(PathCommand::Close);
p2.content.root = Group {
children: vec![Node::Path(PathNode {
path,
fill: Some(Paint::Solid(Rgba::opaque(0, 255, 0))),
stroke: None,
fill_rule: FillRule::NonZero,
})],
..Group::default()
};
scene.pages.as_mut().unwrap().push(p2);
let pdf = write_pdf_from_scene(&scene).unwrap();
let parsed = read_pdf_to_scene(&pdf).unwrap();
let pages = parsed.pages.expect("scene has pages");
assert_eq!(pages.len(), 2);
assert_eq!(pages[0].width, 100.0);
assert_eq!(pages[1].width, 200.0);
}
#[test]
fn read_pdf_metadata_roundtrip() {
let mut scene = make_scene_with_one_red_rect();
scene.metadata = Metadata {
title: Some("Round 3 Doc".into()),
author: Some("Mark".into()),
subject: Some("Reader test".into()),
keywords: vec!["pdf".into(), "round3".into()],
creator: Some("MyApp".into()),
producer: Some("oxideav-pdf".into()),
created_at: Some("2026-05-04T12:30:45Z".into()),
modified_at: Some("2026-05-04T13:00:00Z".into()),
..Metadata::default()
};
let pdf = write_pdf_from_scene(&scene).unwrap();
let parsed = read_pdf_to_scene(&pdf).unwrap();
assert_eq!(parsed.metadata.title.as_deref(), Some("Round 3 Doc"));
assert_eq!(parsed.metadata.author.as_deref(), Some("Mark"));
assert_eq!(parsed.metadata.subject.as_deref(), Some("Reader test"));
assert_eq!(parsed.metadata.creator.as_deref(), Some("MyApp"));
assert_eq!(parsed.metadata.producer.as_deref(), Some("oxideav-pdf"));
assert_eq!(
parsed.metadata.keywords,
vec!["pdf".to_string(), "round3".to_string()]
);
assert_eq!(
parsed.metadata.created_at.as_deref(),
Some("2026-05-04T12:30:45Z")
);
}
#[test]
fn read_pdf_custom_metadata_roundtrip() {
let mut scene = make_scene_with_one_red_rect();
let mut custom = std::collections::BTreeMap::new();
custom.insert("dc:rights".into(), "(c) 2026 Karpeles".into());
custom.insert("Trapped".into(), "False".into());
scene.metadata = Metadata {
custom,
..Metadata::default()
};
let pdf = write_pdf_from_scene(&scene).unwrap();
let parsed = read_pdf_to_scene(&pdf).unwrap();
assert_eq!(
parsed.metadata.custom.get("dc:rights").map(String::as_str),
Some("(c) 2026 Karpeles")
);
assert_eq!(
parsed.metadata.custom.get("Trapped").map(String::as_str),
Some("False")
);
}
#[test]
fn read_pdf_unicode_metadata_roundtrip() {
let mut scene = make_scene_with_one_red_rect();
scene.metadata = Metadata {
title: Some("日本語".into()),
..Metadata::default()
};
let pdf = write_pdf_from_scene(&scene).unwrap();
let parsed = read_pdf_to_scene(&pdf).unwrap();
assert_eq!(parsed.metadata.title.as_deref(), Some("日本語"));
}
#[test]
fn pdf_date_to_iso8601_format() {
assert_eq!(
pdf_date_to_iso8601("D:20260504123045Z".to_string()),
"2026-05-04T12:30:45Z"
);
assert_eq!(
pdf_date_to_iso8601("D:20260504123045+09'00'".to_string()),
"2026-05-04T12:30:45+09:00"
);
}
#[test]
fn no_metadata_yields_default() {
let scene = make_scene_with_one_red_rect();
let pdf = write_pdf_from_scene(&scene).unwrap();
let parsed = read_pdf_to_scene(&pdf).unwrap();
assert!(parsed.metadata.title.is_none());
assert!(parsed.metadata.custom.is_empty());
}
#[test]
fn decode_stream_applies_flate_png_predictor() {
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::io::Write;
let predicted: &[u8] = &[
0, 10, 20, 30, 2, 1, 2, 3, ];
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(predicted).unwrap();
let compressed = enc.finish().unwrap();
let dict = Dict::new()
.with("Filter", Object::Name("FlateDecode".into()))
.with(
"DecodeParms",
Object::Dict(
Dict::new()
.with("Predictor", Object::Integer(12))
.with("Columns", Object::Integer(3)),
),
);
let stream = Stream::new(dict, compressed);
let out = decode_stream(&stream).unwrap();
assert_eq!(out, [10u8, 20, 30, 11, 22, 33]);
}
#[test]
fn decode_stream_flate_without_predictor_is_passthrough() {
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::io::Write;
let raw = b"hello predictor-free world";
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(raw).unwrap();
let compressed = enc.finish().unwrap();
let dict = Dict::new().with("Filter", Object::Name("FlateDecode".into()));
let stream = Stream::new(dict, compressed);
assert_eq!(decode_stream(&stream).unwrap(), raw);
}
}