1use stet_core::context::Context;
8use stet_core::device::OutputDevice;
9use stet_fonts::geometry::PsPath;
10use stet_graphics::device::{ClipParams, FillParams, ImageParams, StrokeParams};
11use stet_graphics::display_list::DisplayList;
12
13use std::collections::{HashMap, HashSet};
14
15use crate::content_stream::{self, ContentStreamResult, ShadingRef};
16use crate::font_embedder;
17use crate::font_tracker::FontTracker;
18use crate::image_ops::ImageXObject;
19use crate::pdf_objects::PdfObj;
20use crate::pdf_writer::PdfWriter;
21use crate::shading_ops;
22
23struct PageData {
26 display_list: DisplayList,
27 width_pts: f64,
28 height_pts: f64,
29 page_w: u32,
30 page_h: u32,
31 dpi: f64,
32 trim_box: Option<(f64, f64, f64, f64)>,
33}
34
35pub struct PdfDevice {
38 pages: Vec<PageData>,
39 page_w: u32,
40 page_h: u32,
41 dpi: f64,
42 output_path: Option<String>,
43 pending_trim_box: Option<(f64, f64, f64, f64)>,
44 #[allow(dead_code)]
47 output_profile: Option<Vec<u8>>,
48 output_intents: Vec<stet_graphics::document_structure::OutputIntentRecord>,
52 emit_page_box_clip: bool,
59}
60
61impl PdfDevice {
62 pub fn new(width: u32, height: u32, dpi: f64) -> Self {
64 Self {
65 pages: Vec::new(),
66 page_w: width,
67 page_h: height,
68 dpi,
69 output_path: None,
70 pending_trim_box: None,
71 output_profile: None,
72 output_intents: Vec::new(),
73 emit_page_box_clip: true,
74 }
75 }
76
77 pub fn set_emit_page_box_clip(&mut self, on: bool) {
82 self.emit_page_box_clip = on;
83 }
84
85 pub fn set_output_intents(
92 &mut self,
93 intents: Vec<stet_graphics::document_structure::OutputIntentRecord>,
94 ) {
95 self.output_intents = intents;
96 }
97
98 pub fn set_trim_box(&mut self, llx: f64, lly: f64, urx: f64, ury: f64) {
100 self.pending_trim_box = Some((llx, lly, urx, ury));
101 }
102
103 pub fn set_page_size(&mut self, width: u32, height: u32) {
109 self.page_w = width;
110 self.page_h = height;
111 }
112
113 #[deprecated(
122 note = "OutputIntent emission is temporarily disabled pending PDF/X-4 support; calling this has no effect"
123 )]
124 pub fn set_output_profile(&mut self, bytes: Vec<u8>) {
125 self.output_profile = Some(bytes);
126 }
127
128 pub fn take_pdf_bytes(&self) -> Option<Vec<u8>> {
133 if self.pages.is_empty() {
134 return None;
135 }
136 let (writer, catalog_ref, info_ref) = self.build_pdf(None).ok()?;
137 let mut buf = Vec::new();
138 writer
139 .write_pdf(&mut buf, catalog_ref, Some(info_ref))
140 .ok()?;
141 Some(buf)
142 }
143
144 pub fn take_pdf_bytes_with_context(&self, ctx: &Context) -> Option<Vec<u8>> {
146 if self.pages.is_empty() {
147 return None;
148 }
149 let (writer, catalog_ref, info_ref) = self.build_pdf(Some(ctx)).ok()?;
150 let mut buf = Vec::new();
151 writer
152 .write_pdf(&mut buf, catalog_ref, Some(info_ref))
153 .ok()?;
154 Some(buf)
155 }
156
157 fn write_pdf(&self, ctx: Option<&Context>) -> Result<(), String> {
159 let path = self.output_path.as_deref().ok_or("no output path set")?;
160 let (writer, catalog_ref, info_ref) = self.build_pdf(ctx)?;
161
162 let file = std::fs::File::create(path).map_err(|e| format!("create {}: {}", path, e))?;
163 let mut bw = std::io::BufWriter::new(file);
164 writer
165 .write_pdf(&mut bw, catalog_ref, Some(info_ref))
166 .map_err(|e| format!("write {}: {}", path, e))?;
167
168 eprintln!("PDF written: {} ({} pages)", path, self.pages.len());
169 Ok(())
170 }
171
172 fn default_producer() -> String {
180 format!("stet {}", env!("CARGO_PKG_VERSION"))
181 }
182
183 fn build_info_dict(&self, ctx: Option<&Context>) -> Vec<(Vec<u8>, PdfObj)> {
189 let docinfo = ctx.map(|c| collect_docinfo(c)).unwrap_or_default();
190
191 let producer = docinfo
192 .producer
193 .clone()
194 .unwrap_or_else(Self::default_producer);
195 let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![(
196 b"Producer".to_vec(),
197 PdfObj::LitString(producer.into_bytes()),
198 )];
199
200 let title = docinfo.title.clone().or_else(|| {
202 self.output_path
203 .as_deref()
204 .and_then(|p| std::path::Path::new(p).file_stem())
205 .and_then(|s| s.to_str())
206 .map(|s| s.to_string())
207 });
208 if let Some(t) = title {
209 entries.push((b"Title".to_vec(), PdfObj::LitString(t.into_bytes())));
210 }
211
212 for (key, value) in [
213 (&b"Author"[..], &docinfo.author),
214 (&b"Subject"[..], &docinfo.subject),
215 (&b"Keywords"[..], &docinfo.keywords),
216 (&b"Creator"[..], &docinfo.creator),
217 ] {
218 if let Some(v) = value {
219 entries.push((key.to_vec(), PdfObj::LitString(v.clone().into_bytes())));
220 }
221 }
222
223 let creation_date = docinfo
225 .creation_date_string()
226 .unwrap_or_else(default_now_pdf_date);
227 entries.push((
228 b"CreationDate".to_vec(),
229 PdfObj::LitString(creation_date.into_bytes()),
230 ));
231
232 if let Some(md) = docinfo.mod_date_string() {
233 entries.push((b"ModDate".to_vec(), PdfObj::LitString(md.into_bytes())));
234 }
235
236 if let Some(t) = docinfo.trapped {
237 let name: &[u8] = match t {
238 stet_graphics::document_structure::TrappedState::True => b"True",
239 stet_graphics::document_structure::TrappedState::False => b"False",
240 stet_graphics::document_structure::TrappedState::Unknown => b"Unknown",
241 _ => b"Unknown",
242 };
243 entries.push((b"Trapped".to_vec(), PdfObj::Name(name.to_vec())));
244 }
245
246 entries
247 }
248
249 fn build_pdf(&self, ctx: Option<&Context>) -> Result<(PdfWriter, u32, u32), String> {
251 let mut writer = PdfWriter::new();
252
253 let catalog_ref = writer.alloc_obj();
255 let pages_ref = writer.alloc_obj();
256
257 let mut font_tracker = FontTracker::new();
259
260 let mut page_results: Vec<(ContentStreamResult, &PageData)> = Vec::new();
262 for page in &self.pages {
263 let result = content_stream::build_content_stream(
264 &page.display_list,
265 page.page_w,
266 page.page_h,
267 page.dpi,
268 ctx,
269 &mut font_tracker,
270 self.emit_page_box_clip,
271 );
272 page_results.push((result, page));
273 }
274
275 let font_obj_map: HashMap<String, u32> =
277 self.embed_all_fonts(&mut writer, &font_tracker, ctx);
278
279 let mut ocg_id_to_ref: HashMap<u32, u32> = HashMap::new();
285 let mut ocg_default_off: HashSet<u32> = HashSet::new();
286 let mut ocg_order: Vec<u32> = Vec::new();
287 for (result, _) in &page_results {
288 for marker in &result.ocg_marker_refs {
289 let mut visit = |ocg_id: u32, default_visible: bool| {
290 if let std::collections::hash_map::Entry::Vacant(e) =
291 ocg_id_to_ref.entry(ocg_id)
292 {
293 let r = writer.add_object(&PdfObj::Dict(vec![
294 (b"Type".to_vec(), PdfObj::name("OCG")),
295 (
296 b"Name".to_vec(),
297 PdfObj::LitString(format!("Layer {}", ocg_id).into_bytes()),
298 ),
299 ]));
300 e.insert(r);
301 ocg_order.push(ocg_id);
302 if !default_visible {
303 ocg_default_off.insert(ocg_id);
304 }
305 }
306 };
307 collect_ocg_ids(&marker.visibility, &mut visit);
308 }
309 }
310
311 let page_refs: Vec<u32> = (0..page_results.len())
315 .map(|_| writer.alloc_obj())
316 .collect();
317
318 let mut per_page_annots: Vec<Vec<u32>> = ctx
324 .map(|c| {
325 let records: Vec<stet_graphics::document_structure::AnnotationRecord> = c
326 .doc_structure
327 .records()
328 .iter()
329 .filter_map(|r| match r {
330 stet_graphics::document_structure::StructuralRecord::Annotation(rec) => {
331 Some(rec.clone())
332 }
333 _ => None,
334 })
335 .collect();
336 if records.is_empty() {
337 return vec![Vec::new(); page_refs.len()];
338 }
339 crate::annotations::collect_per_page(&mut writer, &records, &page_refs)
340 })
341 .unwrap_or_else(|| vec![Vec::new(); page_refs.len()]);
342
343 let acroform_output = ctx.and_then(|c| {
348 let widgets: Vec<(usize, stet_graphics::document_structure::AnnotationRecord)> = c
349 .doc_structure
350 .records()
351 .iter()
352 .enumerate()
353 .filter_map(|(i, r)| match r {
354 stet_graphics::document_structure::StructuralRecord::Annotation(rec)
355 if matches!(
356 rec.subtype,
357 stet_graphics::document_structure::AnnotationSubtype::Widget(_)
358 ) =>
359 {
360 Some((i, rec.clone()))
361 }
362 _ => None,
363 })
364 .collect();
365 let form_record = c
366 .doc_structure
367 .records()
368 .iter()
369 .filter_map(|r| match r {
370 stet_graphics::document_structure::StructuralRecord::Form(rec) => {
371 Some(rec.clone())
372 }
373 _ => None,
374 })
375 .reduce(|acc, next| next.merge_over(&acc));
376 crate::form_fields::write_form(
377 &mut writer,
378 &widgets,
379 form_record.as_ref(),
380 page_refs.len(),
381 )
382 });
383 if let Some(out) = &acroform_output {
384 for (i, refs) in out.per_page_widget_refs.iter().enumerate() {
385 per_page_annots[i].extend(refs);
386 }
387 }
388
389 let per_page_overrides = compute_page_overrides(ctx, page_refs.len());
394
395 for (i, (result, page)) in page_results.iter().enumerate() {
397 self.build_page(
398 &mut writer,
399 page,
400 pages_ref,
401 page_refs[i],
402 result,
403 &font_obj_map,
404 &mut font_tracker,
405 &per_page_annots[i],
406 &per_page_overrides[i],
407 &ocg_id_to_ref,
408 )?;
409 }
410
411 writer.set_object(
413 pages_ref,
414 &PdfObj::Dict(vec![
415 (b"Type".to_vec(), PdfObj::name("Pages")),
416 (
417 b"Kids".to_vec(),
418 PdfObj::Array(page_refs.iter().map(|&r| PdfObj::Ref(r)).collect()),
419 ),
420 (b"Count".to_vec(), PdfObj::Int(page_refs.len() as i64)),
421 ]),
422 );
423
424 let outlines_ref = ctx.and_then(|c| {
428 let records: Vec<stet_graphics::document_structure::OutlineRecord> = c
429 .doc_structure
430 .records()
431 .iter()
432 .filter_map(|r| match r {
433 stet_graphics::document_structure::StructuralRecord::Outline(rec) => {
434 Some(rec.clone())
435 }
436 _ => None,
437 })
438 .collect();
439 if records.is_empty() {
440 return None;
441 }
442 let tree = stet_graphics::document_structure::build_outline_tree(&records);
443 crate::outline::write_outline_tree(&mut writer, &tree, &page_refs)
444 });
445
446 let dests_leaf = ctx.and_then(|c| {
451 let records: Vec<stet_graphics::document_structure::DestRecord> = c
452 .doc_structure
453 .records()
454 .iter()
455 .filter_map(|r| match r {
456 stet_graphics::document_structure::StructuralRecord::Dest(rec) => {
457 Some(rec.clone())
458 }
459 _ => None,
460 })
461 .collect();
462 crate::names::build_dests_leaf(&mut writer, &records, &page_refs)
463 });
464 let embedded_files_leaf = ctx.and_then(|c| {
465 let records: Vec<stet_graphics::document_structure::EmbedRecord> = c
466 .doc_structure
467 .records()
468 .iter()
469 .filter_map(|r| match r {
470 stet_graphics::document_structure::StructuralRecord::Embed(rec) => {
471 Some(rec.clone())
472 }
473 _ => None,
474 })
475 .collect();
476 crate::attachments::build_embedded_files_leaf(&mut writer, &records)
477 });
478 let names_ref =
479 crate::names::write_names_root(&mut writer, dests_leaf, embedded_files_leaf);
480
481 let merged_prefs = ctx.map(collect_viewer_prefs).unwrap_or_default();
486 let viewer_prefs_ref = crate::metadata::write_viewer_prefs(&mut writer, &merged_prefs);
487
488 let metadata_ref = ctx.and_then(|c| {
490 c.doc_structure
491 .records()
492 .iter()
493 .rev()
494 .find_map(|r| match r {
495 stet_graphics::document_structure::StructuralRecord::Metadata(rec) => {
496 Some(rec.clone())
497 }
498 _ => None,
499 })
500 .map(|rec| crate::metadata::write_xmp_metadata(&mut writer, &rec))
501 });
502
503 let mut catalog_entries = vec![
505 (b"Type".to_vec(), PdfObj::name("Catalog")),
506 (b"Pages".to_vec(), PdfObj::Ref(pages_ref)),
507 ];
508
509 if !ocg_id_to_ref.is_empty() {
517 let make_array = || -> Vec<PdfObj> {
518 ocg_order
519 .iter()
520 .filter_map(|id| ocg_id_to_ref.get(id).map(|&r| PdfObj::Ref(r)))
521 .collect()
522 };
523 let off_array: Vec<PdfObj> = ocg_order
524 .iter()
525 .filter(|id| ocg_default_off.contains(id))
526 .filter_map(|id| ocg_id_to_ref.get(id).map(|&r| PdfObj::Ref(r)))
527 .collect();
528 let mut d_entries: Vec<(Vec<u8>, PdfObj)> = vec![
529 (b"Name".to_vec(), PdfObj::LitString(b"Default".to_vec())),
530 (b"BaseState".to_vec(), PdfObj::name("ON")),
531 (b"Order".to_vec(), PdfObj::Array(make_array())),
532 ];
533 if !off_array.is_empty() {
534 d_entries.push((b"OFF".to_vec(), PdfObj::Array(off_array)));
535 }
536 let ocprops = writer.add_object(&PdfObj::Dict(vec![
537 (b"OCGs".to_vec(), PdfObj::Array(make_array())),
538 (b"D".to_vec(), PdfObj::Dict(d_entries)),
539 ]));
540 catalog_entries.push((b"OCProperties".to_vec(), PdfObj::Ref(ocprops)));
541 }
542 if let Some(outline_ref) = outlines_ref {
543 catalog_entries.push((b"Outlines".to_vec(), PdfObj::Ref(outline_ref)));
544 }
545 if let Some(names_ref) = names_ref {
546 catalog_entries.push((b"Names".to_vec(), PdfObj::Ref(names_ref)));
547 }
548 if let Some(viewer_prefs_ref) = viewer_prefs_ref {
549 catalog_entries.push((b"ViewerPreferences".to_vec(), PdfObj::Ref(viewer_prefs_ref)));
550 }
551 if let Some(layout_bytes) = merged_prefs
553 .page_layout
554 .as_deref()
555 .and_then(crate::metadata::validated_page_layout)
556 {
557 catalog_entries.push((b"PageLayout".to_vec(), PdfObj::Name(layout_bytes.to_vec())));
558 }
559 let effective_page_mode: Option<Vec<u8>> = merged_prefs
563 .page_mode
564 .as_deref()
565 .and_then(crate::metadata::validated_page_mode)
566 .map(|v| v.to_vec())
567 .or_else(|| outlines_ref.map(|_| b"UseOutlines".to_vec()));
568 if let Some(mode) = effective_page_mode {
569 catalog_entries.push((b"PageMode".to_vec(), PdfObj::Name(mode)));
570 }
571 if let Some(metadata_ref) = metadata_ref {
572 catalog_entries.push((b"Metadata".to_vec(), PdfObj::Ref(metadata_ref)));
573 }
574 if let Some(out) = &acroform_output {
575 catalog_entries.push((b"AcroForm".to_vec(), PdfObj::Ref(out.acroform_ref)));
576 }
577
578 if !self.output_intents.is_empty() {
582 let intent_refs = emit_output_intents(&mut writer, &self.output_intents);
583 if !intent_refs.is_empty() {
584 catalog_entries.push((
585 b"OutputIntents".to_vec(),
586 PdfObj::Array(intent_refs.into_iter().map(PdfObj::Ref).collect()),
587 ));
588 }
589 }
590
591 writer.set_object(catalog_ref, &PdfObj::Dict(catalog_entries));
592
593 let info_ref = writer.alloc_obj();
596 let info_entries = self.build_info_dict(ctx);
597 writer.set_object(info_ref, &PdfObj::Dict(info_entries));
598
599 Ok((writer, catalog_ref, info_ref))
600 }
601
602 fn embed_all_fonts(
605 &self,
606 writer: &mut PdfWriter,
607 font_tracker: &FontTracker,
608 ctx: Option<&Context>,
609 ) -> HashMap<String, u32> {
610 let mut map = HashMap::new();
611 for usage in font_tracker.fonts() {
612 let font_ref = if let Some(c) = ctx {
613 font_embedder::build_font_resource(writer, usage, c).unwrap_or_else(|| {
614 let tu = font_embedder::build_tounicode_for_fallback(writer, usage, c);
615 self.build_font_reference(writer, usage, tu)
616 })
617 } else {
618 self.build_font_reference(writer, usage, None)
619 };
620 map.insert(usage.pdf_name.clone(), font_ref);
621 }
622 map
623 }
624
625 #[allow(clippy::too_many_arguments)]
631 fn build_page(
632 &self,
633 writer: &mut PdfWriter,
634 page: &PageData,
635 pages_ref: u32,
636 page_ref: u32,
637 result: &ContentStreamResult,
638 font_obj_map: &HashMap<String, u32>,
639 font_tracker: &mut FontTracker,
640 annot_refs: &[u32],
641 overrides: &EffectivePageOverride,
642 ocg_id_to_ref: &HashMap<u32, u32>,
643 ) -> Result<(), String> {
644 let ContentStreamResult {
645 content,
646 images,
647 shading_refs,
648 used_font_names,
649 ext_gstate_dicts,
650 color_spaces,
651 icc_color_spaces,
652 pattern_refs,
653 pattern_cs_entries,
654 transfer_refs,
655 halftone_refs,
656 bg_ucr_refs,
657 soft_mask_refs,
658 ocg_marker_refs,
659 form_xobjects,
660 } = result;
661
662 let mut xobject_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
668 for (i, img) in images.iter().enumerate() {
669 let img_ref = self.build_image_xobject(writer, img);
670 xobject_entries.push((format!("Im{}", i).into_bytes(), PdfObj::Ref(img_ref)));
671 }
672 let mut form_obj_refs: Vec<u32> = Vec::with_capacity(form_xobjects.len());
673 for (i, form) in form_xobjects.iter().enumerate() {
674 let form_ref = build_form_xobject(writer, form);
675 form_obj_refs.push(form_ref);
676 xobject_entries.push((format!("X{}", i).into_bytes(), PdfObj::Ref(form_ref)));
677 }
678
679 let mut shading_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
681 for (i, sh_ref) in shading_refs.iter().enumerate() {
682 let sh_obj = match sh_ref {
683 ShadingRef::Axial(p) => shading_ops::build_axial_shading(writer, p),
684 ShadingRef::Radial(p) => shading_ops::build_radial_shading(writer, p),
685 ShadingRef::Mesh(p) => shading_ops::build_mesh_shading(writer, p),
686 ShadingRef::Patch(p) => shading_ops::build_patch_shading(writer, p),
687 };
688 shading_entries.push((format!("Sh{}", i).into_bytes(), PdfObj::Ref(sh_obj)));
689 }
690
691 let mut font_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
693 for name in used_font_names {
694 if let Some(&obj_ref) = font_obj_map.get(name) {
695 font_entries.push((name.clone().into_bytes(), PdfObj::Ref(obj_ref)));
696 }
697 }
698
699 let mut resources: Vec<(Vec<u8>, PdfObj)> = Vec::new();
701 if !font_entries.is_empty() {
702 resources.push((b"Font".to_vec(), PdfObj::Dict(font_entries)));
703 }
704 if !xobject_entries.is_empty() {
705 resources.push((b"XObject".to_vec(), PdfObj::Dict(xobject_entries)));
706 }
707 if !shading_entries.is_empty() {
708 resources.push((b"Shading".to_vec(), PdfObj::Dict(shading_entries)));
709 }
710
711 if !ext_gstate_dicts.is_empty() {
713 let mut gs_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
714 for (i, gs_dict) in ext_gstate_dicts.iter().enumerate() {
715 let mut entries: Vec<(Vec<u8>, PdfObj)> = gs_dict
717 .entries
718 .iter()
719 .map(|(k, v)| {
720 let obj = match v {
721 PdfObj::Bool(b) => PdfObj::Bool(*b),
722 PdfObj::Int(n) => PdfObj::Int(*n),
723 PdfObj::Real(r) => PdfObj::Real(*r),
724 PdfObj::Name(n) => PdfObj::Name(n.clone()),
725 PdfObj::Ref(r) => PdfObj::Ref(*r),
726 _ => PdfObj::Null,
727 };
728 (k.clone(), obj)
729 })
730 .collect();
731
732 if let Some(tr) = transfer_refs.iter().find(|r| r.ext_gstate_idx == i) {
734 let tr2_value = build_transfer_tr2(writer, &tr.tables, tr.is_color);
735 entries.push((b"TR2".to_vec(), tr2_value));
736 }
737
738 if let Some(hr) = halftone_refs.iter().find(|r| r.ext_gstate_idx == i) {
740 let ht_value = build_halftone_ht(writer, &hr.state);
741 entries.push((b"HT".to_vec(), ht_value));
742 }
743
744 if let Some(br) = bg_ucr_refs.iter().find(|r| r.ext_gstate_idx == i) {
746 if let Some(ref bg) = br.state.bg {
747 let func_ref = build_type0_function(writer, bg);
748 entries.push((b"BG2".to_vec(), PdfObj::Ref(func_ref)));
749 }
750 if let Some(ref ucr) = br.state.ucr {
751 let func_ref = build_type0_function_signed(writer, ucr);
752 entries.push((b"UCR2".to_vec(), PdfObj::Ref(func_ref)));
753 }
754 }
755
756 if let Some(sm) = soft_mask_refs.iter().find(|r| r.ext_gstate_idx == i) {
760 let mask_ref = form_obj_refs[sm.mask_form_idx];
761 let subtype_name: &[u8] = match sm.subtype {
762 stet_graphics::display_list::SoftMaskSubtype::Alpha => b"Alpha",
763 stet_graphics::display_list::SoftMaskSubtype::Luminosity => b"Luminosity",
764 };
765 let mut smask_entries: Vec<(Vec<u8>, PdfObj)> = vec![
766 (b"Type".to_vec(), PdfObj::name("Mask")),
767 (b"S".to_vec(), PdfObj::Name(subtype_name.to_vec())),
768 (b"G".to_vec(), PdfObj::Ref(mask_ref)),
769 ];
770 if let Some([r, g, b]) = sm.backdrop_color {
771 smask_entries.push((
772 b"BC".to_vec(),
773 PdfObj::Array(vec![PdfObj::Real(r), PdfObj::Real(g), PdfObj::Real(b)]),
774 ));
775 }
776 if sm.transfer_invert {
777 let tr_ref = build_invert_transfer(writer);
780 smask_entries.push((b"TR".to_vec(), PdfObj::Ref(tr_ref)));
781 }
782 entries.push((b"SMask".to_vec(), PdfObj::Dict(smask_entries)));
783 }
784
785 let gs_ref = writer.add_object(&PdfObj::Dict(entries));
786 gs_entries.push((format!("GS{}", i).into_bytes(), PdfObj::Ref(gs_ref)));
787 }
788 resources.push((b"ExtGState".to_vec(), PdfObj::Dict(gs_entries)));
789 }
790
791 let mut cs_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
793 for (name, spot_cs) in color_spaces {
794 let cs_obj = build_spot_colorspace(spot_cs, writer);
795 cs_entries.push((name.clone().into_bytes(), cs_obj));
796 }
797 for (name, icc_cs) in icc_color_spaces {
800 let icc_ref = writer.add_stream(
801 vec![(b"N".to_vec(), PdfObj::Int(icc_cs.n as i64))],
802 &icc_cs.profile_data,
803 true,
804 );
805 cs_entries.push((
806 name.clone().into_bytes(),
807 PdfObj::Array(vec![PdfObj::name("ICCBased"), PdfObj::Ref(icc_ref)]),
808 ));
809 }
810 for (name, cs_obj) in pattern_cs_entries {
812 let obj = match cs_obj {
814 PdfObj::Array(items) => {
815 let cloned: Vec<PdfObj> = items
816 .iter()
817 .map(|item| match item {
818 PdfObj::Name(n) => PdfObj::Name(n.clone()),
819 PdfObj::Int(n) => PdfObj::Int(*n),
820 PdfObj::Real(n) => PdfObj::Real(*n),
821 PdfObj::Ref(r) => PdfObj::Ref(*r),
822 _ => PdfObj::Null,
823 })
824 .collect();
825 PdfObj::Array(cloned)
826 }
827 _ => PdfObj::Null,
828 };
829 cs_entries.push((name.clone().into_bytes(), obj));
830 }
831 if !cs_entries.is_empty() {
832 resources.push((b"ColorSpace".to_vec(), PdfObj::Dict(cs_entries)));
833 }
834
835 if !ocg_marker_refs.is_empty() {
840 let mut props_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
841 for marker in ocg_marker_refs {
842 let prop_ref = build_ocg_property_ref(writer, &marker.visibility, ocg_id_to_ref);
843 props_entries.push((
844 marker.resource_name.clone().into_bytes(),
845 PdfObj::Ref(prop_ref),
846 ));
847 }
848 resources.push((b"Properties".to_vec(), PdfObj::Dict(props_entries)));
849 }
850
851 if !pattern_refs.is_empty() {
853 let mut pattern_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
854 for (i, pat_ref) in pattern_refs.iter().enumerate() {
855 let tile_result =
856 content_stream::build_tile_content_stream(&pat_ref.tile, font_tracker);
857
858 let mut tile_resources: Vec<(Vec<u8>, PdfObj)> = Vec::new();
860
861 if !tile_result.images.is_empty() {
863 let mut tile_xobj: Vec<(Vec<u8>, PdfObj)> = Vec::new();
864 for (j, img) in tile_result.images.iter().enumerate() {
865 let img_ref = self.build_image_xobject(writer, img);
866 tile_xobj.push((format!("Im{}", j).into_bytes(), PdfObj::Ref(img_ref)));
867 }
868 tile_resources.push((b"XObject".to_vec(), PdfObj::Dict(tile_xobj)));
869 }
870
871 if !tile_result.shading_refs.is_empty() {
873 let mut tile_sh: Vec<(Vec<u8>, PdfObj)> = Vec::new();
874 for (j, sh_ref) in tile_result.shading_refs.iter().enumerate() {
875 let sh_obj = match sh_ref {
876 ShadingRef::Axial(p) => shading_ops::build_axial_shading(writer, p),
877 ShadingRef::Radial(p) => shading_ops::build_radial_shading(writer, p),
878 ShadingRef::Mesh(p) => shading_ops::build_mesh_shading(writer, p),
879 ShadingRef::Patch(p) => shading_ops::build_patch_shading(writer, p),
880 };
881 tile_sh.push((format!("Sh{}", j).into_bytes(), PdfObj::Ref(sh_obj)));
882 }
883 tile_resources.push((b"Shading".to_vec(), PdfObj::Dict(tile_sh)));
884 }
885
886 if !tile_result.used_font_names.is_empty() {
888 let mut tile_fonts: Vec<(Vec<u8>, PdfObj)> = Vec::new();
889 for name in &tile_result.used_font_names {
890 if let Some(&obj_ref) = font_obj_map.get(name) {
891 tile_fonts.push((name.clone().into_bytes(), PdfObj::Ref(obj_ref)));
892 }
893 }
894 if !tile_fonts.is_empty() {
895 tile_resources.push((b"Font".to_vec(), PdfObj::Dict(tile_fonts)));
896 }
897 }
898
899 if !tile_result.ext_gstate_dicts.is_empty() {
901 let mut tile_gs: Vec<(Vec<u8>, PdfObj)> = Vec::new();
902 for (j, gs_dict) in tile_result.ext_gstate_dicts.iter().enumerate() {
903 let mut entries: Vec<(Vec<u8>, PdfObj)> = gs_dict
904 .entries
905 .iter()
906 .map(|(k, v)| {
907 let obj = match v {
908 PdfObj::Bool(b) => PdfObj::Bool(*b),
909 PdfObj::Int(n) => PdfObj::Int(*n),
910 PdfObj::Real(r) => PdfObj::Real(*r),
911 PdfObj::Name(n) => PdfObj::Name(n.clone()),
912 PdfObj::Ref(r) => PdfObj::Ref(*r),
913 _ => PdfObj::Null,
914 };
915 (k.clone(), obj)
916 })
917 .collect();
918 if let Some(tr) = tile_result
919 .transfer_refs
920 .iter()
921 .find(|r| r.ext_gstate_idx == j)
922 {
923 let tr2_value = build_transfer_tr2(writer, &tr.tables, tr.is_color);
924 entries.push((b"TR2".to_vec(), tr2_value));
925 }
926 if let Some(hr) = tile_result
927 .halftone_refs
928 .iter()
929 .find(|r| r.ext_gstate_idx == j)
930 {
931 let ht_value = build_halftone_ht(writer, &hr.state);
932 entries.push((b"HT".to_vec(), ht_value));
933 }
934 if let Some(br) = tile_result
935 .bg_ucr_refs
936 .iter()
937 .find(|r| r.ext_gstate_idx == j)
938 {
939 if let Some(ref bg) = br.state.bg {
940 let func_ref = build_type0_function(writer, bg);
941 entries.push((b"BG2".to_vec(), PdfObj::Ref(func_ref)));
942 }
943 if let Some(ref ucr) = br.state.ucr {
944 let func_ref = build_type0_function_signed(writer, ucr);
945 entries.push((b"UCR2".to_vec(), PdfObj::Ref(func_ref)));
946 }
947 }
948 let gs_ref = writer.add_object(&PdfObj::Dict(entries));
949 tile_gs.push((format!("GS{}", j).into_bytes(), PdfObj::Ref(gs_ref)));
950 }
951 tile_resources.push((b"ExtGState".to_vec(), PdfObj::Dict(tile_gs)));
952 }
953
954 if !tile_result.color_spaces.is_empty() || !tile_result.icc_color_spaces.is_empty()
956 {
957 let mut tile_cs: Vec<(Vec<u8>, PdfObj)> = Vec::new();
958 for (name, spot_cs) in &tile_result.color_spaces {
959 let cs_obj = build_spot_colorspace(spot_cs, writer);
960 tile_cs.push((name.clone().into_bytes(), cs_obj));
961 }
962 for (name, icc_cs) in &tile_result.icc_color_spaces {
963 let icc_ref = writer.add_stream(
964 vec![(b"N".to_vec(), PdfObj::Int(icc_cs.n as i64))],
965 &icc_cs.profile_data,
966 true,
967 );
968 tile_cs.push((
969 name.clone().into_bytes(),
970 PdfObj::Array(vec![PdfObj::name("ICCBased"), PdfObj::Ref(icc_ref)]),
971 ));
972 }
973 tile_resources.push((b"ColorSpace".to_vec(), PdfObj::Dict(tile_cs)));
974 }
975
976 let m = &pat_ref.pattern_matrix;
978 let pat_dict = vec![
979 (b"Type".to_vec(), PdfObj::name("Pattern")),
980 (b"PatternType".to_vec(), PdfObj::Int(1)),
981 (
982 b"PaintType".to_vec(),
983 PdfObj::Int(pat_ref.paint_type as i64),
984 ),
985 (b"TilingType".to_vec(), PdfObj::Int(1)),
986 (
987 b"BBox".to_vec(),
988 PdfObj::Array(vec![
991 PdfObj::Real(pat_ref.bbox[0] - 0.5),
992 PdfObj::Real(pat_ref.bbox[1] - 0.5),
993 PdfObj::Real(pat_ref.bbox[2] + 0.5),
994 PdfObj::Real(pat_ref.bbox[3] + 0.5),
995 ]),
996 ),
997 (b"XStep".to_vec(), PdfObj::Real(pat_ref.xstep)),
998 (b"YStep".to_vec(), PdfObj::Real(pat_ref.ystep)),
999 (
1000 b"Matrix".to_vec(),
1001 PdfObj::Array(vec![
1002 PdfObj::Real(m.a),
1003 PdfObj::Real(m.b),
1004 PdfObj::Real(m.c),
1005 PdfObj::Real(m.d),
1006 PdfObj::Real(m.tx),
1007 PdfObj::Real(m.ty),
1008 ]),
1009 ),
1010 (b"Resources".to_vec(), PdfObj::Dict(tile_resources)),
1011 ];
1012
1013 let pat_obj = writer.add_stream(pat_dict, &tile_result.content, true);
1014 pattern_entries.push((format!("P{}", i).into_bytes(), PdfObj::Ref(pat_obj)));
1015 }
1016
1017 resources.push((b"Pattern".to_vec(), PdfObj::Dict(pattern_entries)));
1018 }
1019
1020 let content_ref = writer.add_stream(Vec::new(), content, true);
1022
1023 let mut page_entries = vec![
1025 (b"Type".to_vec(), PdfObj::name("Page")),
1026 (b"Parent".to_vec(), PdfObj::Ref(pages_ref)),
1027 (
1028 b"MediaBox".to_vec(),
1029 PdfObj::Array(vec![
1030 PdfObj::Int(0),
1031 PdfObj::Int(0),
1032 PdfObj::Real(page.width_pts),
1033 PdfObj::Real(page.height_pts),
1034 ]),
1035 ),
1036 (b"Contents".to_vec(), PdfObj::Ref(content_ref)),
1037 (b"Resources".to_vec(), PdfObj::Dict(resources)),
1038 ];
1039 let page_cs: Option<&str> = match page.display_list.page_group_color_space() {
1047 stet_graphics::display_list::GroupColorSpace::DeviceGray => Some("DeviceGray"),
1048 stet_graphics::display_list::GroupColorSpace::DeviceRGB => Some("DeviceRGB"),
1049 stet_graphics::display_list::GroupColorSpace::DeviceCMYK => Some("DeviceCMYK"),
1050 stet_graphics::display_list::GroupColorSpace::Inherited => None,
1051 };
1052 if let Some(cs) = page_cs {
1053 page_entries.push((
1054 b"Group".to_vec(),
1055 PdfObj::Dict(vec![
1056 (b"Type".to_vec(), PdfObj::name("Group")),
1057 (b"S".to_vec(), PdfObj::name("Transparency")),
1058 (b"CS".to_vec(), PdfObj::name(cs)),
1059 ]),
1060 ));
1061 }
1062 let effective_trim = overrides.boxes.trim_box.or_else(|| {
1066 page.trim_box
1067 .map(|(llx, lly, urx, ury)| [llx, lly, urx, ury])
1068 });
1069 for (name, b) in [
1070 (b"CropBox".as_slice(), overrides.boxes.crop_box),
1071 (b"BleedBox".as_slice(), overrides.boxes.bleed_box),
1072 (b"TrimBox".as_slice(), effective_trim),
1073 (b"ArtBox".as_slice(), overrides.boxes.art_box),
1074 ] {
1075 if let Some([llx, lly, urx, ury]) = b {
1076 page_entries.push((
1077 name.to_vec(),
1078 PdfObj::Array(vec![
1079 PdfObj::Real(llx),
1080 PdfObj::Real(lly),
1081 PdfObj::Real(urx),
1082 PdfObj::Real(ury),
1083 ]),
1084 ));
1085 }
1086 }
1087 if let Some(rotate) = overrides.rotate
1088 && matches!(rotate, 0 | 90 | 180 | 270 | -90 | -180 | -270)
1089 {
1090 page_entries.push((b"Rotate".to_vec(), PdfObj::Int(rotate as i64)));
1091 }
1092 if !annot_refs.is_empty() {
1093 page_entries.push((
1094 b"Annots".to_vec(),
1095 PdfObj::Array(annot_refs.iter().map(|r| PdfObj::Ref(*r)).collect()),
1096 ));
1097 }
1098 if let Some(aa) = &overrides.additional_actions
1099 && !aa.is_empty()
1100 {
1101 let mut aa_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
1106 if let Some(action) = &aa.on_open
1107 && let Some(dict) = crate::outline::encode_action(action, &[])
1108 {
1109 aa_entries.push((b"O".to_vec(), dict));
1110 }
1111 if let Some(action) = &aa.on_close
1112 && let Some(dict) = crate::outline::encode_action(action, &[])
1113 {
1114 aa_entries.push((b"C".to_vec(), dict));
1115 }
1116 if !aa_entries.is_empty() {
1117 page_entries.push((b"AA".to_vec(), PdfObj::Dict(aa_entries)));
1118 }
1119 }
1120 writer.set_object(page_ref, &PdfObj::Dict(page_entries));
1121
1122 Ok(())
1123 }
1124
1125 fn build_font_reference(
1131 &self,
1132 writer: &mut PdfWriter,
1133 usage: &crate::font_tracker::FontUsage,
1134 tounicode_override: Option<u32>,
1135 ) -> u32 {
1136 let tounicode_ref = tounicode_override.or_else(|| self.build_tounicode_cmap(writer, usage));
1138
1139 let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![
1140 (b"Type".to_vec(), PdfObj::name("Font")),
1141 (b"Subtype".to_vec(), PdfObj::name("Type1")),
1142 (b"BaseFont".to_vec(), PdfObj::Name(usage.font_name.clone())),
1143 ];
1144
1145 if !usage.is_standard_14 {
1146 entries.push((b"Encoding".to_vec(), PdfObj::name("WinAnsiEncoding")));
1148 }
1149
1150 if let Some(tu_ref) = tounicode_ref {
1151 entries.push((b"ToUnicode".to_vec(), PdfObj::Ref(tu_ref)));
1152 }
1153
1154 writer.add_object(&PdfObj::Dict(entries))
1155 }
1156
1157 fn build_tounicode_cmap(
1164 &self,
1165 writer: &mut PdfWriter,
1166 usage: &crate::font_tracker::FontUsage,
1167 ) -> Option<u32> {
1168 use std::collections::HashMap;
1169
1170 let mut map: HashMap<u16, u16> = HashMap::new();
1171
1172 for &code in &usage.used_codes {
1173 if code <= 255 {
1174 if (0x20..=0x7E).contains(&code) {
1176 map.insert(code, code);
1177 }
1178 }
1179 }
1180
1181 if map.is_empty() {
1182 return None;
1183 }
1184
1185 let font_name = String::from_utf8_lossy(&usage.font_name);
1186 let cmap_data = generate_tounicode_cmap(&map, &font_name);
1187 Some(writer.add_stream(Vec::new(), &cmap_data, true))
1188 }
1189
1190 fn build_image_xobject(&self, writer: &mut PdfWriter, img: &ImageXObject) -> u32 {
1192 let smask_ref = img.smask_data.as_ref().map(|smask_data| {
1194 writer.add_stream(
1195 vec![
1196 (b"Type".to_vec(), PdfObj::name("XObject")),
1197 (b"Subtype".to_vec(), PdfObj::name("Image")),
1198 (b"Width".to_vec(), PdfObj::Int(img.width as i64)),
1199 (b"Height".to_vec(), PdfObj::Int(img.height as i64)),
1200 (b"ColorSpace".to_vec(), PdfObj::name("DeviceGray")),
1201 (b"BitsPerComponent".to_vec(), PdfObj::Int(8)),
1202 (b"Interpolate".to_vec(), PdfObj::Bool(false)),
1203 ],
1204 smask_data,
1205 true,
1206 )
1207 });
1208
1209 let icc_ref = img.icc_profile.as_ref().map(|icc| {
1211 writer.add_stream(
1212 vec![(b"N".to_vec(), PdfObj::Int(icc.n as i64))],
1213 &icc.data,
1214 true,
1215 )
1216 });
1217
1218 let cs_obj = build_pdf_colorspace(&img.pdf_color_space, icc_ref, writer);
1220
1221 let mut entries = vec![
1223 (b"Type".to_vec(), PdfObj::name("XObject")),
1224 (b"Subtype".to_vec(), PdfObj::name("Image")),
1225 (b"Width".to_vec(), PdfObj::Int(img.width as i64)),
1226 (b"Height".to_vec(), PdfObj::Int(img.height as i64)),
1227 ];
1228
1229 if img.is_imagemask {
1230 entries.push((b"ImageMask".to_vec(), PdfObj::Bool(true)));
1231 entries.push((
1233 b"Decode".to_vec(),
1234 PdfObj::Array(vec![PdfObj::Int(1), PdfObj::Int(0)]),
1235 ));
1236 } else {
1237 entries.push((b"ColorSpace".to_vec(), cs_obj));
1238 entries.push((
1239 b"BitsPerComponent".to_vec(),
1240 PdfObj::Int(img.bits_per_component as i64),
1241 ));
1242 }
1243
1244 entries.push((b"Interpolate".to_vec(), PdfObj::Bool(false)));
1245
1246 if let Some(smask) = smask_ref {
1247 entries.push((b"SMask".to_vec(), PdfObj::Ref(smask)));
1248 }
1249
1250 if let Some(ref ckm) = img.color_key_mask {
1252 let ncomp = img.pdf_color_space.num_components();
1253 let mask_ints: Vec<PdfObj> = if ckm.len() == ncomp {
1254 ckm.iter()
1256 .flat_map(|&v| [PdfObj::Int(v as i64), PdfObj::Int(v as i64)])
1257 .collect()
1258 } else {
1259 ckm.iter().map(|&v| PdfObj::Int(v as i64)).collect()
1261 };
1262 entries.push((b"Mask".to_vec(), PdfObj::Array(mask_ints)));
1263 }
1264
1265 writer.add_stream(entries, &img.sample_data, true)
1266 }
1267}
1268
1269fn build_pdf_colorspace(
1271 cs: &crate::image_ops::PdfColorSpace,
1272 icc_ref: Option<u32>,
1273 writer: &mut PdfWriter,
1274) -> PdfObj {
1275 use crate::image_ops::PdfColorSpace;
1276 match cs {
1277 PdfColorSpace::DeviceGray => PdfObj::name("DeviceGray"),
1278 PdfColorSpace::DeviceRGB => PdfObj::name("DeviceRGB"),
1279 PdfColorSpace::DeviceCMYK => PdfObj::name("DeviceCMYK"),
1280 PdfColorSpace::ICCBased { .. } => {
1281 if let Some(ref_num) = icc_ref {
1282 PdfObj::Array(vec![PdfObj::name("ICCBased"), PdfObj::Ref(ref_num)])
1283 } else {
1284 PdfObj::name("DeviceRGB") }
1286 }
1287 PdfColorSpace::Indexed {
1288 base,
1289 hival,
1290 lookup,
1291 } => {
1292 let base_obj = build_pdf_colorspace(base, icc_ref, writer);
1298 let lookup_ref = writer.add_stream(Vec::new(), lookup, true);
1300 PdfObj::Array(vec![
1301 PdfObj::name("Indexed"),
1302 base_obj,
1303 PdfObj::Int(*hival as i64),
1304 PdfObj::Ref(lookup_ref),
1305 ])
1306 }
1307 PdfColorSpace::Separation {
1308 name,
1309 alt,
1310 tint_table,
1311 } => {
1312 let alt_obj = build_pdf_colorspace(alt, None, writer);
1313 let func_ref = build_tint_function(tint_table, writer);
1314 PdfObj::Array(vec![
1315 PdfObj::name("Separation"),
1316 PdfObj::Name(name.clone()),
1317 alt_obj,
1318 PdfObj::Ref(func_ref),
1319 ])
1320 }
1321 PdfColorSpace::DeviceN {
1322 names,
1323 alt,
1324 tint_table,
1325 } => {
1326 let alt_obj = build_pdf_colorspace(alt, None, writer);
1327 let func_ref = build_tint_function(tint_table, writer);
1328 let names_arr = PdfObj::Array(names.iter().map(|n| PdfObj::Name(n.clone())).collect());
1329 PdfObj::Array(vec![
1330 PdfObj::name("DeviceN"),
1331 names_arr,
1332 alt_obj,
1333 PdfObj::Ref(func_ref),
1334 ])
1335 }
1336 }
1337}
1338
1339fn emit_output_intents(
1344 writer: &mut PdfWriter,
1345 intents: &[stet_graphics::document_structure::OutputIntentRecord],
1346) -> Vec<u32> {
1347 let mut refs = Vec::with_capacity(intents.len());
1348 for intent in intents {
1349 let profile_ref = intent.dest_output_profile.as_ref().map(|bytes| {
1350 writer.add_stream(
1351 vec![(b"N".to_vec(), PdfObj::Int(intent.n as i64))],
1352 bytes,
1353 true,
1354 )
1355 });
1356 let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![
1357 (b"Type".to_vec(), PdfObj::name("OutputIntent")),
1358 (b"S".to_vec(), PdfObj::Name(intent.subtype.clone())),
1359 ];
1360 if let Some(s) = &intent.output_condition_identifier {
1361 entries.push((
1362 b"OutputConditionIdentifier".to_vec(),
1363 PdfObj::LitString(s.clone()),
1364 ));
1365 }
1366 if let Some(s) = &intent.output_condition {
1367 entries.push((b"OutputCondition".to_vec(), PdfObj::LitString(s.clone())));
1368 }
1369 if let Some(s) = &intent.registry_name {
1370 entries.push((b"RegistryName".to_vec(), PdfObj::LitString(s.clone())));
1371 }
1372 if let Some(s) = &intent.info {
1373 entries.push((b"Info".to_vec(), PdfObj::LitString(s.clone())));
1374 }
1375 if let Some(p_ref) = profile_ref {
1376 entries.push((b"DestOutputProfile".to_vec(), PdfObj::Ref(p_ref)));
1377 }
1378 let intent_ref = writer.add_object(&PdfObj::Dict(entries));
1379 refs.push(intent_ref);
1380 }
1381 refs
1382}
1383
1384fn build_tint_function(
1387 table: &stet_graphics::device::TintLookupTable,
1388 writer: &mut PdfWriter,
1389) -> u32 {
1390 let ni = table.num_inputs as usize;
1391 let no = table.num_outputs as usize;
1392
1393 let spd = table.samples_per_dim as usize;
1398 let total_entries = spd.pow(ni as u32);
1399 let samples: Vec<u8> = if ni <= 1 {
1400 table
1401 .data
1402 .iter()
1403 .map(|&v| (v.clamp(0.0, 1.0) * 255.0) as u8)
1404 .collect()
1405 } else {
1406 let mut out = Vec::with_capacity(total_entries * no);
1408 for pdf_idx in 0..total_entries {
1409 let mut coords = vec![0usize; ni];
1411 let mut rem = pdf_idx;
1412 for coord in coords.iter_mut() {
1413 *coord = rem % spd;
1414 rem /= spd;
1415 }
1416 let mut our_idx = 0;
1418 for coord in coords.iter() {
1419 our_idx = our_idx * spd + coord;
1420 }
1421 let base = our_idx * no;
1422 for c in 0..no {
1423 out.push((table.data[base + c].clamp(0.0, 1.0) * 255.0) as u8);
1424 }
1425 }
1426 out
1427 };
1428
1429 let mut domain = Vec::with_capacity(ni * 2);
1431 for _ in 0..ni {
1432 domain.push(PdfObj::Int(0));
1433 domain.push(PdfObj::Int(1));
1434 }
1435
1436 let mut range = Vec::with_capacity(no * 2);
1438 for _ in 0..no {
1439 range.push(PdfObj::Int(0));
1440 range.push(PdfObj::Int(1));
1441 }
1442
1443 let size: Vec<PdfObj> = (0..ni)
1445 .map(|_| PdfObj::Int(table.samples_per_dim as i64))
1446 .collect();
1447
1448 let dict_entries = vec![
1449 (b"FunctionType".to_vec(), PdfObj::Int(0)),
1450 (b"Domain".to_vec(), PdfObj::Array(domain)),
1451 (b"Range".to_vec(), PdfObj::Array(range)),
1452 (b"Size".to_vec(), PdfObj::Array(size)),
1453 (b"BitsPerSample".to_vec(), PdfObj::Int(8)),
1454 ];
1455
1456 writer.add_stream(dict_entries, &samples, true)
1457}
1458
1459fn build_spot_colorspace(
1462 spot_cs: &stet_graphics::device::SpotColorSpace,
1463 writer: &mut PdfWriter,
1464) -> PdfObj {
1465 use stet_graphics::device::{SimpleColorSpace, SpotColorSpace};
1466 match spot_cs {
1467 SpotColorSpace::Separation {
1468 name,
1469 alt,
1470 tint_table,
1471 } => {
1472 let alt_obj = match alt {
1473 SimpleColorSpace::DeviceGray => PdfObj::name("DeviceGray"),
1474 SimpleColorSpace::DeviceRGB => PdfObj::name("DeviceRGB"),
1475 SimpleColorSpace::DeviceCMYK => PdfObj::name("DeviceCMYK"),
1476 };
1477 let func_ref = build_tint_function(tint_table, writer);
1478 PdfObj::Array(vec![
1479 PdfObj::name("Separation"),
1480 PdfObj::Name(name.clone()),
1481 alt_obj,
1482 PdfObj::Ref(func_ref),
1483 ])
1484 }
1485 SpotColorSpace::DeviceN {
1486 names,
1487 alt,
1488 tint_table,
1489 } => {
1490 let alt_obj = match alt {
1491 SimpleColorSpace::DeviceGray => PdfObj::name("DeviceGray"),
1492 SimpleColorSpace::DeviceRGB => PdfObj::name("DeviceRGB"),
1493 SimpleColorSpace::DeviceCMYK => PdfObj::name("DeviceCMYK"),
1494 };
1495 let func_ref = build_tint_function(tint_table, writer);
1496 let names_arr = PdfObj::Array(names.iter().map(|n| PdfObj::Name(n.clone())).collect());
1497 PdfObj::Array(vec![
1498 PdfObj::name("DeviceN"),
1499 names_arr,
1500 alt_obj,
1501 PdfObj::Ref(func_ref),
1502 ])
1503 }
1504 _ => PdfObj::name("DeviceRGB"),
1505 }
1506}
1507
1508fn generate_tounicode_cmap(map: &std::collections::HashMap<u16, u16>, font_name: &str) -> Vec<u8> {
1510 use std::io::Write;
1511 let mut buf = Vec::new();
1512
1513 writeln!(buf, "/CIDInit /ProcSet findresource begin").unwrap();
1514 writeln!(buf, "12 dict begin").unwrap();
1515 writeln!(buf, "begincmap").unwrap();
1516 writeln!(buf, "/CIDSystemInfo <<").unwrap();
1517 writeln!(buf, " /Registry (Adobe)").unwrap();
1518 writeln!(buf, " /Ordering (UCS)").unwrap();
1519 writeln!(buf, " /Supplement 0").unwrap();
1520 writeln!(buf, ">> def").unwrap();
1521 writeln!(buf, "/CMapName /{}-UCS def", font_name).unwrap();
1522 writeln!(buf, "/CMapType 2 def").unwrap();
1523 writeln!(buf, "1 begincodespacerange").unwrap();
1524 writeln!(buf, "<00> <FF>").unwrap();
1525 writeln!(buf, "endcodespacerange").unwrap();
1526
1527 let mut sorted: Vec<_> = map.iter().collect();
1528 sorted.sort_by_key(|&(&code, _)| code);
1529
1530 for chunk in sorted.chunks(100) {
1531 writeln!(buf, "{} beginbfchar", chunk.len()).unwrap();
1532 for &(&code, &unicode) in chunk {
1533 writeln!(buf, "<{:02X}> <{:04X}>", code, unicode).unwrap();
1534 }
1535 writeln!(buf, "endbfchar").unwrap();
1536 }
1537
1538 writeln!(buf, "endcmap").unwrap();
1539 writeln!(buf, "CMapName currentdict /CMap defineresource pop").unwrap();
1540 writeln!(buf, "end").unwrap();
1541 writeln!(buf, "end").unwrap();
1542
1543 buf
1544}
1545
1546impl OutputDevice for PdfDevice {
1547 fn fill_path(&mut self, _path: &PsPath, _params: &FillParams) {}
1548 fn stroke_path(&mut self, _path: &PsPath, _params: &StrokeParams) {}
1549 fn clip_path(&mut self, _path: &PsPath, _params: &ClipParams) {}
1550 fn init_clip(&mut self) {}
1551 fn erase_page(&mut self) {}
1552
1553 fn set_trim_box(&mut self, llx: f64, lly: f64, urx: f64, ury: f64) {
1554 self.pending_trim_box = Some((llx, lly, urx, ury));
1555 }
1556
1557 fn show_page(&mut self, _output_path: &str) -> Result<(), String> {
1558 Ok(())
1559 }
1560
1561 fn draw_image(&mut self, _sample_data: &[u8], _params: &ImageParams) {}
1562
1563 fn page_size(&self) -> (u32, u32) {
1564 (self.page_w, self.page_h)
1565 }
1566
1567 fn replay_and_show(&mut self, list: DisplayList, output_path: &str) -> Result<(), String> {
1568 if self.output_path.is_none() {
1570 let base = if let Some(pos) = output_path.rfind('.') {
1572 &output_path[..pos]
1573 } else {
1574 output_path
1575 };
1576 let base = if base.len() >= 5 && base.as_bytes()[base.len() - 5] == b'-' {
1578 let suffix = &base[base.len() - 4..];
1579 if suffix.bytes().all(|b| b.is_ascii_digit()) {
1580 &base[..base.len() - 5]
1581 } else {
1582 base
1583 }
1584 } else {
1585 base
1586 };
1587 self.output_path = Some(format!("{}.pdf", base));
1588 }
1589
1590 let scale = 72.0 / self.dpi;
1591
1592 self.pages.push(PageData {
1593 display_list: list,
1594 width_pts: self.page_w as f64 * scale,
1595 height_pts: self.page_h as f64 * scale,
1596 page_w: self.page_w,
1597 page_h: self.page_h,
1598 dpi: self.dpi,
1599 trim_box: self.pending_trim_box.take(),
1600 });
1601
1602 Ok(())
1603 }
1604
1605 fn finish(&mut self) -> Result<(), String> {
1606 if self.pages.is_empty() {
1607 return Ok(());
1608 }
1609 self.write_pdf(None)
1610 }
1611
1612 fn finish_with_context(&mut self, ctx: &Context) -> Result<(), String> {
1613 if self.pages.is_empty() {
1614 return Ok(());
1615 }
1616 self.write_pdf(Some(ctx))
1617 }
1618
1619 fn as_any(&self) -> &dyn std::any::Any {
1620 self
1621 }
1622}
1623
1624#[allow(dead_code)]
1632fn parse_icc_header(data: &[u8]) -> (u32, String) {
1633 let n = if data.len() >= 20 {
1634 match &data[16..20] {
1635 b"CMYK" => 4,
1636 b"RGB " => 3,
1637 b"GRAY" => 1,
1638 b"Lab " => 3,
1639 _ => 4, }
1641 } else {
1642 4
1643 };
1644 let desc = extract_icc_description(data).unwrap_or_else(|| "Custom".to_string());
1645 (n, desc)
1646}
1647
1648#[allow(dead_code)]
1652fn extract_icc_description(data: &[u8]) -> Option<String> {
1653 if data.len() < 132 {
1654 return None;
1655 }
1656 let tag_count = u32::from_be_bytes(data[128..132].try_into().ok()?) as usize;
1657 let tag_table_start = 132;
1658
1659 for i in 0..tag_count {
1660 let offset = tag_table_start + i * 12;
1661 if offset + 12 > data.len() {
1662 break;
1663 }
1664 let tag_sig = &data[offset..offset + 4];
1665 let tag_offset = u32::from_be_bytes(data[offset + 4..offset + 8].try_into().ok()?) as usize;
1666 let tag_size = u32::from_be_bytes(data[offset + 8..offset + 12].try_into().ok()?) as usize;
1667
1668 if tag_sig != b"desc" {
1669 continue;
1670 }
1671 if tag_offset + tag_size > data.len() || tag_size < 12 {
1672 return None;
1673 }
1674
1675 let type_sig = &data[tag_offset..tag_offset + 4];
1676 if type_sig == b"desc" {
1677 let count =
1679 u32::from_be_bytes(data[tag_offset + 8..tag_offset + 12].try_into().ok()?) as usize;
1680 if count == 0 {
1681 return None;
1682 }
1683 let str_end = (tag_offset + 12 + count).min(tag_offset + tag_size);
1684 let s = &data[tag_offset + 12..str_end];
1685 let s = s.split(|&b| b == 0).next().unwrap_or(s);
1687 return Some(String::from_utf8_lossy(s).to_string());
1688 } else if type_sig == b"mluc" {
1689 if tag_size < 20 {
1691 return None;
1692 }
1693 let record_count =
1694 u32::from_be_bytes(data[tag_offset + 8..tag_offset + 12].try_into().ok()?) as usize;
1695 if record_count == 0 {
1696 return None;
1697 }
1698 let rec_base = tag_offset + 16;
1700 if rec_base + 12 > data.len() {
1701 return None;
1702 }
1703 let str_len =
1704 u32::from_be_bytes(data[rec_base + 4..rec_base + 8].try_into().ok()?) as usize;
1705 let str_off =
1706 u32::from_be_bytes(data[rec_base + 8..rec_base + 12].try_into().ok()?) as usize;
1707 let abs_off = tag_offset + str_off;
1708 if abs_off + str_len > data.len() || str_len < 2 {
1709 return None;
1710 }
1711 let utf16: Vec<u16> = data[abs_off..abs_off + str_len]
1713 .chunks_exact(2)
1714 .map(|c| u16::from_be_bytes([c[0], c[1]]))
1715 .collect();
1716 return Some(
1717 String::from_utf16_lossy(&utf16)
1718 .trim_end_matches('\0')
1719 .to_string(),
1720 );
1721 }
1722
1723 break;
1724 }
1725 None
1726}
1727
1728fn build_invert_transfer(writer: &mut PdfWriter) -> u32 {
1733 let dict_entries = vec![
1734 (b"FunctionType".to_vec(), PdfObj::Int(4)),
1735 (
1736 b"Domain".to_vec(),
1737 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1738 ),
1739 (
1740 b"Range".to_vec(),
1741 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1742 ),
1743 ];
1744 writer.add_stream(dict_entries, b"{ 1 exch sub }", false)
1745}
1746
1747fn build_type0_function(writer: &mut PdfWriter, table: &[f64]) -> u32 {
1750 let dict_entries = vec![
1751 (b"FunctionType".to_vec(), PdfObj::Int(0)),
1752 (
1753 b"Domain".to_vec(),
1754 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1755 ),
1756 (
1757 b"Range".to_vec(),
1758 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1759 ),
1760 (
1761 b"Size".to_vec(),
1762 PdfObj::Array(vec![PdfObj::Int(table.len() as i64)]),
1763 ),
1764 (b"BitsPerSample".to_vec(), PdfObj::Int(8)),
1765 ];
1766 let data: Vec<u8> = table
1767 .iter()
1768 .map(|&v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
1769 .collect();
1770 writer.add_stream(dict_entries, &data, false)
1771}
1772
1773fn build_transfer_tr2(
1776 writer: &mut PdfWriter,
1777 tables: &[Option<std::sync::Arc<Vec<f64>>>],
1778 is_color: bool,
1779) -> PdfObj {
1780 if is_color && tables.len() == 4 {
1781 let refs: Vec<PdfObj> = tables
1783 .iter()
1784 .map(|t| {
1785 if let Some(table) = t {
1786 let func_ref = build_type0_function(writer, table);
1787 PdfObj::Ref(func_ref)
1788 } else {
1789 PdfObj::name("Identity")
1790 }
1791 })
1792 .collect();
1793 PdfObj::Array(refs)
1794 } else if !is_color && tables.len() == 1 {
1795 if let Some(ref table) = tables[0] {
1796 let func_ref = build_type0_function(writer, table);
1797 PdfObj::Ref(func_ref)
1798 } else {
1799 PdfObj::name("Identity")
1800 }
1801 } else {
1802 PdfObj::name("Identity")
1803 }
1804}
1805
1806fn build_type0_function_signed(writer: &mut PdfWriter, table: &[f64]) -> u32 {
1809 let dict_entries = vec![
1810 (b"FunctionType".to_vec(), PdfObj::Int(0)),
1811 (
1812 b"Domain".to_vec(),
1813 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1814 ),
1815 (
1816 b"Range".to_vec(),
1817 PdfObj::Array(vec![PdfObj::Int(-1), PdfObj::Int(1)]),
1818 ),
1819 (
1820 b"Size".to_vec(),
1821 PdfObj::Array(vec![PdfObj::Int(table.len() as i64)]),
1822 ),
1823 (b"BitsPerSample".to_vec(), PdfObj::Int(8)),
1824 ];
1825 let data: Vec<u8> = table
1827 .iter()
1828 .map(|&v| ((v.clamp(-1.0, 1.0) + 1.0) / 2.0 * 255.0).round() as u8)
1829 .collect();
1830 writer.add_stream(dict_entries, &data, false)
1831}
1832
1833fn build_type4_function(writer: &mut PdfWriter, tokens: &[u8]) -> u32 {
1836 let dict_entries = vec![
1837 (b"FunctionType".to_vec(), PdfObj::Int(4)),
1838 (
1839 b"Domain".to_vec(),
1840 PdfObj::Array(vec![
1841 PdfObj::Int(-1),
1842 PdfObj::Int(1),
1843 PdfObj::Int(-1),
1844 PdfObj::Int(1),
1845 ]),
1846 ),
1847 (
1848 b"Range".to_vec(),
1849 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1850 ),
1851 ];
1852 writer.add_stream(dict_entries, tokens, false)
1853}
1854
1855fn build_type0_function_2d(writer: &mut PdfWriter, table: &[f64]) -> u32 {
1858 let dict_entries = vec![
1859 (b"FunctionType".to_vec(), PdfObj::Int(0)),
1860 (
1861 b"Domain".to_vec(),
1862 PdfObj::Array(vec![
1863 PdfObj::Int(-1),
1864 PdfObj::Int(1),
1865 PdfObj::Int(-1),
1866 PdfObj::Int(1),
1867 ]),
1868 ),
1869 (
1870 b"Range".to_vec(),
1871 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1872 ),
1873 (
1874 b"Size".to_vec(),
1875 PdfObj::Array(vec![PdfObj::Int(64), PdfObj::Int(64)]),
1876 ),
1877 (b"BitsPerSample".to_vec(), PdfObj::Int(8)),
1878 ];
1879 let data: Vec<u8> = table
1880 .iter()
1881 .map(|&v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
1882 .collect();
1883 writer.add_stream(dict_entries, &data, false)
1884}
1885
1886fn build_halftone_screen(
1889 writer: &mut PdfWriter,
1890 screen: &stet_graphics::device::HalftoneScreen,
1891) -> PdfObj {
1892 let spot_func = if let Some(ref tokens) = screen.type4_tokens {
1893 let func_ref = build_type4_function(writer, tokens);
1894 PdfObj::Ref(func_ref)
1895 } else if let Some(ref table) = screen.sampled_2d {
1896 let func_ref = build_type0_function_2d(writer, table);
1897 PdfObj::Ref(func_ref)
1898 } else {
1899 PdfObj::name("Default")
1900 };
1901
1902 let entries = vec![
1903 (b"Type".to_vec(), PdfObj::name("Halftone")),
1904 (b"HalftoneType".to_vec(), PdfObj::Int(1)),
1905 (b"Frequency".to_vec(), PdfObj::Real(screen.frequency)),
1906 (b"Angle".to_vec(), PdfObj::Real(screen.angle)),
1907 (b"SpotFunction".to_vec(), spot_func),
1908 ];
1909 let obj_ref = writer.add_object(&PdfObj::Dict(entries));
1910 PdfObj::Ref(obj_ref)
1911}
1912
1913fn build_halftone_ht(
1915 writer: &mut PdfWriter,
1916 state: &stet_graphics::device::HalftoneState,
1917) -> PdfObj {
1918 if let Some(ref color) = state.color {
1919 let mut entries = vec![
1921 (b"Type".to_vec(), PdfObj::name("Halftone")),
1922 (b"HalftoneType".to_vec(), PdfObj::Int(5)),
1923 ];
1924 let component_names: [&[u8]; 4] = [b"Red", b"Green", b"Blue", b"Default"];
1925 for (i, screen_opt) in color.iter().enumerate() {
1926 if let Some(screen) = screen_opt {
1927 let ht_obj = build_halftone_screen(writer, screen);
1928 entries.push((component_names[i].to_vec(), ht_obj));
1929 }
1930 }
1931 let obj_ref = writer.add_object(&PdfObj::Dict(entries));
1932 PdfObj::Ref(obj_ref)
1933 } else if let Some(ref gray) = state.gray {
1934 build_halftone_screen(writer, gray)
1935 } else {
1936 PdfObj::name("Default")
1937 }
1938}
1939
1940#[derive(Default, Clone)]
1942struct EffectivePageOverride {
1943 boxes: stet_graphics::document_structure::PageBoxes,
1944 rotate: Option<i32>,
1945 additional_actions: Option<stet_graphics::document_structure::PageAdditionalActions>,
1946}
1947
1948fn compute_page_overrides(ctx: Option<&Context>, page_count: usize) -> Vec<EffectivePageOverride> {
1953 use stet_graphics::document_structure::{PageOverrideScope, StructuralRecord};
1954 let mut out = vec![EffectivePageOverride::default(); page_count];
1955 let Some(c) = ctx else {
1956 return out;
1957 };
1958 let mut all_boxes = stet_graphics::document_structure::PageBoxes::default();
1959 let mut all_rotate: Option<i32> = None;
1960 let mut all_aa: Option<stet_graphics::document_structure::PageAdditionalActions> = None;
1961 let mut per_page_boxes: Vec<stet_graphics::document_structure::PageBoxes> =
1962 vec![stet_graphics::document_structure::PageBoxes::default(); page_count];
1963 let mut per_page_rotate: Vec<Option<i32>> = vec![None; page_count];
1964 let mut per_page_aa: Vec<Option<stet_graphics::document_structure::PageAdditionalActions>> =
1965 vec![None; page_count];
1966
1967 for record in c.doc_structure.records() {
1968 let StructuralRecord::PageOverride(rec) = record else {
1969 continue;
1970 };
1971 match rec.scope {
1972 PageOverrideScope::All => {
1973 all_boxes = rec.boxes.merge_over(&all_boxes);
1974 if rec.rotate.is_some() {
1975 all_rotate = rec.rotate;
1976 }
1977 if let Some(new_aa) = &rec.additional_actions {
1978 all_aa = Some(match all_aa {
1979 Some(prev) => new_aa.merge_over(&prev),
1980 None => new_aa.clone(),
1981 });
1982 }
1983 }
1984 PageOverrideScope::Single(page) => {
1985 let idx = page as usize;
1986 if idx == 0 || idx > page_count {
1987 continue;
1988 }
1989 let i = idx - 1;
1990 per_page_boxes[i] = rec.boxes.merge_over(&per_page_boxes[i]);
1991 if rec.rotate.is_some() {
1992 per_page_rotate[i] = rec.rotate;
1993 }
1994 if let Some(new_aa) = &rec.additional_actions {
1995 per_page_aa[i] = Some(match per_page_aa[i].clone() {
1996 Some(prev) => new_aa.merge_over(&prev),
1997 None => new_aa.clone(),
1998 });
1999 }
2000 }
2001 _ => continue,
2002 }
2003 }
2004
2005 for i in 0..page_count {
2006 out[i].boxes = per_page_boxes[i].merge_over(&all_boxes);
2007 out[i].rotate = per_page_rotate[i].or(all_rotate);
2008 out[i].additional_actions = match (&per_page_aa[i], &all_aa) {
2009 (Some(p), Some(a)) => Some(p.merge_over(a)),
2010 (Some(p), None) => Some(p.clone()),
2011 (None, Some(a)) => Some(a.clone()),
2012 (None, None) => None,
2013 };
2014 }
2015 out
2016}
2017
2018fn days_to_ymd(days: u64) -> (u64, u64, u64) {
2020 let z = days + 719468;
2022 let era = z / 146097;
2023 let doe = z - era * 146097;
2024 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
2025 let y = yoe + era * 400;
2026 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2027 let mp = (5 * doy + 2) / 153;
2028 let d = doy - (153 * mp + 2) / 5 + 1;
2029 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2030 let y = if m <= 2 { y + 1 } else { y };
2031 (y, m, d)
2032}
2033
2034fn default_now_pdf_date() -> String {
2036 use std::time::SystemTime;
2037 let now = SystemTime::now()
2038 .duration_since(SystemTime::UNIX_EPOCH)
2039 .unwrap_or_default()
2040 .as_secs();
2041 let secs_per_day = 86400u64;
2042 let days = now / secs_per_day;
2043 let time_of_day = now % secs_per_day;
2044 let hours = time_of_day / 3600;
2045 let minutes = (time_of_day % 3600) / 60;
2046 let seconds = time_of_day % 60;
2047 let (year, month, day) = days_to_ymd(days);
2048 format!(
2049 "D:{:04}{:02}{:02}{:02}{:02}{:02}Z",
2050 year, month, day, hours, minutes, seconds
2051 )
2052}
2053
2054fn collect_viewer_prefs(ctx: &Context) -> stet_graphics::document_structure::ViewerPrefsRecord {
2062 use stet_graphics::document_structure::{StructuralRecord, ViewerPrefsRecord};
2063 let mut acc = ViewerPrefsRecord::default();
2064 for record in ctx.doc_structure.records() {
2065 if let StructuralRecord::ViewerPrefs(rec) = record {
2066 acc = rec.merge_over(&acc);
2067 }
2068 }
2069 acc
2070}
2071
2072fn collect_docinfo(ctx: &Context) -> stet_graphics::document_structure::DocInfoRecord {
2073 let mut acc = stet_graphics::document_structure::DocInfoRecord::default();
2074 for record in ctx.doc_structure.records() {
2075 let stet_graphics::document_structure::StructuralRecord::DocInfo(rec) = record else {
2076 continue;
2077 };
2078 if let Some(v) = &rec.title {
2079 acc.title = Some(v.clone());
2080 }
2081 if let Some(v) = &rec.author {
2082 acc.author = Some(v.clone());
2083 }
2084 if let Some(v) = &rec.subject {
2085 acc.subject = Some(v.clone());
2086 }
2087 if let Some(v) = &rec.keywords {
2088 acc.keywords = Some(v.clone());
2089 }
2090 if let Some(v) = &rec.creator {
2091 acc.creator = Some(v.clone());
2092 }
2093 if let Some(v) = &rec.producer {
2094 acc.producer = Some(v.clone());
2095 }
2096 if let Some(v) = &rec.creation_date {
2097 acc.creation_date = Some(v.clone());
2098 }
2099 if let Some(v) = &rec.mod_date {
2100 acc.mod_date = Some(v.clone());
2101 }
2102 if let Some(v) = rec.trapped {
2103 acc.trapped = Some(v);
2104 }
2105 }
2106 acc
2107}
2108
2109fn build_form_xobject(writer: &mut PdfWriter, form: &crate::content_stream::FormXObject) -> u32 {
2114 let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![
2115 (b"Type".to_vec(), PdfObj::name("XObject")),
2116 (b"Subtype".to_vec(), PdfObj::name("Form")),
2117 (b"FormType".to_vec(), PdfObj::Int(1)),
2118 (
2119 b"BBox".to_vec(),
2120 PdfObj::Array(vec![
2121 PdfObj::Real(form.bbox[0]),
2122 PdfObj::Real(form.bbox[1]),
2123 PdfObj::Real(form.bbox[2]),
2124 PdfObj::Real(form.bbox[3]),
2125 ]),
2126 ),
2127 ];
2128 if let Some(group_entries) = &form.group_dict_entries {
2129 let entries_clone: Vec<(Vec<u8>, PdfObj)> = group_entries
2130 .iter()
2131 .map(|(k, v)| (k.clone(), clone_pdfobj_shallow(v)))
2132 .collect();
2133 entries.push((b"Group".to_vec(), PdfObj::Dict(entries_clone)));
2134 }
2135 writer.add_stream(entries, &form.content, true)
2136}
2137
2138fn clone_pdfobj_shallow(v: &PdfObj) -> PdfObj {
2144 match v {
2145 PdfObj::Bool(b) => PdfObj::Bool(*b),
2146 PdfObj::Int(n) => PdfObj::Int(*n),
2147 PdfObj::Real(r) => PdfObj::Real(*r),
2148 PdfObj::Name(n) => PdfObj::Name(n.clone()),
2149 PdfObj::Ref(r) => PdfObj::Ref(*r),
2150 PdfObj::Null => PdfObj::Null,
2151 _ => panic!("clone_pdfobj_shallow: unsupported PdfObj variant in /Group dict"),
2152 }
2153}
2154
2155fn collect_ocg_ids<F>(visibility: &stet_graphics::display_list::OcgVisibility, mut visit: F)
2160where
2161 F: FnMut(u32, bool),
2162{
2163 use stet_graphics::display_list::OcgVisibility;
2164 fn walk_expr(
2165 e: &stet_graphics::display_list::VisibilityExpr,
2166 v: &mut impl FnMut(u32, bool),
2167 default_visible: bool,
2168 ) {
2169 use stet_graphics::display_list::VisibilityExpr;
2170 match e {
2171 VisibilityExpr::And(xs) | VisibilityExpr::Or(xs) => {
2172 for x in xs {
2173 walk_expr(x, v, default_visible);
2174 }
2175 }
2176 VisibilityExpr::Not(x) => walk_expr(x, v, default_visible),
2177 VisibilityExpr::Layer(id) => v(*id, default_visible),
2178 }
2179 }
2180 match visibility {
2181 OcgVisibility::Single {
2182 ocg_id,
2183 default_visible,
2184 } => visit(*ocg_id, *default_visible),
2185 OcgVisibility::Membership {
2186 ocg_ids,
2187 default_visible,
2188 ..
2189 } => {
2190 for &id in ocg_ids {
2191 visit(id, *default_visible);
2192 }
2193 }
2194 OcgVisibility::Expression {
2195 expr,
2196 default_visible,
2197 } => walk_expr(expr, &mut visit, *default_visible),
2198 }
2199}
2200
2201fn build_ocg_property_ref(
2208 writer: &mut PdfWriter,
2209 visibility: &stet_graphics::display_list::OcgVisibility,
2210 ocg_id_to_ref: &HashMap<u32, u32>,
2211) -> u32 {
2212 use stet_graphics::display_list::OcgVisibility;
2213 match visibility {
2214 OcgVisibility::Single { ocg_id, .. } => *ocg_id_to_ref.get(ocg_id).unwrap_or(&0),
2215 OcgVisibility::Membership {
2216 ocg_ids, policy, ..
2217 } => {
2218 use stet_graphics::display_list::MembershipPolicy;
2219 let policy_name: &[u8] = match policy {
2220 MembershipPolicy::AllOn => b"AllOn",
2221 MembershipPolicy::AnyOn => b"AnyOn",
2222 MembershipPolicy::AllOff => b"AllOff",
2223 MembershipPolicy::AnyOff => b"AnyOff",
2224 };
2225 let ocgs: Vec<PdfObj> = ocg_ids
2226 .iter()
2227 .filter_map(|id| ocg_id_to_ref.get(id).map(|&r| PdfObj::Ref(r)))
2228 .collect();
2229 writer.add_object(&PdfObj::Dict(vec![
2230 (b"Type".to_vec(), PdfObj::name("OCMD")),
2231 (b"OCGs".to_vec(), PdfObj::Array(ocgs)),
2232 (b"P".to_vec(), PdfObj::Name(policy_name.to_vec())),
2233 ]))
2234 }
2235 OcgVisibility::Expression { expr, .. } => {
2236 let ve = build_ve_array(writer, expr, ocg_id_to_ref);
2237 writer.add_object(&PdfObj::Dict(vec![
2238 (b"Type".to_vec(), PdfObj::name("OCMD")),
2239 (b"VE".to_vec(), ve),
2240 ]))
2241 }
2242 }
2243}
2244
2245fn build_ve_array(
2249 writer: &mut PdfWriter,
2250 expr: &stet_graphics::display_list::VisibilityExpr,
2251 ocg_id_to_ref: &HashMap<u32, u32>,
2252) -> PdfObj {
2253 use stet_graphics::display_list::VisibilityExpr;
2254 match expr {
2255 VisibilityExpr::And(xs) => {
2256 let mut arr = vec![PdfObj::name("And")];
2257 for x in xs {
2258 arr.push(build_ve_array(writer, x, ocg_id_to_ref));
2259 }
2260 PdfObj::Array(arr)
2261 }
2262 VisibilityExpr::Or(xs) => {
2263 let mut arr = vec![PdfObj::name("Or")];
2264 for x in xs {
2265 arr.push(build_ve_array(writer, x, ocg_id_to_ref));
2266 }
2267 PdfObj::Array(arr)
2268 }
2269 VisibilityExpr::Not(x) => PdfObj::Array(vec![
2270 PdfObj::name("Not"),
2271 build_ve_array(writer, x, ocg_id_to_ref),
2272 ]),
2273 VisibilityExpr::Layer(id) => match ocg_id_to_ref.get(id) {
2274 Some(&r) => PdfObj::Ref(r),
2275 None => PdfObj::Null,
2276 },
2277 }
2278}