1pub mod content;
74pub mod crypto;
75pub mod error;
76pub mod filters;
77pub mod lexer;
78pub mod objects;
79pub mod page_tree;
80pub mod resolver;
81pub mod resources;
82pub mod xref;
83
84pub use error::PdfError;
85pub use objects::{PdfDict, PdfObj};
86pub use page_tree::PageInfo;
87
88use content::ContentInterpreter;
89use resolver::Resolver;
90use std::collections::HashSet;
91use std::sync::Arc;
92use stet_fonts::geometry::Matrix;
93use stet_graphics::display_list::DisplayList;
94use stet_graphics::icc::IccCache;
95
96pub type FontProvider = Arc<dyn Fn(&str) -> Option<Vec<u8>> + Send + Sync>;
100
101pub struct PdfDocument<'a> {
103 resolver: Resolver<'a>,
104 pages: Vec<PageInfo>,
105 icc_cache: IccCache,
106 font_provider: Option<FontProvider>,
107 overprint: bool,
110 ocg_off: HashSet<u32>,
113 output_intent_icc: Option<Vec<u8>>,
117}
118
119impl<'a> PdfDocument<'a> {
120 pub fn from_bytes(data: &'a [u8]) -> Result<Self, PdfError> {
122 let mut icc_cache = IccCache::new();
123 icc_cache.search_system_cmyk_profile();
124 Self::from_bytes_inner(data, icc_cache, b"")
125 }
126
127 pub fn from_bytes_with_icc(data: &'a [u8], icc_cache: IccCache) -> Result<Self, PdfError> {
132 Self::from_bytes_inner(data, icc_cache, b"")
133 }
134
135 pub fn from_bytes_with_password(
141 data: &'a [u8],
142 icc_cache: IccCache,
143 password: &[u8],
144 ) -> Result<Self, PdfError> {
145 Self::from_bytes_inner(data, icc_cache, password)
146 }
147
148 fn from_bytes_inner(
149 data: &'a [u8],
150 icc_cache: IccCache,
151 password: &[u8],
152 ) -> Result<Self, PdfError> {
153 if !has_pdf_header(data) {
155 return Err(PdfError::NotAPdf);
156 }
157
158 let xref = xref::parse_xref(data)?;
159
160 let encryption = if let Some(encrypt_ref) = xref.trailer.get(b"Encrypt") {
163 if matches!(encrypt_ref, crate::objects::PdfObj::Null) {
164 None
165 } else {
166 let temp_resolver = Resolver::new(data, &xref);
169 let encrypt_obj = temp_resolver.deref(encrypt_ref)?;
170 let encrypt_dict = encrypt_obj
171 .as_dict()
172 .ok_or(PdfError::Other("Encrypt is not a dict".into()))?;
173
174 let file_id = xref
175 .trailer
176 .get_array(b"ID")
177 .and_then(|arr| arr.first()?.as_str().map(|s| s.to_vec()))
178 .unwrap_or_default();
179
180 Some(crypto::EncryptionState::try_open_with_password(
181 encrypt_dict,
182 &xref.trailer,
183 &file_id,
184 password,
185 )?)
186 }
187 } else {
188 None
189 };
190
191 let resolver = Resolver::with_encryption(data, xref, encryption);
192 let pages = page_tree::collect_pages(&resolver)?;
193 let ocg_off = parse_ocg_off(&resolver);
194 let output_intent_icc = parse_output_intent_icc(&resolver);
195
196 Ok(Self {
197 resolver,
198 pages,
199 icc_cache,
200 font_provider: None,
201 overprint: true,
202 ocg_off,
203 output_intent_icc,
204 })
205 }
206
207 pub fn set_overprint(&mut self, enabled: bool) {
212 self.overprint = enabled;
213 }
214
215 pub fn set_font_provider(&mut self, provider: FontProvider) {
217 self.font_provider = Some(provider);
218 }
219
220 pub fn page_count(&self) -> usize {
222 self.pages.len()
223 }
224
225 pub fn page_size(&self, page: usize) -> Result<(f64, f64), PdfError> {
227 let info = self
228 .pages
229 .get(page)
230 .ok_or(PdfError::PageOutOfRange(page, self.pages.len()))?;
231 let [llx, lly, urx, ury] = info.crop_box;
232 let (w, h) = ((urx - llx).abs(), (ury - lly).abs());
233 match info.rotate.rem_euclid(360) {
234 90 | 270 => Ok((h, w)),
235 _ => Ok((w, h)),
236 }
237 }
238
239 pub fn page_info(&self, page: usize) -> Result<&PageInfo, PdfError> {
241 self.pages
242 .get(page)
243 .ok_or(PdfError::PageOutOfRange(page, self.pages.len()))
244 }
245
246 pub fn page_contents(&self, page: usize) -> Result<Vec<u8>, PdfError> {
250 let info = self
251 .pages
252 .get(page)
253 .ok_or(PdfError::PageOutOfRange(page, self.pages.len()))?;
254
255 if info.contents.is_empty() {
256 return Ok(Vec::new());
257 }
258
259 let mut result = Vec::new();
260 for (i, &(obj_num, gen_num)) in info.contents.iter().enumerate() {
261 match self.resolver.stream_data(obj_num, gen_num) {
264 Ok(data) => {
265 if i > 0 && !result.is_empty() {
266 result.push(b'\n');
267 }
268 result.extend_from_slice(&data);
269 }
270 Err(_) => continue,
271 }
272 }
273
274 Ok(result)
275 }
276
277 pub fn render_page(&self, page: usize, dpi: f64) -> Result<DisplayList, PdfError> {
283 let info = self
284 .pages
285 .get(page)
286 .ok_or(PdfError::PageOutOfRange(page, self.pages.len()))?;
287
288 let [llx, lly, urx, ury] = info.crop_box;
289 let (page_w, page_h) = ((urx - llx).abs(), (ury - lly).abs());
290
291 let scale = dpi / 72.0;
294 let ctm = match info.rotate.rem_euclid(360) {
295 90 => {
296 Matrix::new(0.0, scale, scale, 0.0, 0.0, 0.0).concat(&Matrix::translate(-llx, -lly))
298 }
299 180 => {
300 Matrix::new(-scale, 0.0, 0.0, scale, page_w * scale, 0.0)
302 .concat(&Matrix::translate(-llx, -lly))
303 }
304 270 => {
305 Matrix::new(0.0, -scale, -scale, 0.0, page_h * scale, page_w * scale)
307 .concat(&Matrix::translate(-llx, -lly))
308 }
309 _ => {
310 Matrix::new(scale, 0.0, 0.0, -scale, -llx * scale, ury * scale)
313 }
314 };
315
316 let content_data = self.page_contents(page)?;
318
319 let mut interpreter = ContentInterpreter::new(
321 &self.resolver,
322 info.resources.clone(),
323 ctm,
324 &self.icc_cache,
325 self.font_provider.clone(),
326 self.overprint,
327 &self.ocg_off,
328 );
329
330 let page_group_is_cmyk = if let Ok(page_obj) = self.resolver.resolve(info.obj_num, 0)
334 && let Some(page_dict) = page_obj.as_dict()
335 && let Some(group_obj) = page_dict.get(b"Group")
336 && let Ok(group_resolved) = self.resolver.deref(group_obj)
337 && let Some(group_dict) = group_resolved.as_dict()
338 && group_dict.get_name(b"CS") == Some(b"DeviceCMYK")
339 {
340 interpreter.set_page_group_cmyk();
341 true
342 } else {
343 false
344 };
345
346 if let Err(e) = interpreter.interpret_stream_public(&content_data) {
348 eprintln!("warning: content stream error: {}", e);
349 }
350 interpreter.unwind_gstate_stack();
352
353 if !info.annots.is_empty() {
355 interpreter.reset_clip_for_annotations();
356 for &(n, g) in &info.annots {
357 let _ = interpreter.render_annotation(n, g);
358 }
359 }
360
361 let mut dl = interpreter.into_display_list();
362 if page_group_is_cmyk {
363 dl.set_page_group_color_space(stet_graphics::display_list::GroupColorSpace::DeviceCMYK);
364 }
365 Ok(dl)
366 }
367
368 #[cfg(feature = "render")]
372 pub fn render_page_to_rgba(
373 &self,
374 page: usize,
375 dpi: f64,
376 ) -> Result<(Vec<u8>, u32, u32), PdfError> {
377 let (page_w, page_h) = self.page_size(page)?;
378 let scale = dpi / 72.0;
379 let pixel_w = (page_w * scale).round() as u32;
380 let pixel_h = (page_h * scale).round() as u32;
381
382 let display_list = self.render_page(page, dpi)?;
383
384 let rgba = stet_render::render_to_rgba(
385 &display_list,
386 pixel_w,
387 pixel_h,
388 dpi,
389 Some(&self.icc_cache),
390 false,
391 );
392
393 Ok((rgba, pixel_w, pixel_h))
394 }
395
396 pub fn icc_cache(&self) -> &IccCache {
398 &self.icc_cache
399 }
400
401 pub fn output_intent_icc(&self) -> Option<&[u8]> {
407 self.output_intent_icc.as_deref()
408 }
409
410 pub fn apply_output_intent_as_default_cmyk(&mut self) -> bool {
415 let Some(bytes) = self.output_intent_icc.as_deref() else {
416 return false;
417 };
418 let Some(hash) = self.icc_cache.register_profile(bytes) else {
419 return false;
420 };
421 self.icc_cache.set_system_cmyk(bytes, hash);
422 true
423 }
424
425 pub fn resolver(&self) -> &Resolver<'a> {
427 &self.resolver
428 }
429
430 pub fn pages(&self) -> &[PageInfo] {
432 &self.pages
433 }
434}
435
436fn parse_ocg_off(resolver: &Resolver) -> HashSet<u32> {
440 let mut off = HashSet::new();
441
442 let mut catalog_owned;
446 let catalog_dict = if let Some(root_ref) = resolver.trailer().get_ref(b"Root") {
447 if let Ok(c) = resolver.resolve(root_ref.0, root_ref.1) {
448 catalog_owned = c;
449 match catalog_owned.as_dict() {
450 Some(d) if d.get(b"OCProperties").is_some() => d,
451 _ => match find_catalog(resolver) {
452 Some(c) => {
453 catalog_owned = c;
454 catalog_owned.as_dict().unwrap()
455 }
456 None => return off,
457 },
458 }
459 } else {
460 return off;
461 }
462 } else {
463 return off;
464 };
465
466 let oc_props = match catalog_dict.get(b"OCProperties") {
468 Some(obj) => match resolver.deref(obj) {
469 Ok(o) => o,
470 Err(_) => return off,
471 },
472 None => return off,
473 };
474 let oc_dict = match oc_props.as_dict() {
475 Some(d) => d,
476 None => return off,
477 };
478 let d_obj = match oc_dict.get(b"D") {
479 Some(obj) => match resolver.deref(obj) {
480 Ok(o) => o,
481 Err(_) => return off,
482 },
483 None => return off,
484 };
485 let d_dict = match d_obj.as_dict() {
486 Some(d) => d,
487 None => return off,
488 };
489
490 if let Some(off_obj) = d_dict.get(b"OFF") {
492 let off_resolved = resolver.deref(off_obj).unwrap_or_else(|_| off_obj.clone());
493 if let Some(off_arr) = off_resolved.as_array() {
494 for obj in off_arr {
495 if let Some((num, _gen)) = obj.as_ref() {
496 off.insert(num);
497 }
498 }
499 }
500 }
501
502 off
503}
504
505fn parse_output_intent_icc(resolver: &Resolver) -> Option<Vec<u8>> {
514 let mut catalog_owned;
515 let catalog_dict = if let Some(root_ref) = resolver.trailer().get_ref(b"Root") {
516 if let Ok(c) = resolver.resolve(root_ref.0, root_ref.1) {
517 catalog_owned = c;
518 match catalog_owned.as_dict() {
519 Some(d) if d.get(b"OutputIntents").is_some() => d,
520 _ => {
521 catalog_owned = find_catalog(resolver)?;
522 catalog_owned.as_dict()?
523 }
524 }
525 } else {
526 catalog_owned = find_catalog(resolver)?;
527 catalog_owned.as_dict()?
528 }
529 } else {
530 catalog_owned = find_catalog(resolver)?;
531 catalog_owned.as_dict()?
532 };
533
534 let intents_obj = resolver.deref(catalog_dict.get(b"OutputIntents")?).ok()?;
535 let intents_arr = intents_obj.as_array()?;
536 for entry in intents_arr {
537 let intent = match resolver.deref(entry) {
538 Ok(o) => o,
539 Err(_) => continue,
540 };
541 let Some(intent_dict) = intent.as_dict() else {
542 continue;
543 };
544 let Some(profile_obj) = intent_dict.get(b"DestOutputProfile") else {
545 continue;
546 };
547 let Ok(bytes) = resolver.stream_data_from_obj(profile_obj) else {
548 continue;
549 };
550 if bytes.len() >= 40 && &bytes[36..40] == b"acsp" && &bytes[16..20] == b"CMYK" {
552 return Some(bytes);
553 }
554 }
555 None
556}
557
558fn find_catalog(resolver: &Resolver) -> Option<PdfObj> {
561 let xref_len = resolver.xref_len();
562 for obj_num in 0..xref_len as u32 {
563 if let Ok(obj) = resolver.resolve(obj_num, 0) {
564 if let Some(dict) = obj.as_dict() {
565 if dict.get_name(b"Type") == Some(b"Catalog") && dict.get(b"Pages").is_some() {
566 return Some(obj);
567 }
568 }
569 }
570 }
571 None
572}
573
574fn has_pdf_header(data: &[u8]) -> bool {
577 let search_range = data.len().min(1024);
578 data[..search_range].windows(5).any(|w| w == b"%PDF-")
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584
585 #[test]
586 fn not_a_pdf() {
587 let result = PdfDocument::from_bytes(b"not a pdf");
588 assert!(matches!(result, Err(PdfError::NotAPdf)));
589 }
590
591 #[test]
592 fn parse_minimal_pdf() {
593 let pdf = build_minimal_pdf();
594 let doc = PdfDocument::from_bytes(&pdf).unwrap();
595 assert_eq!(doc.page_count(), 1);
596
597 let (w, h) = doc.page_size(0).unwrap();
598 assert_eq!(w, 612.0);
599 assert_eq!(h, 792.0);
600 }
601
602 #[test]
603 fn page_out_of_range() {
604 let pdf = build_minimal_pdf();
605 let doc = PdfDocument::from_bytes(&pdf).unwrap();
606 assert!(matches!(
607 doc.page_size(5),
608 Err(PdfError::PageOutOfRange(5, 1))
609 ));
610 }
611
612 #[test]
613 fn page_contents_empty() {
614 let pdf = build_minimal_pdf();
615 let doc = PdfDocument::from_bytes(&pdf).unwrap();
616 let contents = doc.page_contents(0).unwrap();
617 assert!(contents.is_empty());
619 }
620
621 #[test]
622 #[ignore]
623 fn dump_display_list() {
624 use stet_fonts::geometry::PsPath;
625 use stet_graphics::display_list::{DisplayElement, DisplayList};
626
627 fn path_bbox(path: &PsPath) -> String {
628 use stet_fonts::geometry::PathSegment;
629 let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
630 for seg in &path.segments {
631 let pts: Vec<(f64, f64)> = match seg {
632 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => vec![(*x, *y)],
633 PathSegment::CurveTo {
634 x1,
635 y1,
636 x2,
637 y2,
638 x3,
639 y3,
640 } => vec![(*x1, *y1), (*x2, *y2), (*x3, *y3)],
641 PathSegment::ClosePath => vec![],
642 };
643 for (px, py) in pts {
644 x0 = x0.min(px);
645 y0 = y0.min(py);
646 x1 = x1.max(px);
647 y1 = y1.max(py);
648 }
649 }
650 format!("bbox=({:.0},{:.0},{:.0},{:.0})", x0, y0, x1, y1)
651 }
652
653 fn dump(list: &DisplayList, depth: usize) {
654 let indent = " ".repeat(depth);
655 for (i, elem) in list.elements().iter().enumerate() {
656 match elem {
657 DisplayElement::Fill { path, params } => {
658 let c = ¶ms.color;
659 let cmyk_str = if let Some((c2, m, y, k)) = params.color.native_cmyk {
660 format!(" cmyk=({:.2},{:.2},{:.2},{:.2})", c2, m, y, k)
661 } else {
662 String::new()
663 };
664 eprintln!(
665 "{indent}[{i}] Fill rgb=({:.2},{:.2},{:.2}){} op={} opm={} ch=0x{:x} a={:.2} {}",
666 c.r,
667 c.g,
668 c.b,
669 cmyk_str,
670 params.overprint,
671 params.overprint_mode,
672 params.painted_channels,
673 params.alpha,
674 path_bbox(path)
675 );
676 }
677 DisplayElement::Stroke { path, params } => {
678 let c = ¶ms.color;
679 eprintln!(
680 "{indent}[{i}] Stroke rgb=({:.2},{:.2},{:.2}) {}",
681 c.r,
682 c.g,
683 c.b,
684 path_bbox(path)
685 );
686 }
687 DisplayElement::Clip { path, .. } => {
688 eprintln!("{indent}[{i}] Clip {}", path_bbox(path))
689 }
690 DisplayElement::InitClip => eprintln!("{indent}[{i}] InitClip"),
691 DisplayElement::Image { params, .. } => {
692 eprintln!("{indent}[{i}] Image {}x{}", params.width, params.height);
693 }
694 DisplayElement::ErasePage => eprintln!("{indent}[{i}] ErasePage"),
695 DisplayElement::AxialShading { params } => {
696 eprintln!(
697 "{indent}[{i}] AxialShading cs={:?} stops={}",
698 params.color_space,
699 params.color_stops.len()
700 );
701 }
702 DisplayElement::RadialShading { params } => {
703 eprintln!(
704 "{indent}[{i}] RadialShading cs={:?} stops={} ext=({},{}) c0=({:.1},{:.1}) r0={:.1} c1=({:.1},{:.1}) r1={:.1} bbox={:?} op={} ch=0x{:x}",
705 params.color_space,
706 params.color_stops.len(),
707 params.extend_start,
708 params.extend_end,
709 params.x0,
710 params.y0,
711 params.r0,
712 params.x1,
713 params.y1,
714 params.r1,
715 params.bbox,
716 params.overprint,
717 params.painted_channels
718 );
719 if let Some(first) = params.color_stops.first() {
721 eprintln!(
722 "{indent} stop[0]: pos={:.3} rgb=({:.3},{:.3},{:.3}) raw={:?}",
723 first.position,
724 first.color.r,
725 first.color.g,
726 first.color.b,
727 first.raw_components
728 );
729 }
730 if let Some(last) = params.color_stops.last() {
731 eprintln!(
732 "{indent} stop[{}]: pos={:.3} rgb=({:.3},{:.3},{:.3}) raw={:?}",
733 params.color_stops.len() - 1,
734 last.position,
735 last.color.r,
736 last.color.g,
737 last.color.b,
738 last.raw_components
739 );
740 }
741 let mid = params.color_stops.len() / 2;
743 if mid > 0 && mid < params.color_stops.len() - 1 {
744 let s = ¶ms.color_stops[mid];
745 eprintln!(
746 "{indent} stop[{mid}]: pos={:.3} rgb=({:.3},{:.3},{:.3}) raw={:?}",
747 s.position, s.color.r, s.color.g, s.color.b, s.raw_components
748 );
749 }
750 }
751 DisplayElement::MeshShading { .. } => eprintln!("{indent}[{i}] MeshShading"),
752 DisplayElement::PatchShading { .. } => eprintln!("{indent}[{i}] PatchShading"),
753 DisplayElement::PatternFill { .. } => eprintln!("{indent}[{i}] PatternFill"),
754 DisplayElement::Text { .. } => eprintln!("{indent}[{i}] Text"),
755 DisplayElement::Group { elements, params } => {
756 eprintln!(
757 "{indent}[{i}] Group iso={} ko={} blend={} a={:.2} bbox=({:.0},{:.0},{:.0},{:.0}) children={}",
758 params.isolated,
759 params.knockout,
760 params.blend_mode,
761 params.alpha,
762 params.bbox[0],
763 params.bbox[1],
764 params.bbox[2],
765 params.bbox[3],
766 elements.len()
767 );
768 dump(elements, depth + 1);
769 }
770 DisplayElement::SoftMasked {
771 mask,
772 content,
773 params,
774 ..
775 } => {
776 eprintln!(
777 "{indent}[{i}] SoftMasked {:?} mask={} content={}",
778 params.subtype,
779 mask.len(),
780 content.len()
781 );
782 eprintln!("{indent} MASK:");
783 dump(mask, depth + 2);
784 eprintln!("{indent} CONTENT:");
785 dump(content, depth + 2);
786 }
787 DisplayElement::OcgGroup {
788 elements,
789 ocg_id,
790 default_visible,
791 } => {
792 eprintln!(
793 "{indent}[{i}] OcgGroup id={} visible={} children={}",
794 ocg_id,
795 default_visible,
796 elements.len()
797 );
798 dump(elements, depth + 1);
799 }
800 }
801 }
802 }
803
804 let data = std::fs::read("../../pdf_samples/PDFX-ready_Output-Test_X4.pdf").unwrap();
805 let doc = PdfDocument::from_bytes(&data).unwrap();
806 let dl = doc.render_page(0, 72.0).unwrap();
807 eprintln!("=== Display list: {} top-level elements ===", dl.len());
808 dump(&dl, 0);
809 }
810
811 fn build_minimal_pdf() -> Vec<u8> {
813 let mut pdf = Vec::new();
814 pdf.extend(b"%PDF-1.4\n");
815
816 let obj1_offset = pdf.len();
818 pdf.extend(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
819
820 let obj2_offset = pdf.len();
822 pdf.extend(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
823
824 let obj3_offset = pdf.len();
826 pdf.extend(b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n");
827
828 let xref_offset = pdf.len();
830 pdf.extend(b"xref\n0 4\n");
831 pdf.extend(b"0000000000 65535 f\r\n");
832 pdf.extend(format!("{:010} 00000 n\r\n", obj1_offset).as_bytes());
833 pdf.extend(format!("{:010} 00000 n\r\n", obj2_offset).as_bytes());
834 pdf.extend(format!("{:010} 00000 n\r\n", obj3_offset).as_bytes());
835 pdf.extend(b"trailer\n<< /Size 4 /Root 1 0 R >>\n");
836 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
837
838 pdf
839 }
840}