1pub mod annotations;
173pub mod content;
174pub mod crypto;
175pub mod destination;
176pub mod diagnostics;
177pub mod embedded_files;
178pub mod error;
179pub mod filters;
180pub mod form_fields;
181pub mod layers;
182pub mod lexer;
183pub mod metadata;
184pub mod name_tree;
185pub mod objects;
186pub mod outline;
187pub mod page_boxes;
188pub mod page_tree;
189pub mod resolver;
190pub mod resources;
191pub mod viewer_prefs;
192pub mod xref;
193
194pub use annotations::{
195 Annotation, AnnotationColor, AnnotationDate, AnnotationFlags, AnnotationKind,
196 AnnotationKindData, Border, CaretAnnotation, FileAttachmentAnnotation, FreeTextAnnotation,
197 InkAnnotation, LineAnnotation, LinkAnnotation, MarkupAnnotation, PolygonAnnotation,
198 PopupAnnotation, ShapeAnnotation, StampAnnotation, TextAnnotation,
199};
200pub use destination::{Action, Destination, ViewSpec};
201pub use diagnostics::{LocationHint, ParsePhase, ParseWarning, Severity, WarningSink};
202pub use embedded_files::{AfRelationship, EmbeddedFile};
203pub use error::PdfError;
204pub use form_fields::{
205 ButtonField, ButtonType, ChoiceField, ChoiceOption, FieldFlags, FieldKind, FieldValue,
206 FormCatalog, FormField, SigFlags, SignatureField, TextField,
207};
208pub use layers::{
209 AutoStateEvent, AutoStateRule, BaseState, Configuration, CreatorInfo, ExportUsage,
210 LanguageUsage, Layer, LayerIntent, LayerSet, LayerTree, LayerTreeNode, LayerUsage, ListMode,
211 MembershipPolicy, OcgVisibility, PageElementSubtype, PrintUsage, RenderIntent, UsageState,
212 UserUsage, ViewUsage, VisibilityExpr, ZoomUsage,
213};
214pub use metadata::{DocumentMetadata, PdfDate, TrappedFlag};
215pub use objects::{PdfDict, PdfObj};
216pub use outline::{OutlineItem, OutlineStyle};
217pub use page_boxes::PageBoxes;
218pub use page_tree::PageInfo;
219pub use viewer_prefs::{
220 Duplex, PageLayout, PageMode, PrintScaling, ReadingDirection, ViewerPreferences,
221};
222
223use content::ContentInterpreter;
224use resolver::Resolver;
225use std::cell::OnceCell;
226use std::collections::{HashMap, HashSet};
227use std::sync::Arc;
228use stet_fonts::geometry::Matrix;
229use stet_graphics::display_list::DisplayList;
230use stet_graphics::document_structure::OutputIntentRecord;
231use stet_graphics::icc::IccCache;
232
233pub type FontProvider = Arc<dyn Fn(&str) -> Option<Vec<u8>> + Send + Sync>;
237
238pub struct PdfDocument<'a> {
240 resolver: Resolver<'a>,
241 pages: Vec<PageInfo>,
242 icc_cache: IccCache,
243 font_provider: Option<FontProvider>,
244 overprint: bool,
247 ocg_off: HashSet<u32>,
250 output_intent_icc: Option<Vec<u8>>,
254 metadata_cache: OnceCell<DocumentMetadata>,
256 viewer_prefs_cache: OnceCell<ViewerPreferences>,
258 outline_cache: OnceCell<Vec<OutlineItem>>,
260 destinations_cache: OnceCell<HashMap<String, Destination>>,
263 page_annotations_cache: Vec<OnceCell<Vec<Annotation>>>,
267 form_cache: OnceCell<Option<FormCatalog>>,
271 embedded_files_cache: OnceCell<HashMap<String, EmbeddedFile>>,
274 layers_cache: OnceCell<Vec<Layer>>,
277 configurations_cache: OnceCell<Vec<Configuration>>,
280 warnings: WarningSink,
285}
286
287impl<'a> PdfDocument<'a> {
288 pub fn from_bytes(data: &'a [u8]) -> Result<Self, PdfError> {
290 let mut icc_cache = IccCache::new();
291 icc_cache.search_system_cmyk_profile();
292 Self::from_bytes_inner(data, icc_cache, b"")
293 }
294
295 pub fn from_bytes_with_icc(data: &'a [u8], icc_cache: IccCache) -> Result<Self, PdfError> {
300 Self::from_bytes_inner(data, icc_cache, b"")
301 }
302
303 pub fn from_bytes_with_password(
309 data: &'a [u8],
310 icc_cache: IccCache,
311 password: &[u8],
312 ) -> Result<Self, PdfError> {
313 Self::from_bytes_inner(data, icc_cache, password)
314 }
315
316 fn from_bytes_inner(
317 data: &'a [u8],
318 icc_cache: IccCache,
319 password: &[u8],
320 ) -> Result<Self, PdfError> {
321 if !has_pdf_header(data) {
323 return Err(PdfError::NotAPdf);
324 }
325
326 let xref = xref::parse_xref(data)?;
327
328 let encryption = if let Some(encrypt_ref) = xref.trailer.get(b"Encrypt") {
331 if matches!(encrypt_ref, crate::objects::PdfObj::Null) {
332 None
333 } else {
334 let temp_resolver = Resolver::new(data, &xref);
337 let encrypt_obj = temp_resolver.deref(encrypt_ref)?;
338 let encrypt_dict = encrypt_obj
339 .as_dict()
340 .ok_or(PdfError::Other("Encrypt is not a dict".into()))?;
341
342 let file_id = xref
343 .trailer
344 .get_array(b"ID")
345 .and_then(|arr| arr.first()?.as_str().map(|s| s.to_vec()))
346 .unwrap_or_default();
347
348 Some(crypto::EncryptionState::try_open_with_password(
349 encrypt_dict,
350 &xref.trailer,
351 &file_id,
352 password,
353 )?)
354 }
355 } else {
356 None
357 };
358
359 let resolver = Resolver::with_encryption(data, xref, encryption);
360 let pages = page_tree::collect_pages(&resolver)?;
361 let ocg_off = parse_ocg_off(&resolver);
362 let output_intent_icc = parse_output_intent_icc(&resolver);
363
364 let page_annotations_cache = (0..pages.len()).map(|_| OnceCell::new()).collect();
365 Ok(Self {
366 resolver,
367 pages,
368 icc_cache,
369 font_provider: None,
370 overprint: true,
371 ocg_off,
372 output_intent_icc,
373 metadata_cache: OnceCell::new(),
374 viewer_prefs_cache: OnceCell::new(),
375 outline_cache: OnceCell::new(),
376 destinations_cache: OnceCell::new(),
377 page_annotations_cache,
378 form_cache: OnceCell::new(),
379 embedded_files_cache: OnceCell::new(),
380 layers_cache: OnceCell::new(),
381 configurations_cache: OnceCell::new(),
382 warnings: WarningSink::new(),
383 })
384 }
385
386 pub fn set_overprint(&mut self, enabled: bool) {
391 self.overprint = enabled;
392 }
393
394 pub fn set_font_provider(&mut self, provider: FontProvider) {
396 self.font_provider = Some(provider);
397 }
398
399 pub fn page_count(&self) -> usize {
401 self.pages.len()
402 }
403
404 pub fn page_size(&self, page: usize) -> Result<(f64, f64), PdfError> {
406 let info = self
407 .pages
408 .get(page)
409 .ok_or(PdfError::PageOutOfRange(page, self.pages.len()))?;
410 let [llx, lly, urx, ury] = info.crop_box;
411 let (w, h) = ((urx - llx).abs(), (ury - lly).abs());
412 match info.rotate.rem_euclid(360) {
413 90 | 270 => Ok((h, w)),
414 _ => Ok((w, h)),
415 }
416 }
417
418 pub fn page_info(&self, page: usize) -> Result<&PageInfo, PdfError> {
420 self.pages
421 .get(page)
422 .ok_or(PdfError::PageOutOfRange(page, self.pages.len()))
423 }
424
425 pub fn page_contents(&self, page: usize) -> Result<Vec<u8>, PdfError> {
429 let info = self
430 .pages
431 .get(page)
432 .ok_or(PdfError::PageOutOfRange(page, self.pages.len()))?;
433
434 if info.contents.is_empty() {
435 return Ok(Vec::new());
436 }
437
438 let mut result = Vec::new();
439 for (i, &(obj_num, gen_num)) in info.contents.iter().enumerate() {
440 match self.resolver.stream_data(obj_num, gen_num) {
443 Ok(data) => {
444 if i > 0 && !result.is_empty() {
445 result.push(b'\n');
446 }
447 result.extend_from_slice(&data);
448 }
449 Err(_) => continue,
450 }
451 }
452
453 Ok(result)
454 }
455
456 pub fn render_page(&self, page: usize, dpi: f64) -> Result<DisplayList, PdfError> {
462 let info = self
463 .pages
464 .get(page)
465 .ok_or(PdfError::PageOutOfRange(page, self.pages.len()))?;
466
467 let [llx, lly, urx, ury] = info.crop_box;
468 let (page_w, page_h) = ((urx - llx).abs(), (ury - lly).abs());
469
470 let scale = dpi / 72.0;
473 let ctm = match info.rotate.rem_euclid(360) {
474 90 => {
475 Matrix::new(0.0, scale, scale, 0.0, 0.0, 0.0).concat(&Matrix::translate(-llx, -lly))
477 }
478 180 => {
479 Matrix::new(-scale, 0.0, 0.0, scale, page_w * scale, 0.0)
481 .concat(&Matrix::translate(-llx, -lly))
482 }
483 270 => {
484 Matrix::new(0.0, -scale, -scale, 0.0, page_h * scale, page_w * scale)
486 .concat(&Matrix::translate(-llx, -lly))
487 }
488 _ => {
489 Matrix::new(scale, 0.0, 0.0, -scale, -llx * scale, ury * scale)
492 }
493 };
494
495 let content_data = self.page_contents(page)?;
497
498 let mut interpreter = ContentInterpreter::new(
500 &self.resolver,
501 info.resources.clone(),
502 ctm,
503 &self.icc_cache,
504 self.font_provider.clone(),
505 self.overprint,
506 &self.ocg_off,
507 );
508
509 let explicit_cmyk_group = if let Ok(page_obj) = self.resolver.resolve(info.obj_num, 0)
520 && let Some(page_dict) = page_obj.as_dict()
521 && let Some(group_obj) = page_dict.get(b"Group")
522 && let Ok(group_resolved) = self.resolver.deref(group_obj)
523 && let Some(group_dict) = group_resolved.as_dict()
524 && group_dict.get_name(b"CS") == Some(b"DeviceCMYK")
525 {
526 true
527 } else {
528 false
529 };
530 let page_group_is_cmyk = explicit_cmyk_group || self.output_intent_icc.is_some();
531 if page_group_is_cmyk {
532 interpreter.set_page_group_cmyk();
533 }
534 if self.output_intent_icc.is_some() {
545 interpreter.set_pdfx_cmyk_intent();
546 }
547
548 if let Err(e) = interpreter.interpret_stream_public(&content_data) {
550 eprintln!("warning: content stream error: {}", e);
551 }
552 interpreter.unwind_gstate_stack();
554
555 if !info.annots.is_empty() {
557 interpreter.reset_clip_for_annotations();
558 for &(n, g) in &info.annots {
559 let _ = interpreter.render_annotation(n, g);
560 }
561 }
562
563 let mut dl = interpreter.into_display_list();
564 if page_group_is_cmyk {
565 dl.set_page_group_color_space(stet_graphics::display_list::GroupColorSpace::DeviceCMYK);
566 }
567 Ok(dl)
568 }
569
570 #[cfg(feature = "render")]
574 pub fn render_page_to_rgba(
575 &self,
576 page: usize,
577 dpi: f64,
578 ) -> Result<(Vec<u8>, u32, u32), PdfError> {
579 self.render_page_to_rgba_with_layers(page, dpi, &LayerSet::new())
580 }
581
582 #[cfg(feature = "render")]
594 pub fn render_page_to_rgba_with_layers(
595 &self,
596 page: usize,
597 dpi: f64,
598 layer_set: &LayerSet,
599 ) -> Result<(Vec<u8>, u32, u32), PdfError> {
600 let (page_w, page_h) = self.page_size(page)?;
601 let scale = dpi / 72.0;
602 let pixel_w = (page_w * scale).round() as u32;
603 let pixel_h = (page_h * scale).round() as u32;
604
605 let display_list = self.render_page(page, dpi)?;
606
607 let rgba = stet_render::render_to_rgba_with_layers(
608 &display_list,
609 pixel_w,
610 pixel_h,
611 dpi,
612 Some(&self.icc_cache),
613 false,
614 layer_set,
615 );
616
617 Ok((rgba, pixel_w, pixel_h))
618 }
619
620 pub fn icc_cache(&self) -> &IccCache {
622 &self.icc_cache
623 }
624
625 pub fn output_intent_icc(&self) -> Option<&[u8]> {
631 self.output_intent_icc.as_deref()
632 }
633
634 pub fn apply_output_intent_as_default_cmyk(&mut self) -> bool {
639 let Some(bytes) = self.output_intent_icc.as_deref() else {
640 return false;
641 };
642 let hash = stet_graphics::icc::IccCache::hash_profile(bytes);
650 self.icc_cache.set_system_cmyk(bytes, hash);
651 self.icc_cache.set_proofing_enabled(true);
652 if self.icc_cache.register_profile(bytes).is_none() {
653 self.icc_cache.set_proofing_enabled(false);
656 return false;
657 }
658 self.icc_cache.prepare_reverse_cmyk();
669 self.icc_cache.prepare_lab_to_oi_cmyk();
674 true
675 }
676
677 pub fn output_intents(&self) -> Vec<OutputIntentRecord> {
684 parse_output_intents_full(&self.resolver)
685 }
686
687 pub fn resolver(&self) -> &Resolver<'a> {
689 &self.resolver
690 }
691
692 pub fn pages(&self) -> &[PageInfo] {
694 &self.pages
695 }
696
697 pub fn metadata(&self) -> &DocumentMetadata {
704 self.metadata_cache
705 .get_or_init(|| metadata::parse_document_metadata(&self.resolver))
706 }
707
708 pub fn viewer_preferences(&self) -> &ViewerPreferences {
715 self.viewer_prefs_cache
716 .get_or_init(|| viewer_prefs::parse_viewer_preferences(&self.resolver))
717 }
718
719 pub fn outline(&self) -> &[OutlineItem] {
727 self.outline_cache.get_or_init(|| {
728 outline::parse_outline_tree(&self.resolver, &self.pages, &self.warnings)
729 })
730 }
731
732 pub fn destinations(&self) -> &HashMap<String, Destination> {
740 self.destinations_cache
741 .get_or_init(|| destination::parse_named_destinations(&self.resolver, &self.pages))
742 }
743
744 pub fn resolve_named_destination(&self, name: &str) -> Option<Destination> {
753 self.destinations().get(name).cloned()
754 }
755
756 pub fn page_annotations(&self, page: usize) -> Result<&[Annotation], PdfError> {
765 if page >= self.pages.len() {
766 return Err(PdfError::PageOutOfRange(page, self.pages.len()));
767 }
768 let cell = &self.page_annotations_cache[page];
769 let annots = cell.get_or_init(|| {
770 annotations::parse_page_annotations(&self.resolver, &self.pages, page, &self.warnings)
771 });
772 Ok(annots.as_slice())
773 }
774
775 pub fn form(&self) -> Option<&FormCatalog> {
787 self.form_cache
788 .get_or_init(|| form_fields::parse_acroform(&self.resolver, &self.warnings))
789 .as_ref()
790 }
791
792 pub fn parse_warnings(&self) -> std::cell::Ref<'_, [ParseWarning]> {
804 self.warnings.borrow_slice()
805 }
806
807 pub fn page_boxes(&self, page: usize) -> Result<PageBoxes, PdfError> {
813 page_boxes::parse_page_boxes(&self.resolver, &self.pages, page)
814 .ok_or(PdfError::PageOutOfRange(page, self.pages.len()))
815 }
816
817 pub fn embedded_files(&self) -> &HashMap<String, EmbeddedFile> {
825 self.embedded_files_cache
826 .get_or_init(|| embedded_files::parse_embedded_files(&self.resolver))
827 }
828
829 pub fn embedded_file_bytes(&self, name: &str) -> Result<Vec<u8>, PdfError> {
833 let ef = self
834 .embedded_files()
835 .get(name)
836 .ok_or_else(|| PdfError::Other(format!("embedded file not found: {name}")))?;
837 embedded_files::decode_embedded_file_stream(
838 &self.resolver,
839 ef.stream_obj_num,
840 ef.stream_gen_num,
841 )
842 }
843
844 pub fn layers(&self) -> &[Layer] {
855 self.layers_cache
856 .get_or_init(|| layers::metadata::parse_layers(&self.resolver, &self.warnings))
857 .as_slice()
858 }
859
860 pub fn layer(&self, ocg_id: u32) -> Option<&Layer> {
865 self.layers().iter().find(|l| l.ocg_id == ocg_id)
866 }
867
868 pub fn configurations(&self) -> &[Configuration] {
877 self.configurations_cache
878 .get_or_init(|| {
879 layers::configuration::parse_configurations(&self.resolver, &self.warnings)
880 })
881 .as_slice()
882 }
883
884 pub fn default_configuration(&self) -> Option<&Configuration> {
888 self.configurations().first()
889 }
890
891 pub fn configuration(&self, index: usize) -> Option<&Configuration> {
894 self.configurations().get(index)
895 }
896
897 pub fn layer_tree(&self) -> LayerTree {
904 self.default_configuration()
905 .map(|c| c.order.clone())
906 .unwrap_or_default()
907 }
908
909 pub fn layer_set_for(&self, intent: RenderIntent) -> LayerSet {
921 layers::layer_set_for(self, intent)
922 }
923}
924
925fn parse_ocg_off(resolver: &Resolver) -> HashSet<u32> {
929 let mut off = HashSet::new();
930
931 let mut catalog_owned;
935 let catalog_dict = if let Some(root_ref) = resolver.trailer().get_ref(b"Root") {
936 if let Ok(c) = resolver.resolve(root_ref.0, root_ref.1) {
937 catalog_owned = c;
938 match catalog_owned.as_dict() {
939 Some(d) if d.get(b"OCProperties").is_some() => d,
940 _ => match find_catalog(resolver) {
941 Some(c) => {
942 catalog_owned = c;
943 catalog_owned.as_dict().unwrap()
944 }
945 None => return off,
946 },
947 }
948 } else {
949 return off;
950 }
951 } else {
952 return off;
953 };
954
955 let oc_props = match catalog_dict.get(b"OCProperties") {
957 Some(obj) => match resolver.deref(obj) {
958 Ok(o) => o,
959 Err(_) => return off,
960 },
961 None => return off,
962 };
963 let oc_dict = match oc_props.as_dict() {
964 Some(d) => d,
965 None => return off,
966 };
967 let d_obj = match oc_dict.get(b"D") {
968 Some(obj) => match resolver.deref(obj) {
969 Ok(o) => o,
970 Err(_) => return off,
971 },
972 None => return off,
973 };
974 let d_dict = match d_obj.as_dict() {
975 Some(d) => d,
976 None => return off,
977 };
978
979 if let Some(off_obj) = d_dict.get(b"OFF") {
981 let off_resolved = resolver.deref(off_obj).unwrap_or_else(|_| off_obj.clone());
982 if let Some(off_arr) = off_resolved.as_array() {
983 for obj in off_arr {
984 if let Some((num, _gen)) = obj.as_ref() {
985 off.insert(num);
986 }
987 }
988 }
989 }
990
991 off
992}
993
994fn parse_output_intent_icc(resolver: &Resolver) -> Option<Vec<u8>> {
1003 let mut catalog_owned;
1004 let catalog_dict = if let Some(root_ref) = resolver.trailer().get_ref(b"Root") {
1005 if let Ok(c) = resolver.resolve(root_ref.0, root_ref.1) {
1006 catalog_owned = c;
1007 match catalog_owned.as_dict() {
1008 Some(d) if d.get(b"OutputIntents").is_some() => d,
1009 _ => {
1010 catalog_owned = find_catalog(resolver)?;
1011 catalog_owned.as_dict()?
1012 }
1013 }
1014 } else {
1015 catalog_owned = find_catalog(resolver)?;
1016 catalog_owned.as_dict()?
1017 }
1018 } else {
1019 catalog_owned = find_catalog(resolver)?;
1020 catalog_owned.as_dict()?
1021 };
1022
1023 let intents_obj = resolver.deref(catalog_dict.get(b"OutputIntents")?).ok()?;
1024 let intents_arr = intents_obj.as_array()?;
1025 for entry in intents_arr {
1026 let intent = match resolver.deref(entry) {
1027 Ok(o) => o,
1028 Err(_) => continue,
1029 };
1030 let Some(intent_dict) = intent.as_dict() else {
1031 continue;
1032 };
1033 let Some(profile_obj) = intent_dict.get(b"DestOutputProfile") else {
1034 continue;
1035 };
1036 let Ok(bytes) = resolver.stream_data_from_obj(profile_obj) else {
1037 continue;
1038 };
1039 if bytes.len() >= 40 && &bytes[36..40] == b"acsp" && &bytes[16..20] == b"CMYK" {
1041 return Some(bytes);
1042 }
1043 }
1044 None
1045}
1046
1047fn parse_output_intents_full(resolver: &Resolver) -> Vec<OutputIntentRecord> {
1053 let catalog_obj = match resolver.trailer().get_ref(b"Root") {
1057 Some(root_ref) => match resolver.resolve(root_ref.0, root_ref.1) {
1058 Ok(c)
1059 if c.as_dict()
1060 .is_some_and(|d| d.get(b"OutputIntents").is_some()) =>
1061 {
1062 c
1063 }
1064 _ => match find_catalog(resolver) {
1065 Some(c) => c,
1066 None => return Vec::new(),
1067 },
1068 },
1069 None => match find_catalog(resolver) {
1070 Some(c) => c,
1071 None => return Vec::new(),
1072 },
1073 };
1074 let Some(catalog_dict) = catalog_obj.as_dict() else {
1075 return Vec::new();
1076 };
1077 let Some(intents_ref) = catalog_dict.get(b"OutputIntents") else {
1078 return Vec::new();
1079 };
1080 let Ok(intents_obj) = resolver.deref(intents_ref) else {
1081 return Vec::new();
1082 };
1083 let Some(intents_arr) = intents_obj.as_array() else {
1084 return Vec::new();
1085 };
1086 let mut out = Vec::new();
1087 for entry in intents_arr {
1088 let intent = match resolver.deref(entry) {
1089 Ok(o) => o,
1090 Err(_) => continue,
1091 };
1092 let Some(intent_dict) = intent.as_dict() else {
1093 continue;
1094 };
1095 let subtype = intent_dict
1096 .get_name(b"S")
1097 .map(|n| n.to_vec())
1098 .unwrap_or_else(|| b"GTS_PDFX".to_vec());
1099 let (profile_bytes, n) = match intent_dict.get(b"DestOutputProfile") {
1100 Some(profile_obj) => match resolver.stream_data_from_obj(profile_obj) {
1101 Ok(bytes) if bytes.len() >= 40 && &bytes[36..40] == b"acsp" => {
1102 let n = match &bytes[16..20] {
1103 b"GRAY" => 1,
1104 b"RGB " => 3,
1105 b"CMYK" => 4,
1106 b"Lab " => 3,
1107 _ => 3,
1108 };
1109 (Some(std::sync::Arc::new(bytes)), n)
1110 }
1111 _ => (None, 0),
1112 },
1113 None => (None, 0),
1114 };
1115 let get_string = |key: &[u8]| -> Option<Vec<u8>> {
1116 intent_dict.get(key).and_then(|o| match resolver.deref(o) {
1117 Ok(PdfObj::Str(s)) => Some(s),
1118 _ => None,
1119 })
1120 };
1121 out.push(OutputIntentRecord {
1122 subtype,
1123 output_condition_identifier: get_string(b"OutputConditionIdentifier"),
1124 output_condition: get_string(b"OutputCondition"),
1125 registry_name: get_string(b"RegistryName"),
1126 info: get_string(b"Info"),
1127 dest_output_profile: profile_bytes,
1128 n,
1129 });
1130 }
1131 out
1132}
1133
1134pub(crate) fn find_catalog(resolver: &Resolver) -> Option<PdfObj> {
1137 let xref_len = resolver.xref_len();
1138 for obj_num in 0..xref_len as u32 {
1139 if let Ok(obj) = resolver.resolve(obj_num, 0) {
1140 if let Some(dict) = obj.as_dict() {
1141 if dict.get_name(b"Type") == Some(b"Catalog") && dict.get(b"Pages").is_some() {
1142 return Some(obj);
1143 }
1144 }
1145 }
1146 }
1147 None
1148}
1149
1150fn has_pdf_header(data: &[u8]) -> bool {
1153 let search_range = data.len().min(1024);
1154 data[..search_range].windows(5).any(|w| w == b"%PDF-")
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159 use super::*;
1160
1161 #[test]
1162 fn not_a_pdf() {
1163 let result = PdfDocument::from_bytes(b"not a pdf");
1164 assert!(matches!(result, Err(PdfError::NotAPdf)));
1165 }
1166
1167 #[test]
1168 fn parse_minimal_pdf() {
1169 let pdf = build_minimal_pdf();
1170 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1171 assert_eq!(doc.page_count(), 1);
1172
1173 let (w, h) = doc.page_size(0).unwrap();
1174 assert_eq!(w, 612.0);
1175 assert_eq!(h, 792.0);
1176 }
1177
1178 #[test]
1179 fn page_out_of_range() {
1180 let pdf = build_minimal_pdf();
1181 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1182 assert!(matches!(
1183 doc.page_size(5),
1184 Err(PdfError::PageOutOfRange(5, 1))
1185 ));
1186 }
1187
1188 #[test]
1189 fn page_contents_empty() {
1190 let pdf = build_minimal_pdf();
1191 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1192 let contents = doc.page_contents(0).unwrap();
1193 assert!(contents.is_empty());
1195 }
1196
1197 #[test]
1198 #[ignore]
1199 fn dump_display_list() {
1200 use stet_fonts::geometry::PsPath;
1201 use stet_graphics::display_list::{DisplayElement, DisplayList};
1202
1203 fn path_bbox(path: &PsPath) -> String {
1204 use stet_fonts::geometry::PathSegment;
1205 let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
1206 for seg in &path.segments {
1207 let pts: Vec<(f64, f64)> = match seg {
1208 PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => vec![(*x, *y)],
1209 PathSegment::CurveTo {
1210 x1,
1211 y1,
1212 x2,
1213 y2,
1214 x3,
1215 y3,
1216 } => vec![(*x1, *y1), (*x2, *y2), (*x3, *y3)],
1217 PathSegment::ClosePath => vec![],
1218 };
1219 for (px, py) in pts {
1220 x0 = x0.min(px);
1221 y0 = y0.min(py);
1222 x1 = x1.max(px);
1223 y1 = y1.max(py);
1224 }
1225 }
1226 format!("bbox=({:.0},{:.0},{:.0},{:.0})", x0, y0, x1, y1)
1227 }
1228
1229 fn dump(list: &DisplayList, depth: usize) {
1230 let indent = " ".repeat(depth);
1231 for (i, elem) in list.elements().iter().enumerate() {
1232 match elem {
1233 DisplayElement::Fill { path, params } => {
1234 let c = ¶ms.color;
1235 let cmyk_str = if let Some((c2, m, y, k)) = params.color.native_cmyk {
1236 format!(" cmyk=({:.2},{:.2},{:.2},{:.2})", c2, m, y, k)
1237 } else {
1238 String::new()
1239 };
1240 eprintln!(
1241 "{indent}[{i}] Fill rgb=({:.2},{:.2},{:.2}){} op={} opm={} ch=0x{:x} a={:.2} {}",
1242 c.r,
1243 c.g,
1244 c.b,
1245 cmyk_str,
1246 params.overprint,
1247 params.overprint_mode,
1248 params.painted_channels,
1249 params.alpha,
1250 path_bbox(path)
1251 );
1252 }
1253 DisplayElement::Stroke { path, params } => {
1254 let c = ¶ms.color;
1255 eprintln!(
1256 "{indent}[{i}] Stroke rgb=({:.2},{:.2},{:.2}) {}",
1257 c.r,
1258 c.g,
1259 c.b,
1260 path_bbox(path)
1261 );
1262 }
1263 DisplayElement::Clip { path, .. } => {
1264 eprintln!("{indent}[{i}] Clip {}", path_bbox(path))
1265 }
1266 DisplayElement::InitClip => eprintln!("{indent}[{i}] InitClip"),
1267 DisplayElement::Image { params, .. } => {
1268 eprintln!("{indent}[{i}] Image {}x{}", params.width, params.height);
1269 }
1270 DisplayElement::ErasePage => eprintln!("{indent}[{i}] ErasePage"),
1271 DisplayElement::AxialShading { params } => {
1272 eprintln!(
1273 "{indent}[{i}] AxialShading cs={:?} stops={}",
1274 params.color_space,
1275 params.color_stops.len()
1276 );
1277 }
1278 DisplayElement::RadialShading { params } => {
1279 eprintln!(
1280 "{indent}[{i}] RadialShading cs={:?} stops={} ext=({},{}) c0=({:.1},{:.1}) r0={:.1} c1=({:.1},{:.1}) r1={:.1} bbox={:?} op={} ch=0x{:x}",
1281 params.color_space,
1282 params.color_stops.len(),
1283 params.extend_start,
1284 params.extend_end,
1285 params.x0,
1286 params.y0,
1287 params.r0,
1288 params.x1,
1289 params.y1,
1290 params.r1,
1291 params.bbox,
1292 params.overprint,
1293 params.painted_channels
1294 );
1295 if let Some(first) = params.color_stops.first() {
1297 eprintln!(
1298 "{indent} stop[0]: pos={:.3} rgb=({:.3},{:.3},{:.3}) raw={:?}",
1299 first.position,
1300 first.color.r,
1301 first.color.g,
1302 first.color.b,
1303 first.raw_components
1304 );
1305 }
1306 if let Some(last) = params.color_stops.last() {
1307 eprintln!(
1308 "{indent} stop[{}]: pos={:.3} rgb=({:.3},{:.3},{:.3}) raw={:?}",
1309 params.color_stops.len() - 1,
1310 last.position,
1311 last.color.r,
1312 last.color.g,
1313 last.color.b,
1314 last.raw_components
1315 );
1316 }
1317 let mid = params.color_stops.len() / 2;
1319 if mid > 0 && mid < params.color_stops.len() - 1 {
1320 let s = ¶ms.color_stops[mid];
1321 eprintln!(
1322 "{indent} stop[{mid}]: pos={:.3} rgb=({:.3},{:.3},{:.3}) raw={:?}",
1323 s.position, s.color.r, s.color.g, s.color.b, s.raw_components
1324 );
1325 }
1326 }
1327 DisplayElement::MeshShading { .. } => eprintln!("{indent}[{i}] MeshShading"),
1328 DisplayElement::PatchShading { .. } => eprintln!("{indent}[{i}] PatchShading"),
1329 DisplayElement::PatternFill { .. } => eprintln!("{indent}[{i}] PatternFill"),
1330 DisplayElement::Text { .. } => eprintln!("{indent}[{i}] Text"),
1331 DisplayElement::Group { elements, params } => {
1332 eprintln!(
1333 "{indent}[{i}] Group iso={} ko={} blend={} a={:.2} bbox=({:.0},{:.0},{:.0},{:.0}) children={}",
1334 params.isolated,
1335 params.knockout,
1336 params.blend_mode,
1337 params.alpha,
1338 params.bbox[0],
1339 params.bbox[1],
1340 params.bbox[2],
1341 params.bbox[3],
1342 elements.len()
1343 );
1344 dump(elements, depth + 1);
1345 }
1346 DisplayElement::SoftMasked {
1347 mask,
1348 content,
1349 params,
1350 ..
1351 } => {
1352 eprintln!(
1353 "{indent}[{i}] SoftMasked {:?} mask={} content={}",
1354 params.subtype,
1355 mask.len(),
1356 content.len()
1357 );
1358 eprintln!("{indent} MASK:");
1359 dump(mask, depth + 2);
1360 eprintln!("{indent} CONTENT:");
1361 dump(content, depth + 2);
1362 }
1363 DisplayElement::OcgGroup {
1364 elements,
1365 visibility,
1366 } => {
1367 eprintln!(
1368 "{indent}[{i}] OcgGroup vis={:?} children={}",
1369 visibility,
1370 elements.len()
1371 );
1372 dump(elements, depth + 1);
1373 }
1374 _ => {}
1375 }
1376 }
1377 }
1378
1379 let data = std::fs::read("../../pdf_samples/PDFX-ready_Output-Test_X4.pdf").unwrap();
1380 let doc = PdfDocument::from_bytes(&data).unwrap();
1381 let dl = doc.render_page(0, 72.0).unwrap();
1382 eprintln!("=== Display list: {} top-level elements ===", dl.len());
1383 dump(&dl, 0);
1384 }
1385
1386 fn build_pdf_with_outline() -> Vec<u8> {
1390 let mut pdf = Vec::new();
1391 pdf.extend(b"%PDF-1.4\n");
1392
1393 let mut offsets: Vec<usize> = Vec::new();
1394 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
1395 offsets.push(buf.len());
1396 buf.extend(body);
1397 };
1398
1399 push_obj(
1401 &mut pdf,
1402 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /Outlines 4 0 R >>\nendobj\n",
1403 );
1404 push_obj(
1406 &mut pdf,
1407 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
1408 );
1409 push_obj(
1411 &mut pdf,
1412 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
1413 );
1414 push_obj(
1416 &mut pdf,
1417 b"4 0 obj\n<< /Type /Outlines /First 5 0 R /Last 5 0 R /Count 3 >>\nendobj\n",
1418 );
1419 push_obj(
1421 &mut pdf,
1422 b"5 0 obj\n<< /Title (Chapter 1) /Parent 4 0 R /First 6 0 R /Last 7 0 R \
1423 /Count 2 /Dest [3 0 R /Fit] /F 2 >>\nendobj\n",
1424 );
1425 push_obj(
1427 &mut pdf,
1428 b"6 0 obj\n<< /Title (Section 1.1) /Parent 5 0 R /Next 7 0 R \
1429 /Dest [3 0 R /XYZ 72 700 1.0] /C [0.2 0.3 0.4] >>\nendobj\n",
1430 );
1431 push_obj(
1433 &mut pdf,
1434 b"7 0 obj\n<< /Title (Section 1.2) /Parent 5 0 R /Prev 6 0 R \
1435 /A << /S /URI /URI (https://example.com) >> /F 1 >>\nendobj\n",
1436 );
1437
1438 let xref_offset = pdf.len();
1439 pdf.extend(b"xref\n0 8\n");
1440 pdf.extend(b"0000000000 65535 f\r\n");
1441 for off in &offsets {
1442 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
1443 }
1444 pdf.extend(b"trailer\n<< /Size 8 /Root 1 0 R >>\n");
1445 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
1446
1447 pdf
1448 }
1449
1450 #[test]
1451 fn outline_basic_tree() {
1452 let pdf = build_pdf_with_outline();
1453 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1454 let outline = doc.outline();
1455
1456 assert_eq!(outline.len(), 1, "expected one top-level entry");
1457 let chapter = &outline[0];
1458 assert_eq!(chapter.title, "Chapter 1");
1459 assert!(chapter.open, "Chapter 1 has /Count 2 (positive = open)");
1460 assert!(chapter.style.bold);
1461 assert!(!chapter.style.italic);
1462 assert_eq!(chapter.children.len(), 2);
1463
1464 let s11 = &chapter.children[0];
1465 assert_eq!(s11.title, "Section 1.1");
1466 assert!(s11.action.is_none());
1467 match &s11.destination {
1468 Some(crate::Destination::PageView { page, view }) => {
1469 assert_eq!(*page, Some(0));
1470 assert!(matches!(view, crate::ViewSpec::Xyz { .. }));
1471 }
1472 other => panic!("expected PageView destination, got {other:?}"),
1473 }
1474 assert_eq!(s11.color, Some([0.2, 0.3, 0.4]));
1475
1476 let s12 = &chapter.children[1];
1477 assert_eq!(s12.title, "Section 1.2");
1478 assert!(s12.style.italic && !s12.style.bold);
1479 match &s12.action {
1480 Some(crate::Action::Uri { uri, is_map }) => {
1481 assert_eq!(uri, "https://example.com");
1482 assert!(!is_map);
1483 }
1484 other => panic!("expected URI action, got {other:?}"),
1485 }
1486 }
1487
1488 #[test]
1489 fn outline_caches_across_calls() {
1490 let pdf = build_pdf_with_outline();
1491 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1492 let a = doc.outline();
1493 let b = doc.outline();
1494 assert!(std::ptr::eq(a, b), "outline() must be cached");
1495 }
1496
1497 #[test]
1498 fn outline_empty_when_absent() {
1499 let pdf = build_minimal_pdf();
1500 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1501 assert!(doc.outline().is_empty());
1502 }
1503
1504 fn build_pdf_with_legacy_dests() -> Vec<u8> {
1507 let mut pdf = Vec::new();
1508 pdf.extend(b"%PDF-1.4\n");
1509
1510 let mut offsets: Vec<usize> = Vec::new();
1511 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
1512 offsets.push(buf.len());
1513 buf.extend(body);
1514 };
1515
1516 push_obj(
1518 &mut pdf,
1519 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /Dests 4 0 R >>\nendobj\n",
1520 );
1521 push_obj(
1523 &mut pdf,
1524 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
1525 );
1526 push_obj(
1528 &mut pdf,
1529 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
1530 );
1531 push_obj(
1533 &mut pdf,
1534 b"4 0 obj\n<< /Intro [3 0 R /Fit] /Glossary [3 0 R /XYZ 100 700 1.0] >>\nendobj\n",
1535 );
1536
1537 let xref_offset = pdf.len();
1538 pdf.extend(b"xref\n0 5\n");
1539 pdf.extend(b"0000000000 65535 f\r\n");
1540 for off in &offsets {
1541 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
1542 }
1543 pdf.extend(b"trailer\n<< /Size 5 /Root 1 0 R >>\n");
1544 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
1545
1546 pdf
1547 }
1548
1549 fn build_pdf_with_name_tree_dests() -> Vec<u8> {
1552 let mut pdf = Vec::new();
1553 pdf.extend(b"%PDF-1.4\n");
1554
1555 let mut offsets: Vec<usize> = Vec::new();
1556 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
1557 offsets.push(buf.len());
1558 buf.extend(body);
1559 };
1560
1561 push_obj(
1563 &mut pdf,
1564 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /Names 4 0 R >>\nendobj\n",
1565 );
1566 push_obj(
1568 &mut pdf,
1569 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
1570 );
1571 push_obj(
1573 &mut pdf,
1574 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
1575 );
1576 push_obj(&mut pdf, b"4 0 obj\n<< /Dests 5 0 R >>\nendobj\n");
1578 push_obj(
1580 &mut pdf,
1581 b"5 0 obj\n<< /Names [(Alpha) [3 0 R /Fit] (Beta) [3 0 R /XYZ 50 500 0]] >>\nendobj\n",
1582 );
1583
1584 let xref_offset = pdf.len();
1585 pdf.extend(b"xref\n0 6\n");
1586 pdf.extend(b"0000000000 65535 f\r\n");
1587 for off in &offsets {
1588 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
1589 }
1590 pdf.extend(b"trailer\n<< /Size 6 /Root 1 0 R >>\n");
1591 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
1592
1593 pdf
1594 }
1595
1596 fn build_pdf_with_dest_conflict() -> Vec<u8> {
1600 let mut pdf = Vec::new();
1601 pdf.extend(b"%PDF-1.4\n");
1602
1603 let mut offsets: Vec<usize> = Vec::new();
1604 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
1605 offsets.push(buf.len());
1606 buf.extend(body);
1607 };
1608
1609 push_obj(
1611 &mut pdf,
1612 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /Dests 4 0 R /Names 5 0 R >>\nendobj\n",
1613 );
1614 push_obj(
1615 &mut pdf,
1616 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
1617 );
1618 push_obj(
1619 &mut pdf,
1620 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
1621 );
1622 push_obj(&mut pdf, b"4 0 obj\n<< /Conflict [3 0 R /Fit] >>\nendobj\n");
1624 push_obj(&mut pdf, b"5 0 obj\n<< /Dests 6 0 R >>\nendobj\n");
1626 push_obj(
1627 &mut pdf,
1628 b"6 0 obj\n<< /Names [(Conflict) [3 0 R /FitB]] >>\nendobj\n",
1629 );
1630
1631 let xref_offset = pdf.len();
1632 pdf.extend(b"xref\n0 7\n");
1633 pdf.extend(b"0000000000 65535 f\r\n");
1634 for off in &offsets {
1635 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
1636 }
1637 pdf.extend(b"trailer\n<< /Size 7 /Root 1 0 R >>\n");
1638 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
1639
1640 pdf
1641 }
1642
1643 #[test]
1644 fn destinations_legacy_dict() {
1645 let pdf = build_pdf_with_legacy_dests();
1646 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1647 let dests = doc.destinations();
1648 assert_eq!(dests.len(), 2);
1649 match dests.get("Intro") {
1650 Some(crate::Destination::PageView { page, view }) => {
1651 assert_eq!(*page, Some(0));
1652 assert_eq!(*view, crate::ViewSpec::Fit);
1653 }
1654 other => panic!("expected PageView for Intro, got {other:?}"),
1655 }
1656 assert!(dests.contains_key("Glossary"));
1657 }
1658
1659 #[test]
1660 fn destinations_name_tree() {
1661 let pdf = build_pdf_with_name_tree_dests();
1662 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1663 let dests = doc.destinations();
1664 assert_eq!(dests.len(), 2);
1665 assert!(dests.contains_key("Alpha"));
1666 assert!(dests.contains_key("Beta"));
1667 }
1668
1669 #[test]
1670 fn destinations_legacy_overrides_name_tree() {
1671 let pdf = build_pdf_with_dest_conflict();
1672 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1673 let dests = doc.destinations();
1674 assert_eq!(dests.len(), 1);
1675 match dests.get("Conflict") {
1676 Some(crate::Destination::PageView { view, .. }) => {
1677 assert_eq!(
1678 *view,
1679 crate::ViewSpec::Fit,
1680 "legacy /Dests must override /Names /Dests"
1681 );
1682 }
1683 other => panic!("expected PageView, got {other:?}"),
1684 }
1685 }
1686
1687 #[test]
1688 fn destinations_caches_across_calls() {
1689 let pdf = build_pdf_with_legacy_dests();
1690 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1691 let a = doc.destinations();
1692 let b = doc.destinations();
1693 assert!(std::ptr::eq(a, b), "destinations() must be cached");
1694 }
1695
1696 fn build_pdf_with_annotations() -> Vec<u8> {
1701 let mut pdf = Vec::new();
1702 pdf.extend(b"%PDF-1.4\n");
1703
1704 let mut offsets: Vec<usize> = Vec::new();
1705 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
1706 offsets.push(buf.len());
1707 buf.extend(body);
1708 };
1709
1710 push_obj(
1712 &mut pdf,
1713 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
1714 );
1715 push_obj(
1717 &mut pdf,
1718 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
1719 );
1720 push_obj(
1722 &mut pdf,
1723 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1724 /Annots [4 0 R 5 0 R 6 0 R 7 0 R 8 0 R] >>\nendobj\n",
1725 );
1726 push_obj(
1728 &mut pdf,
1729 b"4 0 obj\n<< /Type /Annot /Subtype /Link /Rect [72 720 540 740] \
1730 /Border [0 0 1] \
1731 /A << /S /URI /URI (https://example.com) >> >>\nendobj\n",
1732 );
1733 push_obj(
1735 &mut pdf,
1736 b"5 0 obj\n<< /Type /Annot /Subtype /Text /Rect [100 600 120 620] \
1737 /Contents (A note) /Open true /Name /Comment /T (Scott) \
1738 /M (D:20260427120000Z) >>\nendobj\n",
1739 );
1740 push_obj(
1742 &mut pdf,
1743 b"6 0 obj\n<< /Type /Annot /Subtype /Highlight /Rect [72 500 300 520] \
1744 /QuadPoints [72 520 300 520 72 500 300 500] \
1745 /C [1.0 0.95 0.0] >>\nendobj\n",
1746 );
1747 push_obj(
1749 &mut pdf,
1750 b"7 0 obj\n<< /Type /Annot /Subtype /Square /Rect [200 400 300 450] \
1751 /IC [0.0 0.5 1.0] /C [0.0 0.0 0.0] /F 4 >>\nendobj\n",
1752 );
1753 push_obj(
1755 &mut pdf,
1756 b"8 0 obj\n<< /Type /Annot /Subtype /FreeText /Rect [72 300 300 350] \
1757 /Contents (Visible text) /DA (/Helv 10 Tf 0 g) /Q 1 \
1758 /IT /FreeTextCallout >>\nendobj\n",
1759 );
1760
1761 let xref_offset = pdf.len();
1762 pdf.extend(b"xref\n0 9\n");
1763 pdf.extend(b"0000000000 65535 f\r\n");
1764 for off in &offsets {
1765 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
1766 }
1767 pdf.extend(b"trailer\n<< /Size 9 /Root 1 0 R >>\n");
1768 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
1769
1770 pdf
1771 }
1772
1773 #[test]
1774 fn page_annotations_basic_subtypes() {
1775 let pdf = build_pdf_with_annotations();
1776 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1777 let annots = doc.page_annotations(0).unwrap();
1778 assert_eq!(annots.len(), 5);
1779
1780 let link = &annots[0];
1782 assert_eq!(link.kind, crate::AnnotationKind::Link);
1783 assert_eq!(link.rect, [72.0, 720.0, 540.0, 740.0]);
1784 match &link.kind_data {
1785 crate::AnnotationKindData::Link(l) => match &l.action {
1786 Some(crate::Action::Uri { uri, .. }) => {
1787 assert_eq!(uri, "https://example.com");
1788 }
1789 other => panic!("expected Uri action, got {other:?}"),
1790 },
1791 other => panic!("expected Link kind data, got {other:?}"),
1792 }
1793
1794 let text = &annots[1];
1796 assert_eq!(text.kind, crate::AnnotationKind::Text);
1797 assert_eq!(text.contents.as_deref(), Some("A note"));
1798 assert_eq!(text.title.as_deref(), Some("Scott"));
1799 match &text.kind_data {
1800 crate::AnnotationKindData::Text(t) => {
1801 assert!(t.open);
1802 assert_eq!(t.icon.as_deref(), Some("Comment"));
1803 }
1804 _ => panic!("expected Text kind"),
1805 }
1806 assert!(matches!(
1808 text.modified,
1809 Some(crate::AnnotationDate::Date(_))
1810 ));
1811
1812 let hl = &annots[2];
1814 assert_eq!(hl.kind, crate::AnnotationKind::Highlight);
1815 assert_eq!(
1816 hl.color,
1817 Some(crate::AnnotationColor::Rgb([1.0, 0.95, 0.0]))
1818 );
1819 match &hl.kind_data {
1820 crate::AnnotationKindData::Markup(m) => {
1821 assert_eq!(m.quad_points.len(), 1);
1822 }
1823 _ => panic!("expected Markup kind"),
1824 }
1825
1826 let sq = &annots[3];
1828 assert_eq!(sq.kind, crate::AnnotationKind::Square);
1829 assert!(sq.flags.print);
1830 match &sq.kind_data {
1831 crate::AnnotationKindData::Shape(s) => {
1832 assert_eq!(
1833 s.interior_color,
1834 Some(crate::AnnotationColor::Rgb([0.0, 0.5, 1.0]))
1835 );
1836 }
1837 _ => panic!("expected Shape kind"),
1838 }
1839
1840 let ft = &annots[4];
1842 assert_eq!(ft.kind, crate::AnnotationKind::FreeText);
1843 match &ft.kind_data {
1844 crate::AnnotationKindData::FreeText(f) => {
1845 assert_eq!(f.default_appearance.as_deref(), Some("/Helv 10 Tf 0 g"));
1846 assert_eq!(f.quadding, 1);
1847 assert_eq!(f.intent.as_deref(), Some("FreeTextCallout"));
1848 }
1849 _ => panic!("expected FreeText kind"),
1850 }
1851 }
1852
1853 #[test]
1854 fn page_annotations_caches_per_page() {
1855 let pdf = build_pdf_with_annotations();
1856 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1857 let a = doc.page_annotations(0).unwrap();
1858 let b = doc.page_annotations(0).unwrap();
1859 assert!(std::ptr::eq(a, b), "page_annotations(0) must be cached");
1860 }
1861
1862 #[test]
1863 fn page_annotations_out_of_range() {
1864 let pdf = build_pdf_with_annotations();
1865 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1866 assert!(doc.page_annotations(99).is_err());
1867 }
1868
1869 #[test]
1870 fn page_annotations_empty_when_absent() {
1871 let pdf = build_minimal_pdf();
1872 let doc = PdfDocument::from_bytes(&pdf).unwrap();
1873 let annots = doc.page_annotations(0).unwrap();
1874 assert!(annots.is_empty());
1875 }
1876
1877 fn build_pdf_with_form() -> Vec<u8> {
1882 let mut pdf = Vec::new();
1883 pdf.extend(b"%PDF-1.4\n");
1884
1885 let mut offsets: Vec<usize> = Vec::new();
1886 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
1887 offsets.push(buf.len());
1888 buf.extend(body);
1889 };
1890
1891 push_obj(
1893 &mut pdf,
1894 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n",
1895 );
1896 push_obj(
1898 &mut pdf,
1899 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
1900 );
1901 push_obj(
1903 &mut pdf,
1904 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1905 /Annots [5 0 R 6 0 R 9 0 R 10 0 R 12 0 R 14 0 R 15 0 R] >>\nendobj\n",
1906 );
1907 push_obj(
1909 &mut pdf,
1910 b"4 0 obj\n<< /Fields [5 0 R 6 0 R 7 0 R 11 0 R 13 0 R] \
1911 /NeedAppearances true /SigFlags 1 \
1912 /CO [(name)] /DA (/Helv 12 Tf 0 g) /Q 0 >>\nendobj\n",
1913 );
1914 push_obj(
1916 &mut pdf,
1917 b"5 0 obj\n<< /T (name) /TU (Full Name) /FT /Tx /Ff 0 \
1918 /MaxLen 50 /V (Scott) /DV () \
1919 /Subtype /Widget /Rect [72 720 300 740] /Type /Annot >>\nendobj\n",
1920 );
1921 push_obj(
1923 &mut pdf,
1924 b"6 0 obj\n<< /T (agree) /FT /Btn /Ff 0 /V /Yes \
1925 /Subtype /Widget /Rect [72 700 90 718] /Type /Annot >>\nendobj\n",
1926 );
1927 push_obj(
1929 &mut pdf,
1930 b"7 0 obj\n<< /T (color) /FT /Btn /Ff 49152 /V /Red \
1931 /Kids [9 0 R 10 0 R] /Opt [(Red) (Blue)] >>\nendobj\n",
1932 );
1933 push_obj(
1936 &mut pdf,
1937 b"9 0 obj\n<< /Parent 7 0 R /Subtype /Widget /Type /Annot \
1938 /Rect [72 680 90 698] /AS /Red >>\nendobj\n",
1939 );
1940 push_obj(
1942 &mut pdf,
1943 b"10 0 obj\n<< /Parent 7 0 R /Subtype /Widget /Type /Annot \
1944 /Rect [100 680 118 698] /AS /Off >>\nendobj\n",
1945 );
1946 push_obj(
1948 &mut pdf,
1949 b"11 0 obj\n<< /T (country) /FT /Ch /Ff 131072 /V (US) \
1950 /Opt [[(US) (United States)] [(GB) (United Kingdom)]] \
1951 /Subtype /Widget /Rect [72 660 200 678] /Type /Annot >>\nendobj\n",
1952 );
1953 push_obj(
1956 &mut pdf,
1957 b"13 0 obj\n<< /T (shipping) /Kids [14 0 R 15 0 R] >>\nendobj\n",
1958 );
1959 push_obj(
1961 &mut pdf,
1962 b"14 0 obj\n<< /T (street) /Parent 13 0 R /FT /Tx /V (123 Main) \
1963 /Subtype /Widget /Rect [72 640 300 658] /Type /Annot >>\nendobj\n",
1964 );
1965 push_obj(
1967 &mut pdf,
1968 b"15 0 obj\n<< /T (zip) /Parent 13 0 R /FT /Tx /V (12345) \
1969 /Subtype /Widget /Rect [72 620 200 638] /Type /Annot >>\nendobj\n",
1970 );
1971
1972 let xref_offset = pdf.len();
1973 let real_offsets: Vec<usize> = offsets;
1977 let mut entries: Vec<Option<usize>> = vec![None; 16];
1980 let declared = [1u32, 2, 3, 4, 5, 6, 7, 9, 10, 11, 13, 14, 15];
1983 for (i, &n) in declared.iter().enumerate() {
1984 entries[n as usize] = Some(real_offsets[i]);
1985 }
1986 pdf.extend(b"xref\n0 16\n");
1987 pdf.extend(b"0000000000 65535 f\r\n");
1988 for entry in entries.iter().skip(1) {
1989 match entry {
1990 Some(off) => pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes()),
1991 None => pdf.extend(b"0000000000 65535 f\r\n"),
1992 }
1993 }
1994 pdf.extend(b"trailer\n<< /Size 16 /Root 1 0 R >>\n");
1995 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
1996
1997 pdf
1998 }
1999
2000 #[test]
2001 fn form_basic_field_tree() {
2002 let pdf = build_pdf_with_form();
2003 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2004 let form = doc.form().expect("AcroForm should be present");
2005
2006 assert!(form.need_appearances);
2007 assert!(form.sig_flags.signatures_exist);
2008 assert!(!form.sig_flags.append_only);
2009 assert_eq!(form.calculation_order, vec!["name".to_string()]);
2010 assert_eq!(form.default_appearance.as_deref(), Some("/Helv 12 Tf 0 g"));
2011
2012 assert_eq!(form.fields.len(), 5);
2014
2015 let name = &form.fields[0];
2016 assert_eq!(name.name, "name");
2017 assert_eq!(name.alternate_name.as_deref(), Some("Full Name"));
2018 match &name.kind {
2019 crate::FieldKind::Text(t) => {
2020 assert_eq!(t.max_length, Some(50));
2021 assert!(!t.multiline && !t.password);
2022 }
2023 _ => panic!("name should be a Text field"),
2024 }
2025 assert_eq!(name.value, crate::FieldValue::Text("Scott".to_string()));
2026 assert_eq!(name.widget_obj_nums.len(), 1);
2028
2029 let agree = &form.fields[1];
2030 match &agree.kind {
2031 crate::FieldKind::Button(b) => {
2032 assert_eq!(b.button_type, crate::ButtonType::Checkbox);
2033 }
2034 _ => panic!("agree should be a Button"),
2035 }
2036 assert_eq!(agree.value, crate::FieldValue::Name("Yes".to_string()));
2037
2038 let color = &form.fields[2];
2039 assert_eq!(color.name, "color");
2040 match &color.kind {
2041 crate::FieldKind::Button(b) => {
2042 assert_eq!(b.button_type, crate::ButtonType::Radio);
2043 assert!(b.no_toggle_to_off);
2044 assert_eq!(b.options, vec!["Red".to_string(), "Blue".to_string()]);
2045 }
2046 _ => panic!("color should be a Radio group"),
2047 }
2048 assert_eq!(color.widget_obj_nums.len(), 2);
2050 assert!(
2051 color.children.is_empty(),
2052 "widget /Kids should not become children"
2053 );
2054
2055 let country = &form.fields[3];
2056 match &country.kind {
2057 crate::FieldKind::Choice(c) => {
2058 assert!(c.combo);
2059 assert_eq!(c.options.len(), 2);
2060 assert_eq!(c.options[0].export, "US");
2061 assert_eq!(c.options[0].display, "United States");
2062 }
2063 _ => panic!("country should be a Choice"),
2064 }
2065
2066 let shipping = &form.fields[4];
2067 assert_eq!(shipping.name, "shipping");
2068 assert!(matches!(shipping.kind, crate::FieldKind::Container));
2069 assert_eq!(shipping.children.len(), 2);
2070 assert_eq!(shipping.children[0].name, "shipping.street");
2071 assert_eq!(shipping.children[1].name, "shipping.zip");
2072 assert_eq!(
2073 shipping.children[0].value,
2074 crate::FieldValue::Text("123 Main".to_string())
2075 );
2076 }
2077
2078 #[test]
2079 fn form_caches_across_calls() {
2080 let pdf = build_pdf_with_form();
2081 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2082 let a = doc.form().unwrap();
2083 let b = doc.form().unwrap();
2084 assert!(std::ptr::eq(a, b), "form() must be cached");
2085 }
2086
2087 #[test]
2088 fn form_absent_returns_none() {
2089 let pdf = build_minimal_pdf();
2090 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2091 assert!(doc.form().is_none());
2092 }
2093
2094 fn build_pdf_with_page_boxes() -> Vec<u8> {
2097 let mut pdf = Vec::new();
2098 pdf.extend(b"%PDF-1.4\n");
2099
2100 let mut offsets: Vec<usize> = Vec::new();
2101 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
2102 offsets.push(buf.len());
2103 buf.extend(body);
2104 };
2105
2106 push_obj(
2107 &mut pdf,
2108 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
2109 );
2110 push_obj(
2111 &mut pdf,
2112 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
2113 );
2114 push_obj(
2117 &mut pdf,
2118 b"3 0 obj\n<< /Type /Page /Parent 2 0 R \
2119 /MediaBox [0 0 612 792] \
2120 /CropBox [10 10 602 782] \
2121 /BleedBox [5 5 607 787] \
2122 /TrimBox [20 20 592 772] \
2123 /ArtBox [30 30 582 762] \
2124 /Rotate 90 /UserUnit 1.5 /Dur 5.0 \
2125 /Trans << /S /Wipe >> /AA << /O 5 0 R >> >>\nendobj\n",
2126 );
2127
2128 let xref_offset = pdf.len();
2129 pdf.extend(b"xref\n0 4\n");
2130 pdf.extend(b"0000000000 65535 f\r\n");
2131 for off in &offsets {
2132 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
2133 }
2134 pdf.extend(b"trailer\n<< /Size 4 /Root 1 0 R >>\n");
2135 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
2136
2137 pdf
2138 }
2139
2140 #[test]
2141 fn page_boxes_full_set() {
2142 let pdf = build_pdf_with_page_boxes();
2143 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2144 let pb = doc.page_boxes(0).unwrap();
2145 assert_eq!(pb.media_box, [0.0, 0.0, 612.0, 792.0]);
2146 assert_eq!(pb.crop_box, Some([10.0, 10.0, 602.0, 782.0]));
2147 assert_eq!(pb.bleed_box, Some([5.0, 5.0, 607.0, 787.0]));
2148 assert_eq!(pb.trim_box, Some([20.0, 20.0, 592.0, 772.0]));
2149 assert_eq!(pb.art_box, Some([30.0, 30.0, 582.0, 762.0]));
2150 assert_eq!(pb.rotate, 90);
2151 assert_eq!(pb.user_unit, 1.5);
2152 assert_eq!(pb.duration, Some(5.0));
2153 assert!(pb.has_transition);
2154 assert!(pb.has_additional_actions);
2155 }
2156
2157 #[test]
2158 fn page_boxes_minimal_defaults() {
2159 let pdf = build_minimal_pdf();
2160 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2161 let pb = doc.page_boxes(0).unwrap();
2162 assert_eq!(pb.media_box, [0.0, 0.0, 612.0, 792.0]);
2163 assert!(pb.crop_box.is_none());
2165 assert!(pb.bleed_box.is_none());
2166 assert!(pb.trim_box.is_none());
2167 assert!(pb.art_box.is_none());
2168 assert_eq!(pb.rotate, 0);
2169 assert_eq!(pb.user_unit, 1.0);
2170 assert!(pb.duration.is_none());
2171 assert!(!pb.has_transition);
2172 assert!(!pb.has_additional_actions);
2173 }
2174
2175 #[test]
2176 fn page_boxes_out_of_range() {
2177 let pdf = build_minimal_pdf();
2178 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2179 assert!(doc.page_boxes(99).is_err());
2180 }
2181
2182 fn build_pdf_with_embedded_file() -> Vec<u8> {
2187 let mut pdf = Vec::new();
2188 pdf.extend(b"%PDF-1.4\n");
2189
2190 let mut offsets: Vec<usize> = Vec::new();
2191 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
2192 offsets.push(buf.len());
2193 buf.extend(body);
2194 };
2195
2196 push_obj(
2198 &mut pdf,
2199 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /Names 4 0 R >>\nendobj\n",
2200 );
2201 push_obj(
2203 &mut pdf,
2204 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
2205 );
2206 push_obj(
2208 &mut pdf,
2209 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
2210 );
2211 push_obj(&mut pdf, b"4 0 obj\n<< /EmbeddedFiles 5 0 R >>\nendobj\n");
2213 push_obj(
2215 &mut pdf,
2216 b"5 0 obj\n<< /Names [(data.csv) 6 0 R] >>\nendobj\n",
2217 );
2218 push_obj(
2220 &mut pdf,
2221 b"6 0 obj\n<< /Type /Filespec /F (data.csv) /UF (data.csv) \
2222 /Desc (Sample CSV) /AFRelationship /Data \
2223 /EF << /F 7 0 R /UF 7 0 R >> >>\nendobj\n",
2224 );
2225 let payload = b"id,name\n1,a\n";
2228 let stream_header = b"7 0 obj\n<< /Type /EmbeddedFile /Subtype /text#2Fcsv \
2229 /Length 12 /Params << /Size 12 >> >>\nstream\n";
2230 offsets.push(pdf.len());
2231 pdf.extend(stream_header);
2232 pdf.extend(payload);
2233 pdf.extend(b"\nendstream\nendobj\n");
2234
2235 let xref_offset = pdf.len();
2236 pdf.extend(b"xref\n0 8\n");
2237 pdf.extend(b"0000000000 65535 f\r\n");
2238 for off in &offsets {
2239 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
2240 }
2241 pdf.extend(b"trailer\n<< /Size 8 /Root 1 0 R >>\n");
2242 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
2243
2244 pdf
2245 }
2246
2247 #[test]
2248 fn embedded_files_basic() {
2249 let pdf = build_pdf_with_embedded_file();
2250 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2251 let map = doc.embedded_files();
2252 assert_eq!(map.len(), 1);
2253
2254 let ef = map.get("data.csv").expect("data.csv missing");
2255 assert_eq!(ef.name, "data.csv");
2256 assert_eq!(ef.filename.as_deref(), Some("data.csv"));
2257 assert_eq!(ef.unicode_filename.as_deref(), Some("data.csv"));
2258 assert_eq!(ef.description.as_deref(), Some("Sample CSV"));
2259 assert_eq!(ef.relationship, Some(crate::AfRelationship::Data));
2260 assert_eq!(ef.mime_type.as_deref(), Some("text/csv"));
2261 assert_eq!(ef.size, Some(12));
2262
2263 let bytes = doc.embedded_file_bytes("data.csv").unwrap();
2264 assert_eq!(&bytes[..], b"id,name\n1,a\n");
2265 }
2266
2267 #[test]
2268 fn embedded_files_caches_across_calls() {
2269 let pdf = build_pdf_with_embedded_file();
2270 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2271 let a = doc.embedded_files();
2272 let b = doc.embedded_files();
2273 assert!(std::ptr::eq(a, b), "embedded_files() must be cached");
2274 }
2275
2276 fn build_pdf_with_cyclic_outline() -> Vec<u8> {
2279 let mut pdf = Vec::new();
2280 pdf.extend(b"%PDF-1.4\n");
2281
2282 let mut offsets: Vec<usize> = Vec::new();
2283 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
2284 offsets.push(buf.len());
2285 buf.extend(body);
2286 };
2287
2288 push_obj(
2289 &mut pdf,
2290 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /Outlines 4 0 R >>\nendobj\n",
2291 );
2292 push_obj(
2293 &mut pdf,
2294 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
2295 );
2296 push_obj(
2297 &mut pdf,
2298 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
2299 );
2300 push_obj(
2301 &mut pdf,
2302 b"4 0 obj\n<< /Type /Outlines /First 5 0 R /Last 5 0 R /Count 1 >>\nendobj\n",
2303 );
2304 push_obj(
2306 &mut pdf,
2307 b"5 0 obj\n<< /Title (Loop) /Parent 4 0 R /Next 5 0 R >>\nendobj\n",
2308 );
2309
2310 let xref_offset = pdf.len();
2311 pdf.extend(b"xref\n0 6\n");
2312 pdf.extend(b"0000000000 65535 f\r\n");
2313 for off in &offsets {
2314 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
2315 }
2316 pdf.extend(b"trailer\n<< /Size 6 /Root 1 0 R >>\n");
2317 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
2318
2319 pdf
2320 }
2321
2322 #[test]
2323 fn warning_emitted_for_outline_cycle() {
2324 let pdf = build_pdf_with_cyclic_outline();
2325 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2326 let outline = doc.outline();
2328 assert_eq!(outline.len(), 1);
2330 let warnings = doc.parse_warnings();
2331 assert!(
2332 warnings
2333 .iter()
2334 .any(|w| matches!(w.phase, crate::ParsePhase::Outline)
2335 && w.severity == crate::Severity::Warning
2336 && w.message.contains("cycle")),
2337 "expected outline cycle warning, got: {:?}",
2338 warnings.iter().collect::<Vec<_>>()
2339 );
2340 }
2341
2342 fn build_pdf_with_rectless_annot() -> Vec<u8> {
2345 let mut pdf = Vec::new();
2346 pdf.extend(b"%PDF-1.4\n");
2347
2348 let mut offsets: Vec<usize> = Vec::new();
2349 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
2350 offsets.push(buf.len());
2351 buf.extend(body);
2352 };
2353
2354 push_obj(
2355 &mut pdf,
2356 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
2357 );
2358 push_obj(
2359 &mut pdf,
2360 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
2361 );
2362 push_obj(
2363 &mut pdf,
2364 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
2365 /Annots [4 0 R] >>\nendobj\n",
2366 );
2367 push_obj(
2369 &mut pdf,
2370 b"4 0 obj\n<< /Type /Annot /Subtype /Text /Contents (no rect) >>\nendobj\n",
2371 );
2372
2373 let xref_offset = pdf.len();
2374 pdf.extend(b"xref\n0 5\n");
2375 pdf.extend(b"0000000000 65535 f\r\n");
2376 for off in &offsets {
2377 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
2378 }
2379 pdf.extend(b"trailer\n<< /Size 5 /Root 1 0 R >>\n");
2380 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
2381
2382 pdf
2383 }
2384
2385 #[test]
2386 fn warning_emitted_for_rectless_annotation() {
2387 let pdf = build_pdf_with_rectless_annot();
2388 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2389 let annots = doc.page_annotations(0).unwrap();
2390 assert_eq!(annots.len(), 0);
2392 let warnings = doc.parse_warnings();
2393 assert!(
2394 warnings.iter().any(
2395 |w| matches!(w.phase, crate::ParsePhase::Annotations { page: 0 })
2396 && w.message.contains("/Rect")
2397 ),
2398 "expected /Rect warning, got: {:?}",
2399 warnings.iter().collect::<Vec<_>>()
2400 );
2401 }
2402
2403 #[test]
2404 fn parse_warnings_empty_for_clean_document() {
2405 let pdf = build_minimal_pdf();
2406 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2407 let _ = doc.metadata();
2409 let _ = doc.viewer_preferences();
2410 let _ = doc.outline();
2411 let _ = doc.destinations();
2412 let _ = doc.page_annotations(0).unwrap();
2413 let _ = doc.form();
2414 let _ = doc.embedded_files();
2415 let _ = doc.page_boxes(0).unwrap();
2416 let warnings = doc.parse_warnings();
2417 assert_eq!(
2418 warnings.len(),
2419 0,
2420 "got: {:?}",
2421 warnings.iter().collect::<Vec<_>>()
2422 );
2423 }
2424
2425 #[test]
2426 fn embedded_files_empty_when_absent() {
2427 let pdf = build_minimal_pdf();
2428 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2429 assert!(doc.embedded_files().is_empty());
2430 assert!(doc.embedded_file_bytes("missing").is_err());
2431 }
2432
2433 #[test]
2434 fn form_widgets_appear_in_page_annotations() {
2435 let pdf = build_pdf_with_form();
2436 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2437 let form = doc.form().unwrap();
2438 let annots = doc.page_annotations(0).unwrap();
2439
2440 let widget_annot_subtypes: Vec<_> = annots
2444 .iter()
2445 .filter(|a| a.kind == crate::AnnotationKind::Widget)
2446 .collect();
2447 assert!(
2448 !widget_annot_subtypes.is_empty(),
2449 "expected widget annotations on page"
2450 );
2451
2452 let color_field = form.fields.iter().find(|f| f.name == "color").unwrap();
2457 assert_eq!(color_field.widget_obj_nums.len(), 2);
2458 }
2459
2460 #[test]
2461 fn resolve_named_destination_returns_dest() {
2462 let pdf = build_pdf_with_legacy_dests();
2463 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2464 let d = doc.resolve_named_destination("Intro").unwrap();
2465 match d {
2466 crate::Destination::PageView { page, view } => {
2467 assert_eq!(page, Some(0));
2468 assert_eq!(view, crate::ViewSpec::Fit);
2469 }
2470 _ => panic!("expected PageView"),
2471 }
2472 assert!(doc.resolve_named_destination("MissingName").is_none());
2473 }
2474
2475 fn build_pdf_with_layers() -> Vec<u8> {
2488 let mut pdf = Vec::new();
2489 pdf.extend(b"%PDF-1.6\n");
2490
2491 let mut offsets: Vec<usize> = Vec::new();
2492 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
2493 offsets.push(buf.len());
2494 buf.extend(body);
2495 };
2496
2497 push_obj(
2499 &mut pdf,
2500 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /OCProperties << \
2501 /OCGs [5 0 R 6 0 R 7 0 R 8 0 R 9 0 R] \
2502 /D << /Order [5 0 R 6 0 R 7 0 R 8 0 R 9 0 R] \
2503 /OFF [9 0 R] /Locked [6 0 R] >> \
2504 >> >>\nendobj\n",
2505 );
2506 push_obj(
2508 &mut pdf,
2509 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
2510 );
2511 push_obj(
2513 &mut pdf,
2514 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
2515 );
2516 push_obj(&mut pdf, b"4 0 obj\nnull\nendobj\n");
2518
2519 push_obj(
2521 &mut pdf,
2522 b"5 0 obj\n<< /Type /OCG /Name (Background) >>\nendobj\n",
2523 );
2524
2525 let mut obj6 = Vec::new();
2528 obj6.extend(b"6 0 obj\n<< /Type /OCG ");
2529 obj6.extend(b"/Name <FEFF005400650073007400200394> ");
2530 obj6.extend(b"/Intent [/View /Design] ");
2532 obj6.extend(b"/Usage << ");
2534 obj6.extend(b"/View << /ViewState /ON >> ");
2535 obj6.extend(b"/Print << /PrintState /OFF /Subtype /Watermark >> ");
2536 obj6.extend(b"/Export << /ExportState /ON >> ");
2537 obj6.extend(b"/Zoom << /min 0.5 /max 4.0 >> ");
2538 obj6.extend(b"/Language << /Lang (en-US) /Preferred /ON >> ");
2539 obj6.extend(b"/User << /Type /Ind /Name (alice) >> ");
2540 obj6.extend(b"/PageElement << /Subtype /HF >> ");
2541 obj6.extend(b"/CreatorInfo << /Creator (CADtool) /Subtype /Technical >> ");
2542 obj6.extend(b">> ");
2543 obj6.extend(b">>\nendobj\n");
2544 push_obj(&mut pdf, &obj6);
2545
2546 push_obj(
2548 &mut pdf,
2549 b"7 0 obj\n<< /Type /OCG /Name (DesignLayer) /Intent [/Design] >>\nendobj\n",
2550 );
2551
2552 push_obj(
2554 &mut pdf,
2555 b"8 0 obj\n<< /Type /OCG /Name (Custom) /Intent /Custom >>\nendobj\n",
2556 );
2557
2558 push_obj(
2560 &mut pdf,
2561 b"9 0 obj\n<< /Type /OCG /Name (HiddenLayer) \
2562 /CreatorInfo << /Creator (Inkscape) /Subtype /Artwork >> \
2563 /Usage << /User << /Type /Org /Name [(group-a) (group-b)] >> >> \
2564 >>\nendobj\n",
2565 );
2566
2567 let xref_offset = pdf.len();
2568 pdf.extend(b"xref\n0 10\n");
2569 pdf.extend(b"0000000000 65535 f\r\n");
2570 for off in &offsets {
2571 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
2572 }
2573 pdf.extend(b"trailer\n<< /Size 10 /Root 1 0 R >>\n");
2574 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
2575 pdf
2576 }
2577
2578 #[test]
2579 fn layers_basic_enumeration() {
2580 let pdf = build_pdf_with_layers();
2581 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2582 let layers = doc.layers();
2583 assert_eq!(layers.len(), 5, "expected 5 OCGs, got {}", layers.len());
2584
2585 assert!(
2587 layers
2588 .iter()
2589 .find(|l| l.ocg_id == 5)
2590 .unwrap()
2591 .default_visible
2592 );
2593 assert!(
2594 layers
2595 .iter()
2596 .find(|l| l.ocg_id == 6)
2597 .unwrap()
2598 .default_visible
2599 );
2600 assert!(
2601 !layers
2602 .iter()
2603 .find(|l| l.ocg_id == 9)
2604 .unwrap()
2605 .default_visible
2606 );
2607
2608 assert!(layers.iter().find(|l| l.ocg_id == 6).unwrap().locked);
2610 assert!(!layers.iter().find(|l| l.ocg_id == 5).unwrap().locked);
2611 assert!(!layers.iter().find(|l| l.ocg_id == 9).unwrap().locked);
2612 }
2613
2614 #[test]
2615 fn layers_name_decoding() {
2616 let pdf = build_pdf_with_layers();
2617 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2618
2619 let bg = doc.layer(5).unwrap();
2621 assert_eq!(bg.name, "Background");
2622
2623 let utf16 = doc.layer(6).unwrap();
2625 assert_eq!(utf16.name, "Test \u{0394}");
2626 }
2627
2628 #[test]
2629 fn layers_intent_variants() {
2630 let pdf = build_pdf_with_layers();
2631 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2632
2633 assert_eq!(doc.layer(5).unwrap().intent, LayerIntent::View);
2635
2636 match &doc.layer(6).unwrap().intent {
2638 LayerIntent::Multiple(names) => {
2639 assert_eq!(names.len(), 2);
2640 assert_eq!(names[0], "View");
2641 assert_eq!(names[1], "Design");
2642 }
2643 other => panic!("expected Multiple, got {other:?}"),
2644 }
2645
2646 assert_eq!(doc.layer(7).unwrap().intent, LayerIntent::Design);
2648
2649 match &doc.layer(8).unwrap().intent {
2651 LayerIntent::Other(s) => assert_eq!(s, "Custom"),
2652 other => panic!("expected Other, got {other:?}"),
2653 }
2654 }
2655
2656 #[test]
2657 fn layers_full_usage_dict() {
2658 let pdf = build_pdf_with_layers();
2659 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2660 let l = doc.layer(6).unwrap();
2661
2662 let view = l.usage.view.expect("view sub-dict");
2664 assert_eq!(view.state, UsageState::On);
2665
2666 let print = l.usage.print.as_ref().expect("print sub-dict");
2668 assert_eq!(print.state, UsageState::Off);
2669 assert_eq!(print.subtype.as_deref(), Some("Watermark"));
2670
2671 let export = l.usage.export.expect("export sub-dict");
2673 assert_eq!(export.state, UsageState::On);
2674
2675 let zoom = l.usage.zoom.expect("zoom sub-dict");
2677 assert_eq!(zoom.min, Some(0.5));
2678 assert_eq!(zoom.max, Some(4.0));
2679
2680 let lang = l.usage.language.as_ref().expect("language sub-dict");
2682 assert_eq!(lang.lang, "en-US");
2683 assert!(lang.preferred);
2684
2685 let user = l.usage.user.as_ref().expect("user sub-dict");
2687 assert_eq!(user.user_type.as_deref(), Some("Ind"));
2688 assert_eq!(user.names, vec!["alice".to_string()]);
2689
2690 assert_eq!(l.usage.page_element, Some(PageElementSubtype::HeaderFooter));
2692
2693 let ci = l.usage.creator_info.as_ref().expect("creator_info");
2695 assert_eq!(ci.creator, "CADtool");
2696 assert_eq!(ci.subtype.as_deref(), Some("Technical"));
2697 }
2698
2699 #[test]
2700 fn layers_creator_info_on_ocg() {
2701 let pdf = build_pdf_with_layers();
2702 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2703 let hidden = doc.layer(9).unwrap();
2704
2705 let ci = hidden.creator_info.as_ref().expect("creator_info");
2707 assert_eq!(ci.creator, "Inkscape");
2708 assert_eq!(ci.subtype.as_deref(), Some("Artwork"));
2709
2710 let user = hidden.usage.user.as_ref().expect("user sub-dict");
2712 assert_eq!(user.user_type.as_deref(), Some("Org"));
2713 assert_eq!(
2714 user.names,
2715 vec!["group-a".to_string(), "group-b".to_string()]
2716 );
2717 }
2718
2719 #[test]
2720 fn layers_empty_when_no_oc_properties() {
2721 let pdf = build_minimal_pdf();
2722 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2723 assert!(doc.layers().is_empty());
2724 assert!(doc.layer(42).is_none());
2725 }
2726
2727 #[test]
2728 fn layers_caches_across_calls() {
2729 let pdf = build_pdf_with_layers();
2730 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2731 let first = doc.layers().as_ptr();
2732 let second = doc.layers().as_ptr();
2733 assert_eq!(first, second, "layers() should return a cached slice");
2734 }
2735
2736 fn build_pdf_with_layer_hierarchy() -> Vec<u8> {
2750 let mut pdf = Vec::new();
2751 pdf.extend(b"%PDF-1.6\n");
2752
2753 let mut offsets: Vec<usize> = Vec::new();
2754 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
2755 offsets.push(buf.len());
2756 buf.extend(body);
2757 };
2758
2759 let mut cat = Vec::new();
2761 cat.extend(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /OCProperties << ");
2762 cat.extend(b"/OCGs [5 0 R 6 0 R 7 0 R 8 0 R 9 0 R] ");
2763 cat.extend(b"/D << /Name (Default) /Creator (TestApp) ");
2764 cat.extend(b"/BaseState /OFF /ON [5 0 R 7 0 R] /OFF [9 0 R] ");
2765 cat.extend(b"/Locked [6 0 R] ");
2766 cat.extend(b"/Intent /View ");
2767 cat.extend(b"/ListMode /VisiblePages ");
2768 cat.extend(b"/Order [5 0 R (Backgrounds) [6 0 R 7 0 R] 8 0 R [9 0 R] [5 0 R]] ");
2772 cat.extend(b"/RBGroups [[6 0 R 7 0 R]] ");
2773 cat.extend(b"/AS [<< /Event /Print /Category [/Print] /OCGs [9 0 R] >>] ");
2774 cat.extend(b">> ");
2775 cat.extend(b"/Configs [<< /Name (Alternate) /Creator (Other) ");
2776 cat.extend(b"/BaseState /ON /OFF [5 0 R] /Intent /Design ");
2777 cat.extend(b"/Order [6 0 R 7 0 R] >>] ");
2778 cat.extend(b">> >>\nendobj\n");
2779 push_obj(&mut pdf, &cat);
2780
2781 push_obj(
2782 &mut pdf,
2783 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
2784 );
2785 push_obj(
2786 &mut pdf,
2787 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
2788 );
2789 push_obj(&mut pdf, b"4 0 obj\nnull\nendobj\n");
2791 for (n, name) in (5u32..=9).zip(["L5", "L6", "L7", "L8", "L9"]) {
2793 let body = format!("{n} 0 obj\n<< /Type /OCG /Name ({name}) >>\nendobj\n");
2794 push_obj(&mut pdf, body.as_bytes());
2795 }
2796
2797 let xref_offset = pdf.len();
2798 pdf.extend(b"xref\n0 10\n");
2799 pdf.extend(b"0000000000 65535 f\r\n");
2800 for off in &offsets {
2801 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
2802 }
2803 pdf.extend(b"trailer\n<< /Size 10 /Root 1 0 R >>\n");
2804 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
2805 pdf
2806 }
2807
2808 #[test]
2809 fn configurations_default_and_alternate() {
2810 let pdf = build_pdf_with_layer_hierarchy();
2811 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2812 let configs = doc.configurations();
2813 assert_eq!(configs.len(), 2, "default + one alternate");
2814
2815 let d = doc.default_configuration().unwrap();
2816 assert_eq!(d.index, 0);
2817 assert_eq!(d.name.as_deref(), Some("Default"));
2818 assert_eq!(d.creator.as_deref(), Some("TestApp"));
2819 assert_eq!(d.base_state, BaseState::Off);
2820 assert_eq!(d.on, vec![5, 7]);
2821 assert_eq!(d.off, vec![9]);
2822 assert_eq!(d.locked, vec![6]);
2823 assert_eq!(d.list_mode, ListMode::VisiblePages);
2824 assert_eq!(d.intent, LayerIntent::View);
2825
2826 let alt = doc.configuration(1).unwrap();
2827 assert_eq!(alt.index, 1);
2828 assert_eq!(alt.name.as_deref(), Some("Alternate"));
2829 assert_eq!(alt.creator.as_deref(), Some("Other"));
2830 assert_eq!(alt.base_state, BaseState::On);
2831 assert_eq!(alt.off, vec![5]);
2832 assert_eq!(alt.intent, LayerIntent::Design);
2833 }
2834
2835 #[test]
2836 fn order_mixes_flat_labelled_header_and_anonymous_sections() {
2837 let pdf = build_pdf_with_layer_hierarchy();
2838 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2839 let tree = doc.layer_tree();
2840
2841 assert_eq!(tree.nodes.len(), 4, "expected 4 top-level nodes");
2848
2849 match &tree.nodes[0] {
2850 LayerTreeNode::Layer(id) => assert_eq!(*id, 5),
2851 other => panic!("nodes[0]: expected Layer(5), got {other:?}"),
2852 }
2853 match &tree.nodes[1] {
2854 LayerTreeNode::Section {
2855 label,
2856 header_layer,
2857 children,
2858 } => {
2859 assert_eq!(label.as_deref(), Some("Backgrounds"));
2860 assert!(header_layer.is_none());
2861 assert_eq!(children.len(), 2);
2862 if let LayerTreeNode::Layer(id) = &children[0] {
2863 assert_eq!(*id, 6);
2864 } else {
2865 panic!("children[0] not a Layer");
2866 }
2867 if let LayerTreeNode::Layer(id) = &children[1] {
2868 assert_eq!(*id, 7);
2869 } else {
2870 panic!("children[1] not a Layer");
2871 }
2872 }
2873 other => panic!("nodes[1]: expected labelled Section, got {other:?}"),
2874 }
2875 match &tree.nodes[2] {
2876 LayerTreeNode::Section {
2877 label,
2878 header_layer,
2879 children,
2880 } => {
2881 assert!(label.is_none());
2882 assert_eq!(*header_layer, Some(8));
2883 assert_eq!(children.len(), 1);
2884 if let LayerTreeNode::Layer(id) = &children[0] {
2885 assert_eq!(*id, 9);
2886 } else {
2887 panic!("children[0] not a Layer");
2888 }
2889 }
2890 other => panic!("nodes[2]: expected header-layer Section, got {other:?}"),
2891 }
2892 match &tree.nodes[3] {
2893 LayerTreeNode::Section {
2894 label,
2895 header_layer,
2896 children,
2897 } => {
2898 assert!(label.is_none());
2899 assert!(header_layer.is_none());
2900 assert_eq!(children.len(), 1);
2901 if let LayerTreeNode::Layer(id) = &children[0] {
2902 assert_eq!(*id, 5);
2903 } else {
2904 panic!("children[0] not a Layer");
2905 }
2906 }
2907 other => panic!("nodes[3]: expected anonymous Section, got {other:?}"),
2908 }
2909 }
2910
2911 #[test]
2912 fn auto_state_rules_parsed() {
2913 let pdf = build_pdf_with_layer_hierarchy();
2914 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2915 let d = doc.default_configuration().unwrap();
2916 assert_eq!(d.auto_state.len(), 1);
2917 let rule = &d.auto_state[0];
2918 assert_eq!(rule.event, AutoStateEvent::Print);
2919 assert_eq!(rule.categories, vec!["Print".to_string()]);
2920 assert_eq!(rule.ocgs, vec![9]);
2921 }
2922
2923 #[test]
2924 fn rb_groups_parsed() {
2925 let pdf = build_pdf_with_layer_hierarchy();
2926 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2927 let d = doc.default_configuration().unwrap();
2928 assert_eq!(d.rb_groups, vec![vec![6, 7]]);
2929 }
2930
2931 #[test]
2932 fn layer_tree_alternate_config_differs() {
2933 let pdf = build_pdf_with_layer_hierarchy();
2934 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2935 let alt = doc.configuration(1).unwrap();
2936 assert_eq!(alt.order.nodes.len(), 2);
2938 assert!(matches!(alt.order.nodes[0], LayerTreeNode::Layer(6)));
2939 assert!(matches!(alt.order.nodes[1], LayerTreeNode::Layer(7)));
2940 }
2941
2942 #[test]
2943 fn configurations_empty_when_no_oc_properties() {
2944 let pdf = build_minimal_pdf();
2945 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2946 assert!(doc.configurations().is_empty());
2947 assert!(doc.default_configuration().is_none());
2948 assert!(doc.configuration(0).is_none());
2949 assert!(doc.layer_tree().nodes.is_empty());
2950 }
2951
2952 #[test]
2953 fn configurations_caches_across_calls() {
2954 let pdf = build_pdf_with_layer_hierarchy();
2955 let doc = PdfDocument::from_bytes(&pdf).unwrap();
2956 let first = doc.configurations().as_ptr();
2957 let second = doc.configurations().as_ptr();
2958 assert_eq!(first, second);
2959 }
2960
2961 #[test]
2962 fn order_with_dangling_string_emits_warning() {
2963 let mut pdf = Vec::new();
2966 pdf.extend(b"%PDF-1.6\n");
2967 let mut offsets: Vec<usize> = Vec::new();
2968 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
2969 offsets.push(buf.len());
2970 buf.extend(body);
2971 };
2972
2973 push_obj(
2974 &mut pdf,
2975 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /OCProperties << \
2976 /OCGs [4 0 R] /D << /Order [(Orphan) 4 0 R] >> >> >>\nendobj\n",
2977 );
2978 push_obj(
2979 &mut pdf,
2980 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
2981 );
2982 push_obj(
2983 &mut pdf,
2984 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
2985 );
2986 push_obj(
2987 &mut pdf,
2988 b"4 0 obj\n<< /Type /OCG /Name (Solo) >>\nendobj\n",
2989 );
2990 let xref_offset = pdf.len();
2991 pdf.extend(b"xref\n0 5\n");
2992 pdf.extend(b"0000000000 65535 f\r\n");
2993 for off in &offsets {
2994 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
2995 }
2996 pdf.extend(b"trailer\n<< /Size 5 /Root 1 0 R >>\n");
2997 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
2998
2999 let doc = PdfDocument::from_bytes(&pdf).unwrap();
3000 let tree = doc.layer_tree();
3001 assert_eq!(tree.nodes.len(), 1);
3003 assert!(matches!(tree.nodes[0], LayerTreeNode::Layer(4)));
3004
3005 let warnings = doc.parse_warnings();
3006 assert!(
3007 warnings
3008 .iter()
3009 .any(|w| matches!(w.phase, ParsePhase::Layers) && w.message.contains("Orphan")),
3010 "expected a Layers warning about the orphan string, got {warnings:?}"
3011 );
3012 }
3013
3014 fn build_pdf_with_layered_content() -> Vec<u8> {
3020 let mut pdf = Vec::new();
3021 pdf.extend(b"%PDF-1.6\n");
3022 let mut offsets: Vec<usize> = Vec::new();
3023 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
3024 offsets.push(buf.len());
3025 buf.extend(body);
3026 };
3027
3028 push_obj(
3030 &mut pdf,
3031 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /OCProperties << \
3032 /OCGs [5 0 R] /D << /Order [5 0 R] >> >> >>\nendobj\n",
3033 );
3034 push_obj(
3036 &mut pdf,
3037 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
3038 );
3039 push_obj(
3041 &mut pdf,
3042 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] \
3043 /Contents 4 0 R /Resources << /Properties << /OC1 5 0 R >> >> >>\nendobj\n",
3044 );
3045 let stream = b"q 1 0 0 rg 0 0 50 50 re f Q\n\
3047 /OC /OC1 BDC q 0 0 1 rg 50 50 50 50 re f Q EMC";
3048 let stream_obj = format!(
3049 "4 0 obj\n<< /Length {} >>\nstream\n{}\nendstream\nendobj\n",
3050 stream.len(),
3051 std::str::from_utf8(stream).unwrap()
3052 );
3053 push_obj(&mut pdf, stream_obj.as_bytes());
3054 push_obj(
3056 &mut pdf,
3057 b"5 0 obj\n<< /Type /OCG /Name (BlueLayer) >>\nendobj\n",
3058 );
3059
3060 let xref_offset = pdf.len();
3061 pdf.extend(b"xref\n0 6\n");
3062 pdf.extend(b"0000000000 65535 f\r\n");
3063 for off in &offsets {
3064 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
3065 }
3066 pdf.extend(b"trailer\n<< /Size 6 /Root 1 0 R >>\n");
3067 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
3068 pdf
3069 }
3070
3071 fn sample_pixel(rgba: &[u8], w: u32, x: u32, y: u32) -> [u8; 4] {
3074 let i = (y as usize * w as usize + x as usize) * 4;
3075 [rgba[i], rgba[i + 1], rgba[i + 2], rgba[i + 3]]
3076 }
3077
3078 #[cfg(feature = "render")]
3079 #[test]
3080 fn render_default_layer_set_matches_implicit_render() {
3081 let pdf = build_pdf_with_layered_content();
3082 let doc = PdfDocument::from_bytes(&pdf).unwrap();
3083
3084 let (rgba_default, w, h) = doc.render_page_to_rgba(0, 72.0).unwrap();
3085 let (rgba_with_set, w2, h2) = doc
3086 .render_page_to_rgba_with_layers(0, 72.0, &LayerSet::new())
3087 .unwrap();
3088
3089 assert_eq!(w, w2);
3090 assert_eq!(h, h2);
3091 assert_eq!(
3092 rgba_default, rgba_with_set,
3093 "empty LayerSet must render byte-identical to plain render_page_to_rgba"
3094 );
3095 }
3096
3097 #[cfg(feature = "render")]
3098 #[test]
3099 fn render_layer_off_hides_layer_content() {
3100 let pdf = build_pdf_with_layered_content();
3101 let doc = PdfDocument::from_bytes(&pdf).unwrap();
3102
3103 let (rgba_on, w, _h) = doc.render_page_to_rgba(0, 72.0).unwrap();
3107 let on_pixel = sample_pixel(&rgba_on, w, 75, 25);
3108 assert!(
3109 on_pixel[2] > 200 && on_pixel[0] < 50,
3110 "expected blue layer pixel, got rgba={:?}",
3111 on_pixel
3112 );
3113
3114 let mut layers = layers::layer_set_from_document(&doc);
3116 layers.set(5, false);
3117
3118 let (rgba_off, _w, _h) = doc
3119 .render_page_to_rgba_with_layers(0, 72.0, &layers)
3120 .unwrap();
3121 let off_pixel = sample_pixel(&rgba_off, w, 75, 25);
3122 assert!(
3123 off_pixel[0] >= 250 && off_pixel[1] >= 250 && off_pixel[2] >= 250,
3124 "expected layer-off pixel to be background white, got rgba={:?}",
3125 off_pixel
3126 );
3127
3128 let baseline = sample_pixel(&rgba_off, w, 25, 75);
3130 assert!(
3131 baseline[0] > 200 && baseline[1] < 50 && baseline[2] < 50,
3132 "baseline red rect should still render, got rgba={:?}",
3133 baseline
3134 );
3135 }
3136
3137 #[test]
3138 fn layer_set_from_document_populates_defaults() {
3139 let pdf = build_pdf_with_layers();
3140 let doc = PdfDocument::from_bytes(&pdf).unwrap();
3141 let set = layers::layer_set_from_document(&doc);
3142
3143 assert_eq!(set.get(5), Some(true));
3145 assert_eq!(set.get(6), Some(true));
3146 assert_eq!(set.get(9), Some(false));
3147 }
3148
3149 fn build_pdf_with_ocmd(policy: &[u8], off: &[u32]) -> Vec<u8> {
3156 let mut pdf = Vec::new();
3157 pdf.extend(b"%PDF-1.6\n");
3158 let mut offsets: Vec<usize> = Vec::new();
3159 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
3160 offsets.push(buf.len());
3161 buf.extend(body);
3162 };
3163
3164 let mut off_arr = String::from("[");
3165 for id in off {
3166 off_arr.push_str(&format!("{id} 0 R "));
3167 }
3168 off_arr.push(']');
3169
3170 let cat = format!(
3171 "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /OCProperties << \
3172 /OCGs [5 0 R 6 0 R] /D << /Order [5 0 R 6 0 R] /OFF {off_arr} >> >> >>\nendobj\n"
3173 );
3174 push_obj(&mut pdf, cat.as_bytes());
3175 push_obj(
3176 &mut pdf,
3177 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
3178 );
3179 push_obj(
3181 &mut pdf,
3182 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] \
3183 /Contents 4 0 R /Resources << /Properties << /OC1 7 0 R >> >> >>\nendobj\n",
3184 );
3185 let stream = b"q 1 0 0 rg 0 0 50 50 re f Q\n\
3187 /OC /OC1 BDC q 0 0 1 rg 50 50 50 50 re f Q EMC";
3188 let stream_obj = format!(
3189 "4 0 obj\n<< /Length {} >>\nstream\n{}\nendstream\nendobj\n",
3190 stream.len(),
3191 std::str::from_utf8(stream).unwrap()
3192 );
3193 push_obj(&mut pdf, stream_obj.as_bytes());
3194 push_obj(
3195 &mut pdf,
3196 b"5 0 obj\n<< /Type /OCG /Name (LayerA) >>\nendobj\n",
3197 );
3198 push_obj(
3199 &mut pdf,
3200 b"6 0 obj\n<< /Type /OCG /Name (LayerB) >>\nendobj\n",
3201 );
3202 let ocmd = format!(
3204 "7 0 obj\n<< /Type /OCMD /OCGs [5 0 R 6 0 R] /P /{} >>\nendobj\n",
3205 std::str::from_utf8(policy).unwrap()
3206 );
3207 push_obj(&mut pdf, ocmd.as_bytes());
3208
3209 let xref_offset = pdf.len();
3210 pdf.extend(b"xref\n0 8\n");
3211 pdf.extend(b"0000000000 65535 f\r\n");
3212 for off_v in &offsets {
3213 pdf.extend(format!("{:010} 00000 n\r\n", off_v).as_bytes());
3214 }
3215 pdf.extend(b"trailer\n<< /Size 8 /Root 1 0 R >>\n");
3216 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
3217 pdf
3218 }
3219
3220 fn ocmd_visibility(pdf: &[u8]) -> OcgVisibility {
3221 let doc = PdfDocument::from_bytes(pdf).unwrap();
3222 let dl = doc.render_page(0, 72.0).unwrap();
3223 for elem in dl.elements() {
3224 if let stet_graphics::display_list::DisplayElement::OcgGroup { visibility, .. } = elem {
3225 return visibility.clone();
3226 }
3227 }
3228 panic!("expected an OcgGroup in display list")
3229 }
3230
3231 #[test]
3232 fn ocmd_emits_membership_with_policy() {
3233 let v = ocmd_visibility(&build_pdf_with_ocmd(b"AllOn", &[]));
3234 match v {
3235 OcgVisibility::Membership {
3236 ocg_ids,
3237 policy,
3238 default_visible,
3239 } => {
3240 assert_eq!(ocg_ids, vec![5, 6]);
3241 assert_eq!(policy, MembershipPolicy::AllOn);
3242 assert!(default_visible);
3244 }
3245 other => panic!("expected Membership, got {other:?}"),
3246 }
3247
3248 let v = ocmd_visibility(&build_pdf_with_ocmd(b"AnyOff", &[]));
3250 match v {
3251 OcgVisibility::Membership {
3252 policy,
3253 default_visible,
3254 ..
3255 } => {
3256 assert_eq!(policy, MembershipPolicy::AnyOff);
3257 assert!(!default_visible);
3258 }
3259 other => panic!("expected Membership, got {other:?}"),
3260 }
3261
3262 let v = ocmd_visibility(&build_pdf_with_ocmd(b"AllOff", &[5, 6]));
3264 match v {
3265 OcgVisibility::Membership {
3266 policy,
3267 default_visible,
3268 ..
3269 } => {
3270 assert_eq!(policy, MembershipPolicy::AllOff);
3271 assert!(default_visible);
3272 }
3273 other => panic!("expected Membership, got {other:?}"),
3274 }
3275 }
3276
3277 #[test]
3278 fn ocmd_membership_truth_table_via_layer_set() {
3279 let pdf = build_pdf_with_ocmd(b"AllOn", &[]);
3281 let v = ocmd_visibility(&pdf);
3282
3283 for a in [false, true] {
3284 for b in [false, true] {
3285 let mut s = LayerSet::new();
3286 s.set(5, a);
3287 s.set(6, b);
3288 let expected = a && b;
3289 assert_eq!(
3290 s.evaluate(&v),
3291 expected,
3292 "AllOn(5={a}, 6={b}) expected {expected}"
3293 );
3294 }
3295 }
3296 }
3297
3298 fn build_pdf_with_ve_expression() -> Vec<u8> {
3301 let mut pdf = Vec::new();
3302 pdf.extend(b"%PDF-1.6\n");
3303 let mut offsets: Vec<usize> = Vec::new();
3304 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
3305 offsets.push(buf.len());
3306 buf.extend(body);
3307 };
3308
3309 push_obj(
3310 &mut pdf,
3311 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /OCProperties << \
3312 /OCGs [5 0 R 6 0 R 7 0 R] /D << /Order [5 0 R 6 0 R 7 0 R] >> >> >>\nendobj\n",
3313 );
3314 push_obj(
3315 &mut pdf,
3316 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
3317 );
3318 push_obj(
3319 &mut pdf,
3320 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] \
3321 /Contents 4 0 R /Resources << /Properties << /OC1 8 0 R >> >> >>\nendobj\n",
3322 );
3323 let stream = b"/OC /OC1 BDC q 0 0 1 rg 0 0 100 100 re f Q EMC";
3324 let stream_obj = format!(
3325 "4 0 obj\n<< /Length {} >>\nstream\n{}\nendstream\nendobj\n",
3326 stream.len(),
3327 std::str::from_utf8(stream).unwrap()
3328 );
3329 push_obj(&mut pdf, stream_obj.as_bytes());
3330 push_obj(&mut pdf, b"5 0 obj\n<< /Type /OCG /Name (A) >>\nendobj\n");
3331 push_obj(&mut pdf, b"6 0 obj\n<< /Type /OCG /Name (B) >>\nendobj\n");
3332 push_obj(&mut pdf, b"7 0 obj\n<< /Type /OCG /Name (C) >>\nendobj\n");
3333 push_obj(
3337 &mut pdf,
3338 b"8 0 obj\n<< /Type /OCMD /VE [/And 5 0 R [/Or 6 0 R [/Not 7 0 R]]] >>\nendobj\n",
3339 );
3340
3341 let xref_offset = pdf.len();
3342 pdf.extend(b"xref\n0 9\n");
3343 pdf.extend(b"0000000000 65535 f\r\n");
3344 for off in &offsets {
3345 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
3346 }
3347 pdf.extend(b"trailer\n<< /Size 9 /Root 1 0 R >>\n");
3348 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
3349 pdf
3350 }
3351
3352 #[test]
3353 fn ve_expression_parsed_into_visibility_expr() {
3354 let pdf = build_pdf_with_ve_expression();
3355 let v = ocmd_visibility(&pdf);
3356 match &v {
3357 OcgVisibility::Expression { expr, .. } => match expr {
3358 VisibilityExpr::And(args) => {
3359 assert_eq!(args.len(), 2);
3360 assert!(matches!(args[0], VisibilityExpr::Layer(5)));
3361 match &args[1] {
3362 VisibilityExpr::Or(or_args) => {
3363 assert_eq!(or_args.len(), 2);
3364 assert!(matches!(or_args[0], VisibilityExpr::Layer(6)));
3365 match &or_args[1] {
3366 VisibilityExpr::Not(inner) => {
3367 assert!(matches!(**inner, VisibilityExpr::Layer(7)));
3368 }
3369 other => panic!("expected Not, got {other:?}"),
3370 }
3371 }
3372 other => panic!("expected Or, got {other:?}"),
3373 }
3374 }
3375 other => panic!("expected And, got {other:?}"),
3376 },
3377 other => panic!("expected Expression, got {other:?}"),
3378 }
3379
3380 for a in [false, true] {
3382 for b in [false, true] {
3383 for c in [false, true] {
3384 let mut s = LayerSet::new();
3385 s.set(5, a);
3386 s.set(6, b);
3387 s.set(7, c);
3388 let expected = a && (b || !c);
3389 assert_eq!(
3390 s.evaluate(&v),
3391 expected,
3392 "(a={a}, b={b}, c={c}) expected {expected}"
3393 );
3394 }
3395 }
3396 }
3397 }
3398
3399 #[test]
3400 fn malformed_ve_falls_back_to_membership() {
3401 let mut pdf = Vec::new();
3403 pdf.extend(b"%PDF-1.6\n");
3404 let mut offsets: Vec<usize> = Vec::new();
3405 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
3406 offsets.push(buf.len());
3407 buf.extend(body);
3408 };
3409 push_obj(
3410 &mut pdf,
3411 b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /OCProperties << \
3412 /OCGs [5 0 R] /D << /Order [5 0 R] >> >> >>\nendobj\n",
3413 );
3414 push_obj(
3415 &mut pdf,
3416 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
3417 );
3418 push_obj(
3419 &mut pdf,
3420 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] \
3421 /Contents 4 0 R /Resources << /Properties << /OC1 6 0 R >> >> >>\nendobj\n",
3422 );
3423 let stream = b"/OC /OC1 BDC q 1 0 0 rg 0 0 100 100 re f Q EMC";
3424 let stream_obj = format!(
3425 "4 0 obj\n<< /Length {} >>\nstream\n{}\nendstream\nendobj\n",
3426 stream.len(),
3427 std::str::from_utf8(stream).unwrap()
3428 );
3429 push_obj(&mut pdf, stream_obj.as_bytes());
3430 push_obj(&mut pdf, b"5 0 obj\n<< /Type /OCG /Name (X) >>\nendobj\n");
3431 push_obj(
3433 &mut pdf,
3434 b"6 0 obj\n<< /Type /OCMD /VE [/Not 5 0 R 5 0 R] /OCGs [5 0 R] /P /AnyOn >>\nendobj\n",
3435 );
3436 let xref_offset = pdf.len();
3437 pdf.extend(b"xref\n0 7\n");
3438 pdf.extend(b"0000000000 65535 f\r\n");
3439 for off in &offsets {
3440 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
3441 }
3442 pdf.extend(b"trailer\n<< /Size 7 /Root 1 0 R >>\n");
3443 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
3444
3445 let v = ocmd_visibility(&pdf);
3446 match v {
3447 OcgVisibility::Membership {
3448 ocg_ids, policy, ..
3449 } => {
3450 assert_eq!(ocg_ids, vec![5]);
3451 assert_eq!(policy, MembershipPolicy::AnyOn);
3452 }
3453 other => panic!("expected fallback to Membership, got {other:?}"),
3454 }
3455 }
3456
3457 #[test]
3458 fn layer_set_from_configuration_applies_base_state() {
3459 let pdf = build_pdf_with_layer_hierarchy();
3460 let doc = PdfDocument::from_bytes(&pdf).unwrap();
3461
3462 let d = layers::layer_set_from_configuration(&doc, 0).unwrap();
3464 assert_eq!(d.get(5), Some(true));
3465 assert_eq!(d.get(6), Some(false));
3466 assert_eq!(d.get(7), Some(true));
3467 assert_eq!(d.get(8), Some(false));
3468 assert_eq!(d.get(9), Some(false));
3469
3470 let alt = layers::layer_set_from_configuration(&doc, 1).unwrap();
3472 assert_eq!(alt.get(5), Some(false));
3473 assert_eq!(alt.get(6), Some(true));
3474 assert_eq!(alt.get(7), Some(true));
3475
3476 assert!(layers::layer_set_from_configuration(&doc, 99).is_none());
3478 }
3479
3480 fn build_pdf_with_auto_state_rules() -> Vec<u8> {
3490 let mut pdf = Vec::new();
3491 pdf.extend(b"%PDF-1.6\n");
3492 let mut offsets: Vec<usize> = Vec::new();
3493 let mut push_obj = |buf: &mut Vec<u8>, body: &[u8]| {
3494 offsets.push(buf.len());
3495 buf.extend(body);
3496 };
3497
3498 let mut cat = Vec::new();
3499 cat.extend(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /OCProperties << ");
3500 cat.extend(b"/OCGs [5 0 R 6 0 R 7 0 R] /D << /Order [5 0 R 6 0 R 7 0 R] ");
3501 cat.extend(b"/AS [");
3502 cat.extend(b"<< /Event /Print /Category [/Print] /OCGs [5 0 R 6 0 R] >> ");
3504 cat.extend(b"<< /Event /Export /Category [/Export] /OCGs [5 0 R] >>");
3506 cat.extend(b"] ");
3507 cat.extend(b">> >>\nendobj\n");
3508 push_obj(&mut pdf, &cat);
3509 push_obj(
3510 &mut pdf,
3511 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
3512 );
3513 push_obj(
3514 &mut pdf,
3515 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>\nendobj\n",
3516 );
3517 push_obj(&mut pdf, b"4 0 obj\nnull\nendobj\n");
3519 push_obj(
3521 &mut pdf,
3522 b"5 0 obj\n<< /Type /OCG /Name (Watermark) \
3523 /Usage << /Print << /PrintState /OFF /Subtype /Watermark >> \
3524 /Export << /ExportState /OFF >> \
3525 >> >>\nendobj\n",
3526 );
3527 push_obj(
3529 &mut pdf,
3530 b"6 0 obj\n<< /Type /OCG /Name (ScreenOnly) \
3531 /Usage << /View << /ViewState /ON >> \
3532 /Print << /PrintState /OFF >> \
3533 >> >>\nendobj\n",
3534 );
3535 push_obj(
3537 &mut pdf,
3538 b"7 0 obj\n<< /Type /OCG /Name (Hint) \
3539 /Usage << /Export << /ExportState /OFF >> >> >>\nendobj\n",
3540 );
3541
3542 let xref_offset = pdf.len();
3543 pdf.extend(b"xref\n0 8\n");
3544 pdf.extend(b"0000000000 65535 f\r\n");
3545 for off in &offsets {
3546 pdf.extend(format!("{:010} 00000 n\r\n", off).as_bytes());
3547 }
3548 pdf.extend(b"trailer\n<< /Size 8 /Root 1 0 R >>\n");
3549 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
3550 pdf
3551 }
3552
3553 #[test]
3554 fn layer_set_for_view_keeps_defaults() {
3555 let pdf = build_pdf_with_auto_state_rules();
3556 let doc = PdfDocument::from_bytes(&pdf).unwrap();
3557 let set = doc.layer_set_for(RenderIntent::View);
3558
3559 assert_eq!(set.get(5), Some(true));
3561 assert_eq!(set.get(6), Some(true));
3562 assert_eq!(set.get(7), Some(true));
3563 }
3564
3565 #[test]
3566 fn layer_set_for_print_applies_off_rules() {
3567 let pdf = build_pdf_with_auto_state_rules();
3568 let doc = PdfDocument::from_bytes(&pdf).unwrap();
3569 let set = doc.layer_set_for(RenderIntent::Print);
3570
3571 assert_eq!(set.get(5), Some(false));
3573 assert_eq!(set.get(6), Some(false));
3574 assert_eq!(set.get(7), Some(true));
3576 }
3577
3578 #[test]
3579 fn layer_set_for_export_only_touches_listed_ocgs() {
3580 let pdf = build_pdf_with_auto_state_rules();
3581 let doc = PdfDocument::from_bytes(&pdf).unwrap();
3582 let set = doc.layer_set_for(RenderIntent::Export);
3583
3584 assert_eq!(set.get(5), Some(false));
3586 assert_eq!(set.get(6), Some(true));
3588 assert_eq!(set.get(7), Some(true));
3590 }
3591
3592 #[test]
3593 fn layer_set_for_with_no_oc_properties_returns_empty() {
3594 let pdf = build_minimal_pdf();
3595 let doc = PdfDocument::from_bytes(&pdf).unwrap();
3596 let set = doc.layer_set_for(RenderIntent::Print);
3597 assert!(set.is_empty());
3598 }
3599
3600 fn build_minimal_pdf() -> Vec<u8> {
3602 let mut pdf = Vec::new();
3603 pdf.extend(b"%PDF-1.4\n");
3604
3605 let obj1_offset = pdf.len();
3607 pdf.extend(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
3608
3609 let obj2_offset = pdf.len();
3611 pdf.extend(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
3612
3613 let obj3_offset = pdf.len();
3615 pdf.extend(b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n");
3616
3617 let xref_offset = pdf.len();
3619 pdf.extend(b"xref\n0 4\n");
3620 pdf.extend(b"0000000000 65535 f\r\n");
3621 pdf.extend(format!("{:010} 00000 n\r\n", obj1_offset).as_bytes());
3622 pdf.extend(format!("{:010} 00000 n\r\n", obj2_offset).as_bytes());
3623 pdf.extend(format!("{:010} 00000 n\r\n", obj3_offset).as_bytes());
3624 pdf.extend(b"trailer\n<< /Size 4 /Root 1 0 R >>\n");
3625 pdf.extend(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
3626
3627 pdf
3628 }
3629}