1pub mod annot_appearance;
2pub mod annotation;
3mod catalog;
4pub mod embedded_files;
5pub mod font_loader;
6pub mod forms;
7pub mod optional_content;
8pub mod output_intents;
9pub mod page;
10
11pub use annotation::Annotation;
12pub use catalog::Catalog;
13pub use embedded_files::{EmbeddedFile, EmbeddedSource};
14pub use forms::{AcroForm, FieldKind, FieldValue, FormField};
15pub use optional_content::OcConfig;
16pub use output_intents::OutputIntent;
17pub use page::{PdfPage, ResourceDict};
18
19use std::sync::{Arc, OnceLock};
20use zpdf_core::{Error, ParseLimits, Result};
21use zpdf_font::FontCache;
22use zpdf_parser::PdfFile;
23
24pub struct PdfDocument {
25 file: PdfFile,
26 catalog: Catalog,
27 acro_form: OnceLock<Option<AcroForm>>,
30}
31
32impl PdfDocument {
33 pub fn open(data: impl Into<Arc<[u8]>>) -> Result<Self> {
34 Self::open_with_limits(data, ParseLimits::default())
35 }
36
37 pub fn open_with_limits(data: impl Into<Arc<[u8]>>, limits: ParseLimits) -> Result<Self> {
38 Self::open_with_password_and_limits(data, b"", limits)
39 }
40
41 pub fn open_with_password(data: impl Into<Arc<[u8]>>, password: &[u8]) -> Result<Self> {
45 Self::open_with_password_and_limits(data, password, ParseLimits::default())
46 }
47
48 pub fn open_with_password_and_limits(
49 data: impl Into<Arc<[u8]>>,
50 password: &[u8],
51 limits: ParseLimits,
52 ) -> Result<Self> {
53 let file = PdfFile::parse_with_password_and_limits(data, password, limits)?;
54 let catalog = Catalog::from_trailer(&file)?;
55 Ok(Self {
56 file,
57 catalog,
58 acro_form: OnceLock::new(),
59 })
60 }
61
62 pub fn is_encrypted(&self) -> bool {
64 self.file.is_encrypted()
65 }
66
67 pub fn page_count(&self) -> usize {
68 self.catalog.page_count
69 }
70
71 pub fn page(&self, index: usize) -> Result<PdfPage> {
72 self.catalog.get_page(&self.file, index)
73 }
74
75 pub fn file(&self) -> &PdfFile {
76 &self.file
77 }
78
79 pub fn version(&self) -> (u8, u8) {
80 (self.file.header.major, self.file.header.minor)
81 }
82
83 pub fn page_content_bytes(&self, page: &PdfPage) -> Result<Vec<u8>> {
85 let mut all_bytes = Vec::new();
86 for &content_id in &page.contents {
87 match self.file.resolve_stream_data(content_id) {
88 Ok(bytes) => {
89 if !all_bytes.is_empty() {
90 all_bytes.push(b'\n');
91 }
92 all_bytes.extend_from_slice(&bytes);
93 }
94 Err(e) => {
95 tracing::warn!("failed to decode content stream {content_id}: {e}");
96 }
97 }
98 }
99 Ok(all_bytes)
100 }
101
102 pub fn load_page_fonts(&self, page: &PdfPage) -> FontCache {
104 font_loader::load_page_fonts(self.file(), page)
105 }
106
107 pub fn page_annotations(&self, page: &PdfPage) -> Vec<Annotation> {
112 annotation::parse_annotations(&self.file, page, self.acro_form())
113 }
114
115 pub fn acro_form(&self) -> Option<&AcroForm> {
118 self.acro_form
119 .get_or_init(|| AcroForm::parse(&self.file))
120 .as_ref()
121 }
122
123 pub fn oc_config(&self) -> Option<OcConfig> {
125 optional_content::parse_oc_config(&self.file)
126 }
127
128 pub fn output_intents(&self) -> Vec<OutputIntent> {
132 output_intents::parse_output_intents(&self.file)
133 }
134
135 pub fn page_output_intents<'a>(&self, page: &'a PdfPage) -> &'a [OutputIntent] {
138 &page.output_intents
139 }
140
141 pub fn embedded_files(&self) -> Vec<EmbeddedFile> {
146 embedded_files::parse_embedded_files(&self.file)
147 }
148
149 pub fn associated_files(&self) -> Vec<EmbeddedFile> {
153 embedded_files::parse_associated_files(&self.file)
154 }
155
156 pub fn page_associated_files(&self, page: &PdfPage) -> Vec<EmbeddedFile> {
159 match self
160 .file
161 .resolve(page.id)
162 .ok()
163 .and_then(|o| o.as_dict().ok().cloned())
164 {
165 Some(dict) => embedded_files::parse_page_associated_files(&self.file, &dict),
166 None => Vec::new(),
167 }
168 }
169
170 pub fn embedded_file_bytes(&self, file: &EmbeddedFile) -> Result<Vec<u8>> {
175 match file.stream {
176 Some(id) => self.file.resolve_stream_data(id),
177 None => Err(Error::MissingKey("EF".into())),
181 }
182 }
183}
184
185#[cfg(test)]
186pub(crate) mod test_util {
187 pub fn build_pdf(objects: &[&str]) -> Vec<u8> {
191 let mut buf = Vec::from(&b"%PDF-1.7\n"[..]);
192 let mut offsets = Vec::with_capacity(objects.len());
193 for (i, body) in objects.iter().enumerate() {
194 offsets.push(buf.len());
195 buf.extend_from_slice(format!("{} 0 obj\n{body}\nendobj\n", i + 1).as_bytes());
196 }
197 let xref_off = buf.len();
198 buf.extend_from_slice(
199 format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes(),
200 );
201 for off in &offsets {
202 buf.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
203 }
204 buf.extend_from_slice(
205 format!(
206 "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref_off}\n%%EOF\n",
207 objects.len() + 1
208 )
209 .as_bytes(),
210 );
211 buf
212 }
213}