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 pub fn set_output_path(&mut self, path: impl Into<String>) {
178 self.output_path = Some(path.into());
179 }
180
181 fn default_producer() -> String {
189 format!("stet {}", env!("CARGO_PKG_VERSION"))
190 }
191
192 fn build_info_dict(&self, ctx: Option<&Context>) -> Vec<(Vec<u8>, PdfObj)> {
198 let docinfo = ctx.map(|c| collect_docinfo(c)).unwrap_or_default();
199
200 let producer = docinfo
201 .producer
202 .clone()
203 .unwrap_or_else(Self::default_producer);
204 let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![(
205 b"Producer".to_vec(),
206 PdfObj::LitString(producer.into_bytes()),
207 )];
208
209 let title = docinfo.title.clone().or_else(|| {
211 self.output_path
212 .as_deref()
213 .and_then(|p| std::path::Path::new(p).file_stem())
214 .and_then(|s| s.to_str())
215 .map(|s| s.to_string())
216 });
217 if let Some(t) = title {
218 entries.push((b"Title".to_vec(), PdfObj::LitString(t.into_bytes())));
219 }
220
221 for (key, value) in [
222 (&b"Author"[..], &docinfo.author),
223 (&b"Subject"[..], &docinfo.subject),
224 (&b"Keywords"[..], &docinfo.keywords),
225 (&b"Creator"[..], &docinfo.creator),
226 ] {
227 if let Some(v) = value {
228 entries.push((key.to_vec(), PdfObj::LitString(v.clone().into_bytes())));
229 }
230 }
231
232 let creation_date = docinfo
234 .creation_date_string()
235 .unwrap_or_else(default_now_pdf_date);
236 entries.push((
237 b"CreationDate".to_vec(),
238 PdfObj::LitString(creation_date.into_bytes()),
239 ));
240
241 if let Some(md) = docinfo.mod_date_string() {
242 entries.push((b"ModDate".to_vec(), PdfObj::LitString(md.into_bytes())));
243 }
244
245 if let Some(t) = docinfo.trapped {
246 let name: &[u8] = match t {
247 stet_graphics::document_structure::TrappedState::True => b"True",
248 stet_graphics::document_structure::TrappedState::False => b"False",
249 stet_graphics::document_structure::TrappedState::Unknown => b"Unknown",
250 _ => b"Unknown",
251 };
252 entries.push((b"Trapped".to_vec(), PdfObj::Name(name.to_vec())));
253 }
254
255 entries
256 }
257
258 fn build_pdf(&self, ctx: Option<&Context>) -> Result<(PdfWriter, u32, u32), String> {
260 let mut writer = PdfWriter::new();
261
262 let catalog_ref = writer.alloc_obj();
264 let pages_ref = writer.alloc_obj();
265
266 let mut font_tracker = FontTracker::new();
268
269 let mut page_results: Vec<(ContentStreamResult, &PageData)> = Vec::new();
271 for page in &self.pages {
272 let result = content_stream::build_content_stream(
273 &page.display_list,
274 page.page_w,
275 page.page_h,
276 page.dpi,
277 ctx,
278 &mut font_tracker,
279 self.emit_page_box_clip,
280 );
281 page_results.push((result, page));
282 }
283
284 let font_obj_map: HashMap<String, u32> =
286 self.embed_all_fonts(&mut writer, &font_tracker, ctx);
287
288 let mut ocg_id_to_ref: HashMap<u32, u32> = HashMap::new();
294 let mut ocg_default_off: HashSet<u32> = HashSet::new();
295 let mut ocg_order: Vec<u32> = Vec::new();
296 for (result, _) in &page_results {
297 for marker in &result.ocg_marker_refs {
298 let mut visit = |ocg_id: u32, default_visible: bool| {
299 if let std::collections::hash_map::Entry::Vacant(e) =
300 ocg_id_to_ref.entry(ocg_id)
301 {
302 let r = writer.add_object(&PdfObj::Dict(vec![
303 (b"Type".to_vec(), PdfObj::name("OCG")),
304 (
305 b"Name".to_vec(),
306 PdfObj::LitString(format!("Layer {}", ocg_id).into_bytes()),
307 ),
308 ]));
309 e.insert(r);
310 ocg_order.push(ocg_id);
311 if !default_visible {
312 ocg_default_off.insert(ocg_id);
313 }
314 }
315 };
316 collect_ocg_ids(&marker.visibility, &mut visit);
317 }
318 }
319
320 let page_refs: Vec<u32> = (0..page_results.len())
324 .map(|_| writer.alloc_obj())
325 .collect();
326
327 let mut per_page_annots: Vec<Vec<u32>> = ctx
333 .map(|c| {
334 let records: Vec<stet_graphics::document_structure::AnnotationRecord> = c
335 .doc_structure
336 .records()
337 .iter()
338 .filter_map(|r| match r {
339 stet_graphics::document_structure::StructuralRecord::Annotation(rec) => {
340 Some(rec.clone())
341 }
342 _ => None,
343 })
344 .collect();
345 if records.is_empty() {
346 return vec![Vec::new(); page_refs.len()];
347 }
348 crate::annotations::collect_per_page(&mut writer, &records, &page_refs)
349 })
350 .unwrap_or_else(|| vec![Vec::new(); page_refs.len()]);
351
352 let acroform_output = ctx.and_then(|c| {
357 let widgets: Vec<(usize, stet_graphics::document_structure::AnnotationRecord)> = c
358 .doc_structure
359 .records()
360 .iter()
361 .enumerate()
362 .filter_map(|(i, r)| match r {
363 stet_graphics::document_structure::StructuralRecord::Annotation(rec)
364 if matches!(
365 rec.subtype,
366 stet_graphics::document_structure::AnnotationSubtype::Widget(_)
367 ) =>
368 {
369 Some((i, rec.clone()))
370 }
371 _ => None,
372 })
373 .collect();
374 let form_record = c
375 .doc_structure
376 .records()
377 .iter()
378 .filter_map(|r| match r {
379 stet_graphics::document_structure::StructuralRecord::Form(rec) => {
380 Some(rec.clone())
381 }
382 _ => None,
383 })
384 .reduce(|acc, next| next.merge_over(&acc));
385 crate::form_fields::write_form(
386 &mut writer,
387 &widgets,
388 form_record.as_ref(),
389 page_refs.len(),
390 )
391 });
392 if let Some(out) = &acroform_output {
393 for (i, refs) in out.per_page_widget_refs.iter().enumerate() {
394 per_page_annots[i].extend(refs);
395 }
396 }
397
398 let per_page_overrides = compute_page_overrides(ctx, page_refs.len());
403
404 for (i, (result, page)) in page_results.iter().enumerate() {
406 self.build_page(
407 &mut writer,
408 page,
409 pages_ref,
410 page_refs[i],
411 result,
412 &font_obj_map,
413 &mut font_tracker,
414 &per_page_annots[i],
415 &per_page_overrides[i],
416 &ocg_id_to_ref,
417 )?;
418 }
419
420 writer.set_object(
422 pages_ref,
423 &PdfObj::Dict(vec![
424 (b"Type".to_vec(), PdfObj::name("Pages")),
425 (
426 b"Kids".to_vec(),
427 PdfObj::Array(page_refs.iter().map(|&r| PdfObj::Ref(r)).collect()),
428 ),
429 (b"Count".to_vec(), PdfObj::Int(page_refs.len() as i64)),
430 ]),
431 );
432
433 let outlines_ref = ctx.and_then(|c| {
437 let records: Vec<stet_graphics::document_structure::OutlineRecord> = c
438 .doc_structure
439 .records()
440 .iter()
441 .filter_map(|r| match r {
442 stet_graphics::document_structure::StructuralRecord::Outline(rec) => {
443 Some(rec.clone())
444 }
445 _ => None,
446 })
447 .collect();
448 if records.is_empty() {
449 return None;
450 }
451 let tree = stet_graphics::document_structure::build_outline_tree(&records);
452 crate::outline::write_outline_tree(&mut writer, &tree, &page_refs)
453 });
454
455 let dests_leaf = ctx.and_then(|c| {
460 let records: Vec<stet_graphics::document_structure::DestRecord> = c
461 .doc_structure
462 .records()
463 .iter()
464 .filter_map(|r| match r {
465 stet_graphics::document_structure::StructuralRecord::Dest(rec) => {
466 Some(rec.clone())
467 }
468 _ => None,
469 })
470 .collect();
471 crate::names::build_dests_leaf(&mut writer, &records, &page_refs)
472 });
473 let embedded_files_leaf = ctx.and_then(|c| {
474 let records: Vec<stet_graphics::document_structure::EmbedRecord> = c
475 .doc_structure
476 .records()
477 .iter()
478 .filter_map(|r| match r {
479 stet_graphics::document_structure::StructuralRecord::Embed(rec) => {
480 Some(rec.clone())
481 }
482 _ => None,
483 })
484 .collect();
485 crate::attachments::build_embedded_files_leaf(&mut writer, &records)
486 });
487 let names_ref =
488 crate::names::write_names_root(&mut writer, dests_leaf, embedded_files_leaf);
489
490 let merged_prefs = ctx.map(collect_viewer_prefs).unwrap_or_default();
495 let viewer_prefs_ref = crate::metadata::write_viewer_prefs(&mut writer, &merged_prefs);
496
497 let metadata_ref = ctx.and_then(|c| {
499 c.doc_structure
500 .records()
501 .iter()
502 .rev()
503 .find_map(|r| match r {
504 stet_graphics::document_structure::StructuralRecord::Metadata(rec) => {
505 Some(rec.clone())
506 }
507 _ => None,
508 })
509 .map(|rec| crate::metadata::write_xmp_metadata(&mut writer, &rec))
510 });
511
512 let mut catalog_entries = vec![
514 (b"Type".to_vec(), PdfObj::name("Catalog")),
515 (b"Pages".to_vec(), PdfObj::Ref(pages_ref)),
516 ];
517
518 if !ocg_id_to_ref.is_empty() {
526 let make_array = || -> Vec<PdfObj> {
527 ocg_order
528 .iter()
529 .filter_map(|id| ocg_id_to_ref.get(id).map(|&r| PdfObj::Ref(r)))
530 .collect()
531 };
532 let off_array: Vec<PdfObj> = ocg_order
533 .iter()
534 .filter(|id| ocg_default_off.contains(id))
535 .filter_map(|id| ocg_id_to_ref.get(id).map(|&r| PdfObj::Ref(r)))
536 .collect();
537 let mut d_entries: Vec<(Vec<u8>, PdfObj)> = vec![
538 (b"Name".to_vec(), PdfObj::LitString(b"Default".to_vec())),
539 (b"BaseState".to_vec(), PdfObj::name("ON")),
540 (b"Order".to_vec(), PdfObj::Array(make_array())),
541 ];
542 if !off_array.is_empty() {
543 d_entries.push((b"OFF".to_vec(), PdfObj::Array(off_array)));
544 }
545 let ocprops = writer.add_object(&PdfObj::Dict(vec![
546 (b"OCGs".to_vec(), PdfObj::Array(make_array())),
547 (b"D".to_vec(), PdfObj::Dict(d_entries)),
548 ]));
549 catalog_entries.push((b"OCProperties".to_vec(), PdfObj::Ref(ocprops)));
550 }
551 if let Some(outline_ref) = outlines_ref {
552 catalog_entries.push((b"Outlines".to_vec(), PdfObj::Ref(outline_ref)));
553 }
554 if let Some(names_ref) = names_ref {
555 catalog_entries.push((b"Names".to_vec(), PdfObj::Ref(names_ref)));
556 }
557 if let Some(viewer_prefs_ref) = viewer_prefs_ref {
558 catalog_entries.push((b"ViewerPreferences".to_vec(), PdfObj::Ref(viewer_prefs_ref)));
559 }
560 if let Some(layout_bytes) = merged_prefs
562 .page_layout
563 .as_deref()
564 .and_then(crate::metadata::validated_page_layout)
565 {
566 catalog_entries.push((b"PageLayout".to_vec(), PdfObj::Name(layout_bytes.to_vec())));
567 }
568 let effective_page_mode: Option<Vec<u8>> = merged_prefs
572 .page_mode
573 .as_deref()
574 .and_then(crate::metadata::validated_page_mode)
575 .map(|v| v.to_vec())
576 .or_else(|| outlines_ref.map(|_| b"UseOutlines".to_vec()));
577 if let Some(mode) = effective_page_mode {
578 catalog_entries.push((b"PageMode".to_vec(), PdfObj::Name(mode)));
579 }
580 if let Some(metadata_ref) = metadata_ref {
581 catalog_entries.push((b"Metadata".to_vec(), PdfObj::Ref(metadata_ref)));
582 }
583 if let Some(out) = &acroform_output {
584 catalog_entries.push((b"AcroForm".to_vec(), PdfObj::Ref(out.acroform_ref)));
585 }
586
587 if !self.output_intents.is_empty() {
591 let intent_refs = emit_output_intents(&mut writer, &self.output_intents);
592 if !intent_refs.is_empty() {
593 catalog_entries.push((
594 b"OutputIntents".to_vec(),
595 PdfObj::Array(intent_refs.into_iter().map(PdfObj::Ref).collect()),
596 ));
597 }
598 }
599
600 writer.set_object(catalog_ref, &PdfObj::Dict(catalog_entries));
601
602 let info_ref = writer.alloc_obj();
605 let info_entries = self.build_info_dict(ctx);
606 writer.set_object(info_ref, &PdfObj::Dict(info_entries));
607
608 Ok((writer, catalog_ref, info_ref))
609 }
610
611 fn embed_all_fonts(
614 &self,
615 writer: &mut PdfWriter,
616 font_tracker: &FontTracker,
617 ctx: Option<&Context>,
618 ) -> HashMap<String, u32> {
619 let mut map = HashMap::new();
620 for usage in font_tracker.fonts() {
621 let font_ref = if let Some(c) = ctx {
622 font_embedder::build_font_resource(writer, usage, c).unwrap_or_else(|| {
623 let tu = font_embedder::build_tounicode_for_fallback(writer, usage, c);
624 self.build_font_reference(writer, usage, tu)
625 })
626 } else {
627 self.build_font_reference(writer, usage, None)
628 };
629 map.insert(usage.pdf_name.clone(), font_ref);
630 }
631 map
632 }
633
634 #[allow(clippy::too_many_arguments)]
640 fn build_page(
641 &self,
642 writer: &mut PdfWriter,
643 page: &PageData,
644 pages_ref: u32,
645 page_ref: u32,
646 result: &ContentStreamResult,
647 font_obj_map: &HashMap<String, u32>,
648 font_tracker: &mut FontTracker,
649 annot_refs: &[u32],
650 overrides: &EffectivePageOverride,
651 ocg_id_to_ref: &HashMap<u32, u32>,
652 ) -> Result<(), String> {
653 let ContentStreamResult {
654 content,
655 images,
656 shading_refs,
657 used_font_names,
658 ext_gstate_dicts,
659 color_spaces,
660 icc_color_spaces,
661 pattern_refs,
662 pattern_cs_entries,
663 transfer_refs,
664 halftone_refs,
665 bg_ucr_refs,
666 soft_mask_refs,
667 ocg_marker_refs,
668 form_xobjects,
669 } = result;
670
671 let mut xobject_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
677 for (i, img) in images.iter().enumerate() {
678 let img_ref = self.build_image_xobject(writer, img);
679 xobject_entries.push((format!("Im{}", i).into_bytes(), PdfObj::Ref(img_ref)));
680 }
681 let mut form_obj_refs: Vec<u32> = Vec::with_capacity(form_xobjects.len());
682 for (i, form) in form_xobjects.iter().enumerate() {
683 let form_ref = build_form_xobject(writer, form);
684 form_obj_refs.push(form_ref);
685 xobject_entries.push((format!("X{}", i).into_bytes(), PdfObj::Ref(form_ref)));
686 }
687
688 let mut shading_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
690 for (i, sh_ref) in shading_refs.iter().enumerate() {
691 let sh_obj = match sh_ref {
692 ShadingRef::Axial(p) => shading_ops::build_axial_shading(writer, p),
693 ShadingRef::Radial(p) => shading_ops::build_radial_shading(writer, p),
694 ShadingRef::Mesh(p) => shading_ops::build_mesh_shading(writer, p),
695 ShadingRef::Patch(p) => shading_ops::build_patch_shading(writer, p),
696 };
697 shading_entries.push((format!("Sh{}", i).into_bytes(), PdfObj::Ref(sh_obj)));
698 }
699
700 let mut font_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
702 for name in used_font_names {
703 if let Some(&obj_ref) = font_obj_map.get(name) {
704 font_entries.push((name.clone().into_bytes(), PdfObj::Ref(obj_ref)));
705 }
706 }
707
708 let mut resources: Vec<(Vec<u8>, PdfObj)> = Vec::new();
710 if !font_entries.is_empty() {
711 resources.push((b"Font".to_vec(), PdfObj::Dict(font_entries)));
712 }
713 if !xobject_entries.is_empty() {
714 resources.push((b"XObject".to_vec(), PdfObj::Dict(xobject_entries)));
715 }
716 if !shading_entries.is_empty() {
717 resources.push((b"Shading".to_vec(), PdfObj::Dict(shading_entries)));
718 }
719
720 if !ext_gstate_dicts.is_empty() {
722 let mut gs_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
723 for (i, gs_dict) in ext_gstate_dicts.iter().enumerate() {
724 let mut entries: Vec<(Vec<u8>, PdfObj)> = gs_dict
726 .entries
727 .iter()
728 .map(|(k, v)| {
729 let obj = match v {
730 PdfObj::Bool(b) => PdfObj::Bool(*b),
731 PdfObj::Int(n) => PdfObj::Int(*n),
732 PdfObj::Real(r) => PdfObj::Real(*r),
733 PdfObj::Name(n) => PdfObj::Name(n.clone()),
734 PdfObj::Ref(r) => PdfObj::Ref(*r),
735 _ => PdfObj::Null,
736 };
737 (k.clone(), obj)
738 })
739 .collect();
740
741 if let Some(tr) = transfer_refs.iter().find(|r| r.ext_gstate_idx == i) {
743 let tr2_value = build_transfer_tr2(writer, &tr.tables, tr.is_color);
744 entries.push((b"TR2".to_vec(), tr2_value));
745 }
746
747 if let Some(hr) = halftone_refs.iter().find(|r| r.ext_gstate_idx == i) {
749 let ht_value = build_halftone_ht(writer, &hr.state);
750 entries.push((b"HT".to_vec(), ht_value));
751 }
752
753 if let Some(br) = bg_ucr_refs.iter().find(|r| r.ext_gstate_idx == i) {
755 if let Some(ref bg) = br.state.bg {
756 let func_ref = build_type0_function(writer, bg);
757 entries.push((b"BG2".to_vec(), PdfObj::Ref(func_ref)));
758 }
759 if let Some(ref ucr) = br.state.ucr {
760 let func_ref = build_type0_function_signed(writer, ucr);
761 entries.push((b"UCR2".to_vec(), PdfObj::Ref(func_ref)));
762 }
763 }
764
765 if let Some(sm) = soft_mask_refs.iter().find(|r| r.ext_gstate_idx == i) {
769 let mask_ref = form_obj_refs[sm.mask_form_idx];
770 let subtype_name: &[u8] = match sm.subtype {
771 stet_graphics::display_list::SoftMaskSubtype::Alpha => b"Alpha",
772 stet_graphics::display_list::SoftMaskSubtype::Luminosity => b"Luminosity",
773 };
774 let mut smask_entries: Vec<(Vec<u8>, PdfObj)> = vec![
775 (b"Type".to_vec(), PdfObj::name("Mask")),
776 (b"S".to_vec(), PdfObj::Name(subtype_name.to_vec())),
777 (b"G".to_vec(), PdfObj::Ref(mask_ref)),
778 ];
779 if let Some([r, g, b]) = sm.backdrop_color {
780 smask_entries.push((
781 b"BC".to_vec(),
782 PdfObj::Array(vec![PdfObj::Real(r), PdfObj::Real(g), PdfObj::Real(b)]),
783 ));
784 }
785 if sm.transfer_invert {
786 let tr_ref = build_invert_transfer(writer);
789 smask_entries.push((b"TR".to_vec(), PdfObj::Ref(tr_ref)));
790 }
791 entries.push((b"SMask".to_vec(), PdfObj::Dict(smask_entries)));
792 }
793
794 let gs_ref = writer.add_object(&PdfObj::Dict(entries));
795 gs_entries.push((format!("GS{}", i).into_bytes(), PdfObj::Ref(gs_ref)));
796 }
797 resources.push((b"ExtGState".to_vec(), PdfObj::Dict(gs_entries)));
798 }
799
800 let mut cs_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
802 for (name, spot_cs) in color_spaces {
803 let cs_obj = build_spot_colorspace(spot_cs, writer);
804 cs_entries.push((name.clone().into_bytes(), cs_obj));
805 }
806 for (name, icc_cs) in icc_color_spaces {
809 let icc_ref = writer.add_stream(
810 vec![(b"N".to_vec(), PdfObj::Int(icc_cs.n as i64))],
811 &icc_cs.profile_data,
812 true,
813 );
814 cs_entries.push((
815 name.clone().into_bytes(),
816 PdfObj::Array(vec![PdfObj::name("ICCBased"), PdfObj::Ref(icc_ref)]),
817 ));
818 }
819 for (name, cs_obj) in pattern_cs_entries {
821 let obj = match cs_obj {
823 PdfObj::Array(items) => {
824 let cloned: Vec<PdfObj> = items
825 .iter()
826 .map(|item| match item {
827 PdfObj::Name(n) => PdfObj::Name(n.clone()),
828 PdfObj::Int(n) => PdfObj::Int(*n),
829 PdfObj::Real(n) => PdfObj::Real(*n),
830 PdfObj::Ref(r) => PdfObj::Ref(*r),
831 _ => PdfObj::Null,
832 })
833 .collect();
834 PdfObj::Array(cloned)
835 }
836 _ => PdfObj::Null,
837 };
838 cs_entries.push((name.clone().into_bytes(), obj));
839 }
840 if !cs_entries.is_empty() {
841 resources.push((b"ColorSpace".to_vec(), PdfObj::Dict(cs_entries)));
842 }
843
844 if !ocg_marker_refs.is_empty() {
849 let mut props_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
850 for marker in ocg_marker_refs {
851 let prop_ref = build_ocg_property_ref(writer, &marker.visibility, ocg_id_to_ref);
852 props_entries.push((
853 marker.resource_name.clone().into_bytes(),
854 PdfObj::Ref(prop_ref),
855 ));
856 }
857 resources.push((b"Properties".to_vec(), PdfObj::Dict(props_entries)));
858 }
859
860 if !pattern_refs.is_empty() {
862 let mut pattern_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
863 for (i, pat_ref) in pattern_refs.iter().enumerate() {
864 let tile_result =
865 content_stream::build_tile_content_stream(&pat_ref.tile, font_tracker);
866
867 let mut tile_resources: Vec<(Vec<u8>, PdfObj)> = Vec::new();
869
870 if !tile_result.images.is_empty() {
872 let mut tile_xobj: Vec<(Vec<u8>, PdfObj)> = Vec::new();
873 for (j, img) in tile_result.images.iter().enumerate() {
874 let img_ref = self.build_image_xobject(writer, img);
875 tile_xobj.push((format!("Im{}", j).into_bytes(), PdfObj::Ref(img_ref)));
876 }
877 tile_resources.push((b"XObject".to_vec(), PdfObj::Dict(tile_xobj)));
878 }
879
880 if !tile_result.shading_refs.is_empty() {
882 let mut tile_sh: Vec<(Vec<u8>, PdfObj)> = Vec::new();
883 for (j, sh_ref) in tile_result.shading_refs.iter().enumerate() {
884 let sh_obj = match sh_ref {
885 ShadingRef::Axial(p) => shading_ops::build_axial_shading(writer, p),
886 ShadingRef::Radial(p) => shading_ops::build_radial_shading(writer, p),
887 ShadingRef::Mesh(p) => shading_ops::build_mesh_shading(writer, p),
888 ShadingRef::Patch(p) => shading_ops::build_patch_shading(writer, p),
889 };
890 tile_sh.push((format!("Sh{}", j).into_bytes(), PdfObj::Ref(sh_obj)));
891 }
892 tile_resources.push((b"Shading".to_vec(), PdfObj::Dict(tile_sh)));
893 }
894
895 if !tile_result.used_font_names.is_empty() {
897 let mut tile_fonts: Vec<(Vec<u8>, PdfObj)> = Vec::new();
898 for name in &tile_result.used_font_names {
899 if let Some(&obj_ref) = font_obj_map.get(name) {
900 tile_fonts.push((name.clone().into_bytes(), PdfObj::Ref(obj_ref)));
901 }
902 }
903 if !tile_fonts.is_empty() {
904 tile_resources.push((b"Font".to_vec(), PdfObj::Dict(tile_fonts)));
905 }
906 }
907
908 if !tile_result.ext_gstate_dicts.is_empty() {
910 let mut tile_gs: Vec<(Vec<u8>, PdfObj)> = Vec::new();
911 for (j, gs_dict) in tile_result.ext_gstate_dicts.iter().enumerate() {
912 let mut entries: Vec<(Vec<u8>, PdfObj)> = gs_dict
913 .entries
914 .iter()
915 .map(|(k, v)| {
916 let obj = match v {
917 PdfObj::Bool(b) => PdfObj::Bool(*b),
918 PdfObj::Int(n) => PdfObj::Int(*n),
919 PdfObj::Real(r) => PdfObj::Real(*r),
920 PdfObj::Name(n) => PdfObj::Name(n.clone()),
921 PdfObj::Ref(r) => PdfObj::Ref(*r),
922 _ => PdfObj::Null,
923 };
924 (k.clone(), obj)
925 })
926 .collect();
927 if let Some(tr) = tile_result
928 .transfer_refs
929 .iter()
930 .find(|r| r.ext_gstate_idx == j)
931 {
932 let tr2_value = build_transfer_tr2(writer, &tr.tables, tr.is_color);
933 entries.push((b"TR2".to_vec(), tr2_value));
934 }
935 if let Some(hr) = tile_result
936 .halftone_refs
937 .iter()
938 .find(|r| r.ext_gstate_idx == j)
939 {
940 let ht_value = build_halftone_ht(writer, &hr.state);
941 entries.push((b"HT".to_vec(), ht_value));
942 }
943 if let Some(br) = tile_result
944 .bg_ucr_refs
945 .iter()
946 .find(|r| r.ext_gstate_idx == j)
947 {
948 if let Some(ref bg) = br.state.bg {
949 let func_ref = build_type0_function(writer, bg);
950 entries.push((b"BG2".to_vec(), PdfObj::Ref(func_ref)));
951 }
952 if let Some(ref ucr) = br.state.ucr {
953 let func_ref = build_type0_function_signed(writer, ucr);
954 entries.push((b"UCR2".to_vec(), PdfObj::Ref(func_ref)));
955 }
956 }
957 let gs_ref = writer.add_object(&PdfObj::Dict(entries));
958 tile_gs.push((format!("GS{}", j).into_bytes(), PdfObj::Ref(gs_ref)));
959 }
960 tile_resources.push((b"ExtGState".to_vec(), PdfObj::Dict(tile_gs)));
961 }
962
963 if !tile_result.color_spaces.is_empty() || !tile_result.icc_color_spaces.is_empty()
965 {
966 let mut tile_cs: Vec<(Vec<u8>, PdfObj)> = Vec::new();
967 for (name, spot_cs) in &tile_result.color_spaces {
968 let cs_obj = build_spot_colorspace(spot_cs, writer);
969 tile_cs.push((name.clone().into_bytes(), cs_obj));
970 }
971 for (name, icc_cs) in &tile_result.icc_color_spaces {
972 let icc_ref = writer.add_stream(
973 vec![(b"N".to_vec(), PdfObj::Int(icc_cs.n as i64))],
974 &icc_cs.profile_data,
975 true,
976 );
977 tile_cs.push((
978 name.clone().into_bytes(),
979 PdfObj::Array(vec![PdfObj::name("ICCBased"), PdfObj::Ref(icc_ref)]),
980 ));
981 }
982 tile_resources.push((b"ColorSpace".to_vec(), PdfObj::Dict(tile_cs)));
983 }
984
985 let m = &pat_ref.pattern_matrix;
987 let pat_dict = vec![
988 (b"Type".to_vec(), PdfObj::name("Pattern")),
989 (b"PatternType".to_vec(), PdfObj::Int(1)),
990 (
991 b"PaintType".to_vec(),
992 PdfObj::Int(pat_ref.paint_type as i64),
993 ),
994 (b"TilingType".to_vec(), PdfObj::Int(1)),
995 (
996 b"BBox".to_vec(),
997 PdfObj::Array(vec![
1000 PdfObj::Real(pat_ref.bbox[0] - 0.5),
1001 PdfObj::Real(pat_ref.bbox[1] - 0.5),
1002 PdfObj::Real(pat_ref.bbox[2] + 0.5),
1003 PdfObj::Real(pat_ref.bbox[3] + 0.5),
1004 ]),
1005 ),
1006 (b"XStep".to_vec(), PdfObj::Real(pat_ref.xstep)),
1007 (b"YStep".to_vec(), PdfObj::Real(pat_ref.ystep)),
1008 (
1009 b"Matrix".to_vec(),
1010 PdfObj::Array(vec![
1011 PdfObj::Real(m.a),
1012 PdfObj::Real(m.b),
1013 PdfObj::Real(m.c),
1014 PdfObj::Real(m.d),
1015 PdfObj::Real(m.tx),
1016 PdfObj::Real(m.ty),
1017 ]),
1018 ),
1019 (b"Resources".to_vec(), PdfObj::Dict(tile_resources)),
1020 ];
1021
1022 let pat_obj = writer.add_stream(pat_dict, &tile_result.content, true);
1023 pattern_entries.push((format!("P{}", i).into_bytes(), PdfObj::Ref(pat_obj)));
1024 }
1025
1026 resources.push((b"Pattern".to_vec(), PdfObj::Dict(pattern_entries)));
1027 }
1028
1029 let content_ref = writer.add_stream(Vec::new(), content, true);
1031
1032 let mut page_entries = vec![
1034 (b"Type".to_vec(), PdfObj::name("Page")),
1035 (b"Parent".to_vec(), PdfObj::Ref(pages_ref)),
1036 (
1037 b"MediaBox".to_vec(),
1038 PdfObj::Array(vec![
1039 PdfObj::Int(0),
1040 PdfObj::Int(0),
1041 PdfObj::Real(page.width_pts),
1042 PdfObj::Real(page.height_pts),
1043 ]),
1044 ),
1045 (b"Contents".to_vec(), PdfObj::Ref(content_ref)),
1046 (b"Resources".to_vec(), PdfObj::Dict(resources)),
1047 ];
1048 let page_cs: Option<&str> = match page.display_list.page_group_color_space() {
1056 stet_graphics::display_list::GroupColorSpace::DeviceGray => Some("DeviceGray"),
1057 stet_graphics::display_list::GroupColorSpace::DeviceRGB => Some("DeviceRGB"),
1058 stet_graphics::display_list::GroupColorSpace::DeviceCMYK => Some("DeviceCMYK"),
1059 stet_graphics::display_list::GroupColorSpace::Inherited => None,
1060 };
1061 if let Some(cs) = page_cs {
1062 page_entries.push((
1063 b"Group".to_vec(),
1064 PdfObj::Dict(vec![
1065 (b"Type".to_vec(), PdfObj::name("Group")),
1066 (b"S".to_vec(), PdfObj::name("Transparency")),
1067 (b"CS".to_vec(), PdfObj::name(cs)),
1068 ]),
1069 ));
1070 }
1071 let effective_trim = overrides.boxes.trim_box.or_else(|| {
1075 page.trim_box
1076 .map(|(llx, lly, urx, ury)| [llx, lly, urx, ury])
1077 });
1078 for (name, b) in [
1079 (b"CropBox".as_slice(), overrides.boxes.crop_box),
1080 (b"BleedBox".as_slice(), overrides.boxes.bleed_box),
1081 (b"TrimBox".as_slice(), effective_trim),
1082 (b"ArtBox".as_slice(), overrides.boxes.art_box),
1083 ] {
1084 if let Some([llx, lly, urx, ury]) = b {
1085 page_entries.push((
1086 name.to_vec(),
1087 PdfObj::Array(vec![
1088 PdfObj::Real(llx),
1089 PdfObj::Real(lly),
1090 PdfObj::Real(urx),
1091 PdfObj::Real(ury),
1092 ]),
1093 ));
1094 }
1095 }
1096 if let Some(rotate) = overrides.rotate
1097 && matches!(rotate, 0 | 90 | 180 | 270 | -90 | -180 | -270)
1098 {
1099 page_entries.push((b"Rotate".to_vec(), PdfObj::Int(rotate as i64)));
1100 }
1101 if !annot_refs.is_empty() {
1102 page_entries.push((
1103 b"Annots".to_vec(),
1104 PdfObj::Array(annot_refs.iter().map(|r| PdfObj::Ref(*r)).collect()),
1105 ));
1106 }
1107 if let Some(aa) = &overrides.additional_actions
1108 && !aa.is_empty()
1109 {
1110 let mut aa_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
1115 if let Some(action) = &aa.on_open
1116 && let Some(dict) = crate::outline::encode_action(action, &[])
1117 {
1118 aa_entries.push((b"O".to_vec(), dict));
1119 }
1120 if let Some(action) = &aa.on_close
1121 && let Some(dict) = crate::outline::encode_action(action, &[])
1122 {
1123 aa_entries.push((b"C".to_vec(), dict));
1124 }
1125 if !aa_entries.is_empty() {
1126 page_entries.push((b"AA".to_vec(), PdfObj::Dict(aa_entries)));
1127 }
1128 }
1129 writer.set_object(page_ref, &PdfObj::Dict(page_entries));
1130
1131 Ok(())
1132 }
1133
1134 fn build_font_reference(
1140 &self,
1141 writer: &mut PdfWriter,
1142 usage: &crate::font_tracker::FontUsage,
1143 tounicode_override: Option<u32>,
1144 ) -> u32 {
1145 let tounicode_ref = tounicode_override.or_else(|| self.build_tounicode_cmap(writer, usage));
1147
1148 let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![
1149 (b"Type".to_vec(), PdfObj::name("Font")),
1150 (b"Subtype".to_vec(), PdfObj::name("Type1")),
1151 (b"BaseFont".to_vec(), PdfObj::Name(usage.font_name.clone())),
1152 ];
1153
1154 if !usage.is_standard_14 {
1155 entries.push((b"Encoding".to_vec(), PdfObj::name("WinAnsiEncoding")));
1157 }
1158
1159 if let Some(tu_ref) = tounicode_ref {
1160 entries.push((b"ToUnicode".to_vec(), PdfObj::Ref(tu_ref)));
1161 }
1162
1163 writer.add_object(&PdfObj::Dict(entries))
1164 }
1165
1166 fn build_tounicode_cmap(
1173 &self,
1174 writer: &mut PdfWriter,
1175 usage: &crate::font_tracker::FontUsage,
1176 ) -> Option<u32> {
1177 use std::collections::HashMap;
1178
1179 let mut map: HashMap<u16, u16> = HashMap::new();
1180
1181 for &code in &usage.used_codes {
1182 if code <= 255 {
1183 if (0x20..=0x7E).contains(&code) {
1185 map.insert(code, code);
1186 }
1187 }
1188 }
1189
1190 if map.is_empty() {
1191 return None;
1192 }
1193
1194 let font_name = String::from_utf8_lossy(&usage.font_name);
1195 let cmap_data = generate_tounicode_cmap(&map, &font_name);
1196 Some(writer.add_stream(Vec::new(), &cmap_data, true))
1197 }
1198
1199 fn build_image_xobject(&self, writer: &mut PdfWriter, img: &ImageXObject) -> u32 {
1201 let smask_ref = img.smask_data.as_ref().map(|smask_data| {
1203 writer.add_stream(
1204 vec![
1205 (b"Type".to_vec(), PdfObj::name("XObject")),
1206 (b"Subtype".to_vec(), PdfObj::name("Image")),
1207 (b"Width".to_vec(), PdfObj::Int(img.width as i64)),
1208 (b"Height".to_vec(), PdfObj::Int(img.height as i64)),
1209 (b"ColorSpace".to_vec(), PdfObj::name("DeviceGray")),
1210 (b"BitsPerComponent".to_vec(), PdfObj::Int(8)),
1211 (b"Interpolate".to_vec(), PdfObj::Bool(false)),
1212 ],
1213 smask_data,
1214 true,
1215 )
1216 });
1217
1218 let icc_ref = img.icc_profile.as_ref().map(|icc| {
1220 writer.add_stream(
1221 vec![(b"N".to_vec(), PdfObj::Int(icc.n as i64))],
1222 &icc.data,
1223 true,
1224 )
1225 });
1226
1227 let cs_obj = build_pdf_colorspace(&img.pdf_color_space, icc_ref, writer);
1229
1230 let mut entries = vec![
1232 (b"Type".to_vec(), PdfObj::name("XObject")),
1233 (b"Subtype".to_vec(), PdfObj::name("Image")),
1234 (b"Width".to_vec(), PdfObj::Int(img.width as i64)),
1235 (b"Height".to_vec(), PdfObj::Int(img.height as i64)),
1236 ];
1237
1238 if img.is_imagemask {
1239 entries.push((b"ImageMask".to_vec(), PdfObj::Bool(true)));
1240 entries.push((
1242 b"Decode".to_vec(),
1243 PdfObj::Array(vec![PdfObj::Int(1), PdfObj::Int(0)]),
1244 ));
1245 } else {
1246 entries.push((b"ColorSpace".to_vec(), cs_obj));
1247 entries.push((
1248 b"BitsPerComponent".to_vec(),
1249 PdfObj::Int(img.bits_per_component as i64),
1250 ));
1251 }
1252
1253 entries.push((b"Interpolate".to_vec(), PdfObj::Bool(false)));
1254
1255 if let Some(smask) = smask_ref {
1256 entries.push((b"SMask".to_vec(), PdfObj::Ref(smask)));
1257 }
1258
1259 if let Some(ref ckm) = img.color_key_mask {
1261 let ncomp = img.pdf_color_space.num_components();
1262 let mask_ints: Vec<PdfObj> = if ckm.len() == ncomp {
1263 ckm.iter()
1265 .flat_map(|&v| [PdfObj::Int(v as i64), PdfObj::Int(v as i64)])
1266 .collect()
1267 } else {
1268 ckm.iter().map(|&v| PdfObj::Int(v as i64)).collect()
1270 };
1271 entries.push((b"Mask".to_vec(), PdfObj::Array(mask_ints)));
1272 }
1273
1274 writer.add_stream(entries, &img.sample_data, true)
1275 }
1276}
1277
1278fn build_pdf_colorspace(
1280 cs: &crate::image_ops::PdfColorSpace,
1281 icc_ref: Option<u32>,
1282 writer: &mut PdfWriter,
1283) -> PdfObj {
1284 use crate::image_ops::PdfColorSpace;
1285 match cs {
1286 PdfColorSpace::DeviceGray => PdfObj::name("DeviceGray"),
1287 PdfColorSpace::DeviceRGB => PdfObj::name("DeviceRGB"),
1288 PdfColorSpace::DeviceCMYK => PdfObj::name("DeviceCMYK"),
1289 PdfColorSpace::ICCBased { .. } => {
1290 if let Some(ref_num) = icc_ref {
1291 PdfObj::Array(vec![PdfObj::name("ICCBased"), PdfObj::Ref(ref_num)])
1292 } else {
1293 PdfObj::name("DeviceRGB") }
1295 }
1296 PdfColorSpace::Indexed {
1297 base,
1298 hival,
1299 lookup,
1300 } => {
1301 let base_obj = build_pdf_colorspace(base, icc_ref, writer);
1307 let lookup_ref = writer.add_stream(Vec::new(), lookup, true);
1309 PdfObj::Array(vec![
1310 PdfObj::name("Indexed"),
1311 base_obj,
1312 PdfObj::Int(*hival as i64),
1313 PdfObj::Ref(lookup_ref),
1314 ])
1315 }
1316 PdfColorSpace::Separation {
1317 name,
1318 alt,
1319 tint_table,
1320 } => {
1321 let alt_obj = build_pdf_colorspace(alt, None, writer);
1322 let func_ref = build_tint_function(tint_table, writer);
1323 PdfObj::Array(vec![
1324 PdfObj::name("Separation"),
1325 PdfObj::Name(name.clone()),
1326 alt_obj,
1327 PdfObj::Ref(func_ref),
1328 ])
1329 }
1330 PdfColorSpace::DeviceN {
1331 names,
1332 alt,
1333 tint_table,
1334 } => {
1335 let alt_obj = build_pdf_colorspace(alt, None, writer);
1336 let func_ref = build_tint_function(tint_table, writer);
1337 let names_arr = PdfObj::Array(names.iter().map(|n| PdfObj::Name(n.clone())).collect());
1338 PdfObj::Array(vec![
1339 PdfObj::name("DeviceN"),
1340 names_arr,
1341 alt_obj,
1342 PdfObj::Ref(func_ref),
1343 ])
1344 }
1345 }
1346}
1347
1348fn emit_output_intents(
1353 writer: &mut PdfWriter,
1354 intents: &[stet_graphics::document_structure::OutputIntentRecord],
1355) -> Vec<u32> {
1356 let mut refs = Vec::with_capacity(intents.len());
1357 for intent in intents {
1358 let profile_ref = intent.dest_output_profile.as_ref().map(|bytes| {
1359 writer.add_stream(
1360 vec![(b"N".to_vec(), PdfObj::Int(intent.n as i64))],
1361 bytes,
1362 true,
1363 )
1364 });
1365 let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![
1366 (b"Type".to_vec(), PdfObj::name("OutputIntent")),
1367 (b"S".to_vec(), PdfObj::Name(intent.subtype.clone())),
1368 ];
1369 if let Some(s) = &intent.output_condition_identifier {
1370 entries.push((
1371 b"OutputConditionIdentifier".to_vec(),
1372 PdfObj::LitString(s.clone()),
1373 ));
1374 }
1375 if let Some(s) = &intent.output_condition {
1376 entries.push((b"OutputCondition".to_vec(), PdfObj::LitString(s.clone())));
1377 }
1378 if let Some(s) = &intent.registry_name {
1379 entries.push((b"RegistryName".to_vec(), PdfObj::LitString(s.clone())));
1380 }
1381 if let Some(s) = &intent.info {
1382 entries.push((b"Info".to_vec(), PdfObj::LitString(s.clone())));
1383 }
1384 if let Some(p_ref) = profile_ref {
1385 entries.push((b"DestOutputProfile".to_vec(), PdfObj::Ref(p_ref)));
1386 }
1387 let intent_ref = writer.add_object(&PdfObj::Dict(entries));
1388 refs.push(intent_ref);
1389 }
1390 refs
1391}
1392
1393fn build_tint_function(
1396 table: &stet_graphics::device::TintLookupTable,
1397 writer: &mut PdfWriter,
1398) -> u32 {
1399 let ni = table.num_inputs as usize;
1400 let no = table.num_outputs as usize;
1401
1402 let spd = table.samples_per_dim as usize;
1407 let total_entries = spd.pow(ni as u32);
1408 let samples: Vec<u8> = if ni <= 1 {
1409 table
1410 .data
1411 .iter()
1412 .map(|&v| (v.clamp(0.0, 1.0) * 255.0) as u8)
1413 .collect()
1414 } else {
1415 let mut out = Vec::with_capacity(total_entries * no);
1417 for pdf_idx in 0..total_entries {
1418 let mut coords = vec![0usize; ni];
1420 let mut rem = pdf_idx;
1421 for coord in coords.iter_mut() {
1422 *coord = rem % spd;
1423 rem /= spd;
1424 }
1425 let mut our_idx = 0;
1427 for coord in coords.iter() {
1428 our_idx = our_idx * spd + coord;
1429 }
1430 let base = our_idx * no;
1431 for c in 0..no {
1432 out.push((table.data[base + c].clamp(0.0, 1.0) * 255.0) as u8);
1433 }
1434 }
1435 out
1436 };
1437
1438 let mut domain = Vec::with_capacity(ni * 2);
1440 for _ in 0..ni {
1441 domain.push(PdfObj::Int(0));
1442 domain.push(PdfObj::Int(1));
1443 }
1444
1445 let mut range = Vec::with_capacity(no * 2);
1447 for _ in 0..no {
1448 range.push(PdfObj::Int(0));
1449 range.push(PdfObj::Int(1));
1450 }
1451
1452 let size: Vec<PdfObj> = (0..ni)
1454 .map(|_| PdfObj::Int(table.samples_per_dim as i64))
1455 .collect();
1456
1457 let dict_entries = vec![
1458 (b"FunctionType".to_vec(), PdfObj::Int(0)),
1459 (b"Domain".to_vec(), PdfObj::Array(domain)),
1460 (b"Range".to_vec(), PdfObj::Array(range)),
1461 (b"Size".to_vec(), PdfObj::Array(size)),
1462 (b"BitsPerSample".to_vec(), PdfObj::Int(8)),
1463 ];
1464
1465 writer.add_stream(dict_entries, &samples, true)
1466}
1467
1468fn build_spot_colorspace(
1471 spot_cs: &stet_graphics::device::SpotColorSpace,
1472 writer: &mut PdfWriter,
1473) -> PdfObj {
1474 use stet_graphics::device::{SimpleColorSpace, SpotColorSpace};
1475 match spot_cs {
1476 SpotColorSpace::Separation {
1477 name,
1478 alt,
1479 tint_table,
1480 } => {
1481 let alt_obj = match alt {
1482 SimpleColorSpace::DeviceGray => PdfObj::name("DeviceGray"),
1483 SimpleColorSpace::DeviceRGB => PdfObj::name("DeviceRGB"),
1484 SimpleColorSpace::DeviceCMYK => PdfObj::name("DeviceCMYK"),
1485 };
1486 let func_ref = build_tint_function(tint_table, writer);
1487 PdfObj::Array(vec![
1488 PdfObj::name("Separation"),
1489 PdfObj::Name(name.clone()),
1490 alt_obj,
1491 PdfObj::Ref(func_ref),
1492 ])
1493 }
1494 SpotColorSpace::DeviceN {
1495 names,
1496 alt,
1497 tint_table,
1498 } => {
1499 let alt_obj = match alt {
1500 SimpleColorSpace::DeviceGray => PdfObj::name("DeviceGray"),
1501 SimpleColorSpace::DeviceRGB => PdfObj::name("DeviceRGB"),
1502 SimpleColorSpace::DeviceCMYK => PdfObj::name("DeviceCMYK"),
1503 };
1504 let func_ref = build_tint_function(tint_table, writer);
1505 let names_arr = PdfObj::Array(names.iter().map(|n| PdfObj::Name(n.clone())).collect());
1506 PdfObj::Array(vec![
1507 PdfObj::name("DeviceN"),
1508 names_arr,
1509 alt_obj,
1510 PdfObj::Ref(func_ref),
1511 ])
1512 }
1513 _ => PdfObj::name("DeviceRGB"),
1514 }
1515}
1516
1517fn generate_tounicode_cmap(map: &std::collections::HashMap<u16, u16>, font_name: &str) -> Vec<u8> {
1519 use std::io::Write;
1520 let mut buf = Vec::new();
1521
1522 writeln!(buf, "/CIDInit /ProcSet findresource begin").unwrap();
1523 writeln!(buf, "12 dict begin").unwrap();
1524 writeln!(buf, "begincmap").unwrap();
1525 writeln!(buf, "/CIDSystemInfo <<").unwrap();
1526 writeln!(buf, " /Registry (Adobe)").unwrap();
1527 writeln!(buf, " /Ordering (UCS)").unwrap();
1528 writeln!(buf, " /Supplement 0").unwrap();
1529 writeln!(buf, ">> def").unwrap();
1530 writeln!(buf, "/CMapName /{}-UCS def", font_name).unwrap();
1531 writeln!(buf, "/CMapType 2 def").unwrap();
1532 writeln!(buf, "1 begincodespacerange").unwrap();
1533 writeln!(buf, "<00> <FF>").unwrap();
1534 writeln!(buf, "endcodespacerange").unwrap();
1535
1536 let mut sorted: Vec<_> = map.iter().collect();
1537 sorted.sort_by_key(|&(&code, _)| code);
1538
1539 for chunk in sorted.chunks(100) {
1540 writeln!(buf, "{} beginbfchar", chunk.len()).unwrap();
1541 for &(&code, &unicode) in chunk {
1542 writeln!(buf, "<{:02X}> <{:04X}>", code, unicode).unwrap();
1543 }
1544 writeln!(buf, "endbfchar").unwrap();
1545 }
1546
1547 writeln!(buf, "endcmap").unwrap();
1548 writeln!(buf, "CMapName currentdict /CMap defineresource pop").unwrap();
1549 writeln!(buf, "end").unwrap();
1550 writeln!(buf, "end").unwrap();
1551
1552 buf
1553}
1554
1555impl OutputDevice for PdfDevice {
1556 fn fill_path(&mut self, _path: &PsPath, _params: &FillParams) {}
1557 fn stroke_path(&mut self, _path: &PsPath, _params: &StrokeParams) {}
1558 fn clip_path(&mut self, _path: &PsPath, _params: &ClipParams) {}
1559 fn init_clip(&mut self) {}
1560 fn erase_page(&mut self) {}
1561
1562 fn set_trim_box(&mut self, llx: f64, lly: f64, urx: f64, ury: f64) {
1563 self.pending_trim_box = Some((llx, lly, urx, ury));
1564 }
1565
1566 fn show_page(&mut self, _output_path: &str) -> Result<(), String> {
1567 Ok(())
1568 }
1569
1570 fn writes_file_per_page(&self) -> bool {
1573 false
1574 }
1575
1576 fn draw_image(&mut self, _sample_data: &[u8], _params: &ImageParams) {}
1577
1578 fn page_size(&self) -> (u32, u32) {
1579 (self.page_w, self.page_h)
1580 }
1581
1582 fn replay_and_show(&mut self, list: DisplayList, output_path: &str) -> Result<(), String> {
1583 if self.output_path.is_none() {
1585 let base = if let Some(pos) = output_path.rfind('.') {
1587 &output_path[..pos]
1588 } else {
1589 output_path
1590 };
1591 let base = if base.len() >= 5 && base.as_bytes()[base.len() - 5] == b'-' {
1593 let suffix = &base[base.len() - 4..];
1594 if suffix.bytes().all(|b| b.is_ascii_digit()) {
1595 &base[..base.len() - 5]
1596 } else {
1597 base
1598 }
1599 } else {
1600 base
1601 };
1602 self.output_path = Some(format!("{}.pdf", base));
1603 }
1604
1605 let scale = 72.0 / self.dpi;
1606
1607 self.pages.push(PageData {
1608 display_list: list,
1609 width_pts: self.page_w as f64 * scale,
1610 height_pts: self.page_h as f64 * scale,
1611 page_w: self.page_w,
1612 page_h: self.page_h,
1613 dpi: self.dpi,
1614 trim_box: self.pending_trim_box.take(),
1615 });
1616
1617 Ok(())
1618 }
1619
1620 fn finish(&mut self) -> Result<(), String> {
1621 if self.pages.is_empty() {
1622 return Ok(());
1623 }
1624 self.write_pdf(None)
1625 }
1626
1627 fn finish_with_context(&mut self, ctx: &Context) -> Result<(), String> {
1628 if self.pages.is_empty() {
1629 return Ok(());
1630 }
1631 self.write_pdf(Some(ctx))
1632 }
1633
1634 fn as_any(&self) -> &dyn std::any::Any {
1635 self
1636 }
1637}
1638
1639#[allow(dead_code)]
1647fn parse_icc_header(data: &[u8]) -> (u32, String) {
1648 let n = if data.len() >= 20 {
1649 match &data[16..20] {
1650 b"CMYK" => 4,
1651 b"RGB " => 3,
1652 b"GRAY" => 1,
1653 b"Lab " => 3,
1654 _ => 4, }
1656 } else {
1657 4
1658 };
1659 let desc = extract_icc_description(data).unwrap_or_else(|| "Custom".to_string());
1660 (n, desc)
1661}
1662
1663#[allow(dead_code)]
1667fn extract_icc_description(data: &[u8]) -> Option<String> {
1668 if data.len() < 132 {
1669 return None;
1670 }
1671 let tag_count = u32::from_be_bytes(data[128..132].try_into().ok()?) as usize;
1672 let tag_table_start = 132;
1673
1674 for i in 0..tag_count {
1675 let offset = tag_table_start + i * 12;
1676 if offset + 12 > data.len() {
1677 break;
1678 }
1679 let tag_sig = &data[offset..offset + 4];
1680 let tag_offset = u32::from_be_bytes(data[offset + 4..offset + 8].try_into().ok()?) as usize;
1681 let tag_size = u32::from_be_bytes(data[offset + 8..offset + 12].try_into().ok()?) as usize;
1682
1683 if tag_sig != b"desc" {
1684 continue;
1685 }
1686 if tag_offset + tag_size > data.len() || tag_size < 12 {
1687 return None;
1688 }
1689
1690 let type_sig = &data[tag_offset..tag_offset + 4];
1691 if type_sig == b"desc" {
1692 let count =
1694 u32::from_be_bytes(data[tag_offset + 8..tag_offset + 12].try_into().ok()?) as usize;
1695 if count == 0 {
1696 return None;
1697 }
1698 let str_end = (tag_offset + 12 + count).min(tag_offset + tag_size);
1699 let s = &data[tag_offset + 12..str_end];
1700 let s = s.split(|&b| b == 0).next().unwrap_or(s);
1702 return Some(String::from_utf8_lossy(s).to_string());
1703 } else if type_sig == b"mluc" {
1704 if tag_size < 20 {
1706 return None;
1707 }
1708 let record_count =
1709 u32::from_be_bytes(data[tag_offset + 8..tag_offset + 12].try_into().ok()?) as usize;
1710 if record_count == 0 {
1711 return None;
1712 }
1713 let rec_base = tag_offset + 16;
1715 if rec_base + 12 > data.len() {
1716 return None;
1717 }
1718 let str_len =
1719 u32::from_be_bytes(data[rec_base + 4..rec_base + 8].try_into().ok()?) as usize;
1720 let str_off =
1721 u32::from_be_bytes(data[rec_base + 8..rec_base + 12].try_into().ok()?) as usize;
1722 let abs_off = tag_offset + str_off;
1723 if abs_off + str_len > data.len() || str_len < 2 {
1724 return None;
1725 }
1726 let utf16: Vec<u16> = data[abs_off..abs_off + str_len]
1728 .chunks_exact(2)
1729 .map(|c| u16::from_be_bytes([c[0], c[1]]))
1730 .collect();
1731 return Some(
1732 String::from_utf16_lossy(&utf16)
1733 .trim_end_matches('\0')
1734 .to_string(),
1735 );
1736 }
1737
1738 break;
1739 }
1740 None
1741}
1742
1743fn build_invert_transfer(writer: &mut PdfWriter) -> u32 {
1748 let dict_entries = vec![
1749 (b"FunctionType".to_vec(), PdfObj::Int(4)),
1750 (
1751 b"Domain".to_vec(),
1752 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1753 ),
1754 (
1755 b"Range".to_vec(),
1756 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1757 ),
1758 ];
1759 writer.add_stream(dict_entries, b"{ 1 exch sub }", false)
1760}
1761
1762fn build_type0_function(writer: &mut PdfWriter, table: &[f64]) -> u32 {
1765 let dict_entries = vec![
1766 (b"FunctionType".to_vec(), PdfObj::Int(0)),
1767 (
1768 b"Domain".to_vec(),
1769 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1770 ),
1771 (
1772 b"Range".to_vec(),
1773 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1774 ),
1775 (
1776 b"Size".to_vec(),
1777 PdfObj::Array(vec![PdfObj::Int(table.len() as i64)]),
1778 ),
1779 (b"BitsPerSample".to_vec(), PdfObj::Int(8)),
1780 ];
1781 let data: Vec<u8> = table
1782 .iter()
1783 .map(|&v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
1784 .collect();
1785 writer.add_stream(dict_entries, &data, false)
1786}
1787
1788fn build_transfer_tr2(
1791 writer: &mut PdfWriter,
1792 tables: &[Option<std::sync::Arc<Vec<f64>>>],
1793 is_color: bool,
1794) -> PdfObj {
1795 if is_color && tables.len() == 4 {
1796 let refs: Vec<PdfObj> = tables
1798 .iter()
1799 .map(|t| {
1800 if let Some(table) = t {
1801 let func_ref = build_type0_function(writer, table);
1802 PdfObj::Ref(func_ref)
1803 } else {
1804 PdfObj::name("Identity")
1805 }
1806 })
1807 .collect();
1808 PdfObj::Array(refs)
1809 } else if !is_color && tables.len() == 1 {
1810 if let Some(ref table) = tables[0] {
1811 let func_ref = build_type0_function(writer, table);
1812 PdfObj::Ref(func_ref)
1813 } else {
1814 PdfObj::name("Identity")
1815 }
1816 } else {
1817 PdfObj::name("Identity")
1818 }
1819}
1820
1821fn build_type0_function_signed(writer: &mut PdfWriter, table: &[f64]) -> u32 {
1824 let dict_entries = vec![
1825 (b"FunctionType".to_vec(), PdfObj::Int(0)),
1826 (
1827 b"Domain".to_vec(),
1828 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1829 ),
1830 (
1831 b"Range".to_vec(),
1832 PdfObj::Array(vec![PdfObj::Int(-1), PdfObj::Int(1)]),
1833 ),
1834 (
1835 b"Size".to_vec(),
1836 PdfObj::Array(vec![PdfObj::Int(table.len() as i64)]),
1837 ),
1838 (b"BitsPerSample".to_vec(), PdfObj::Int(8)),
1839 ];
1840 let data: Vec<u8> = table
1842 .iter()
1843 .map(|&v| ((v.clamp(-1.0, 1.0) + 1.0) / 2.0 * 255.0).round() as u8)
1844 .collect();
1845 writer.add_stream(dict_entries, &data, false)
1846}
1847
1848fn build_type4_function(writer: &mut PdfWriter, tokens: &[u8]) -> u32 {
1851 let dict_entries = vec![
1852 (b"FunctionType".to_vec(), PdfObj::Int(4)),
1853 (
1854 b"Domain".to_vec(),
1855 PdfObj::Array(vec![
1856 PdfObj::Int(-1),
1857 PdfObj::Int(1),
1858 PdfObj::Int(-1),
1859 PdfObj::Int(1),
1860 ]),
1861 ),
1862 (
1863 b"Range".to_vec(),
1864 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1865 ),
1866 ];
1867 writer.add_stream(dict_entries, tokens, false)
1868}
1869
1870fn build_type0_function_2d(writer: &mut PdfWriter, table: &[f64]) -> u32 {
1873 let dict_entries = vec![
1874 (b"FunctionType".to_vec(), PdfObj::Int(0)),
1875 (
1876 b"Domain".to_vec(),
1877 PdfObj::Array(vec![
1878 PdfObj::Int(-1),
1879 PdfObj::Int(1),
1880 PdfObj::Int(-1),
1881 PdfObj::Int(1),
1882 ]),
1883 ),
1884 (
1885 b"Range".to_vec(),
1886 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1887 ),
1888 (
1889 b"Size".to_vec(),
1890 PdfObj::Array(vec![PdfObj::Int(64), PdfObj::Int(64)]),
1891 ),
1892 (b"BitsPerSample".to_vec(), PdfObj::Int(8)),
1893 ];
1894 let data: Vec<u8> = table
1895 .iter()
1896 .map(|&v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
1897 .collect();
1898 writer.add_stream(dict_entries, &data, false)
1899}
1900
1901fn build_halftone_screen(
1904 writer: &mut PdfWriter,
1905 screen: &stet_graphics::device::HalftoneScreen,
1906) -> PdfObj {
1907 let spot_func = if let Some(ref tokens) = screen.type4_tokens {
1908 let func_ref = build_type4_function(writer, tokens);
1909 PdfObj::Ref(func_ref)
1910 } else if let Some(ref table) = screen.sampled_2d {
1911 let func_ref = build_type0_function_2d(writer, table);
1912 PdfObj::Ref(func_ref)
1913 } else {
1914 PdfObj::name("Default")
1915 };
1916
1917 let entries = vec![
1918 (b"Type".to_vec(), PdfObj::name("Halftone")),
1919 (b"HalftoneType".to_vec(), PdfObj::Int(1)),
1920 (b"Frequency".to_vec(), PdfObj::Real(screen.frequency)),
1921 (b"Angle".to_vec(), PdfObj::Real(screen.angle)),
1922 (b"SpotFunction".to_vec(), spot_func),
1923 ];
1924 let obj_ref = writer.add_object(&PdfObj::Dict(entries));
1925 PdfObj::Ref(obj_ref)
1926}
1927
1928fn build_halftone_ht(
1930 writer: &mut PdfWriter,
1931 state: &stet_graphics::device::HalftoneState,
1932) -> PdfObj {
1933 if let Some(ref color) = state.color {
1934 let mut entries = vec![
1936 (b"Type".to_vec(), PdfObj::name("Halftone")),
1937 (b"HalftoneType".to_vec(), PdfObj::Int(5)),
1938 ];
1939 let component_names: [&[u8]; 4] = [b"Red", b"Green", b"Blue", b"Default"];
1940 for (i, screen_opt) in color.iter().enumerate() {
1941 if let Some(screen) = screen_opt {
1942 let ht_obj = build_halftone_screen(writer, screen);
1943 entries.push((component_names[i].to_vec(), ht_obj));
1944 }
1945 }
1946 let obj_ref = writer.add_object(&PdfObj::Dict(entries));
1947 PdfObj::Ref(obj_ref)
1948 } else if let Some(ref gray) = state.gray {
1949 build_halftone_screen(writer, gray)
1950 } else {
1951 PdfObj::name("Default")
1952 }
1953}
1954
1955#[derive(Default, Clone)]
1957struct EffectivePageOverride {
1958 boxes: stet_graphics::document_structure::PageBoxes,
1959 rotate: Option<i32>,
1960 additional_actions: Option<stet_graphics::document_structure::PageAdditionalActions>,
1961}
1962
1963fn compute_page_overrides(ctx: Option<&Context>, page_count: usize) -> Vec<EffectivePageOverride> {
1968 use stet_graphics::document_structure::{PageOverrideScope, StructuralRecord};
1969 let mut out = vec![EffectivePageOverride::default(); page_count];
1970 let Some(c) = ctx else {
1971 return out;
1972 };
1973 let mut all_boxes = stet_graphics::document_structure::PageBoxes::default();
1974 let mut all_rotate: Option<i32> = None;
1975 let mut all_aa: Option<stet_graphics::document_structure::PageAdditionalActions> = None;
1976 let mut per_page_boxes: Vec<stet_graphics::document_structure::PageBoxes> =
1977 vec![stet_graphics::document_structure::PageBoxes::default(); page_count];
1978 let mut per_page_rotate: Vec<Option<i32>> = vec![None; page_count];
1979 let mut per_page_aa: Vec<Option<stet_graphics::document_structure::PageAdditionalActions>> =
1980 vec![None; page_count];
1981
1982 for record in c.doc_structure.records() {
1983 let StructuralRecord::PageOverride(rec) = record else {
1984 continue;
1985 };
1986 match rec.scope {
1987 PageOverrideScope::All => {
1988 all_boxes = rec.boxes.merge_over(&all_boxes);
1989 if rec.rotate.is_some() {
1990 all_rotate = rec.rotate;
1991 }
1992 if let Some(new_aa) = &rec.additional_actions {
1993 all_aa = Some(match all_aa {
1994 Some(prev) => new_aa.merge_over(&prev),
1995 None => new_aa.clone(),
1996 });
1997 }
1998 }
1999 PageOverrideScope::Single(page) => {
2000 let idx = page as usize;
2001 if idx == 0 || idx > page_count {
2002 continue;
2003 }
2004 let i = idx - 1;
2005 per_page_boxes[i] = rec.boxes.merge_over(&per_page_boxes[i]);
2006 if rec.rotate.is_some() {
2007 per_page_rotate[i] = rec.rotate;
2008 }
2009 if let Some(new_aa) = &rec.additional_actions {
2010 per_page_aa[i] = Some(match per_page_aa[i].clone() {
2011 Some(prev) => new_aa.merge_over(&prev),
2012 None => new_aa.clone(),
2013 });
2014 }
2015 }
2016 _ => continue,
2017 }
2018 }
2019
2020 for i in 0..page_count {
2021 out[i].boxes = per_page_boxes[i].merge_over(&all_boxes);
2022 out[i].rotate = per_page_rotate[i].or(all_rotate);
2023 out[i].additional_actions = match (&per_page_aa[i], &all_aa) {
2024 (Some(p), Some(a)) => Some(p.merge_over(a)),
2025 (Some(p), None) => Some(p.clone()),
2026 (None, Some(a)) => Some(a.clone()),
2027 (None, None) => None,
2028 };
2029 }
2030 out
2031}
2032
2033fn days_to_ymd(days: u64) -> (u64, u64, u64) {
2035 let z = days + 719468;
2037 let era = z / 146097;
2038 let doe = z - era * 146097;
2039 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
2040 let y = yoe + era * 400;
2041 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2042 let mp = (5 * doy + 2) / 153;
2043 let d = doy - (153 * mp + 2) / 5 + 1;
2044 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2045 let y = if m <= 2 { y + 1 } else { y };
2046 (y, m, d)
2047}
2048
2049fn default_now_pdf_date() -> String {
2051 use std::time::SystemTime;
2052 let now = SystemTime::now()
2053 .duration_since(SystemTime::UNIX_EPOCH)
2054 .unwrap_or_default()
2055 .as_secs();
2056 let secs_per_day = 86400u64;
2057 let days = now / secs_per_day;
2058 let time_of_day = now % secs_per_day;
2059 let hours = time_of_day / 3600;
2060 let minutes = (time_of_day % 3600) / 60;
2061 let seconds = time_of_day % 60;
2062 let (year, month, day) = days_to_ymd(days);
2063 format!(
2064 "D:{:04}{:02}{:02}{:02}{:02}{:02}Z",
2065 year, month, day, hours, minutes, seconds
2066 )
2067}
2068
2069fn collect_viewer_prefs(ctx: &Context) -> stet_graphics::document_structure::ViewerPrefsRecord {
2077 use stet_graphics::document_structure::{StructuralRecord, ViewerPrefsRecord};
2078 let mut acc = ViewerPrefsRecord::default();
2079 for record in ctx.doc_structure.records() {
2080 if let StructuralRecord::ViewerPrefs(rec) = record {
2081 acc = rec.merge_over(&acc);
2082 }
2083 }
2084 acc
2085}
2086
2087fn collect_docinfo(ctx: &Context) -> stet_graphics::document_structure::DocInfoRecord {
2088 let mut acc = stet_graphics::document_structure::DocInfoRecord::default();
2089 for record in ctx.doc_structure.records() {
2090 let stet_graphics::document_structure::StructuralRecord::DocInfo(rec) = record else {
2091 continue;
2092 };
2093 if let Some(v) = &rec.title {
2094 acc.title = Some(v.clone());
2095 }
2096 if let Some(v) = &rec.author {
2097 acc.author = Some(v.clone());
2098 }
2099 if let Some(v) = &rec.subject {
2100 acc.subject = Some(v.clone());
2101 }
2102 if let Some(v) = &rec.keywords {
2103 acc.keywords = Some(v.clone());
2104 }
2105 if let Some(v) = &rec.creator {
2106 acc.creator = Some(v.clone());
2107 }
2108 if let Some(v) = &rec.producer {
2109 acc.producer = Some(v.clone());
2110 }
2111 if let Some(v) = &rec.creation_date {
2112 acc.creation_date = Some(v.clone());
2113 }
2114 if let Some(v) = &rec.mod_date {
2115 acc.mod_date = Some(v.clone());
2116 }
2117 if let Some(v) = rec.trapped {
2118 acc.trapped = Some(v);
2119 }
2120 }
2121 acc
2122}
2123
2124fn build_form_xobject(writer: &mut PdfWriter, form: &crate::content_stream::FormXObject) -> u32 {
2129 let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![
2130 (b"Type".to_vec(), PdfObj::name("XObject")),
2131 (b"Subtype".to_vec(), PdfObj::name("Form")),
2132 (b"FormType".to_vec(), PdfObj::Int(1)),
2133 (
2134 b"BBox".to_vec(),
2135 PdfObj::Array(vec![
2136 PdfObj::Real(form.bbox[0]),
2137 PdfObj::Real(form.bbox[1]),
2138 PdfObj::Real(form.bbox[2]),
2139 PdfObj::Real(form.bbox[3]),
2140 ]),
2141 ),
2142 ];
2143 if let Some(group_entries) = &form.group_dict_entries {
2144 let entries_clone: Vec<(Vec<u8>, PdfObj)> = group_entries
2145 .iter()
2146 .map(|(k, v)| (k.clone(), clone_pdfobj_shallow(v)))
2147 .collect();
2148 entries.push((b"Group".to_vec(), PdfObj::Dict(entries_clone)));
2149 }
2150 writer.add_stream(entries, &form.content, true)
2151}
2152
2153fn clone_pdfobj_shallow(v: &PdfObj) -> PdfObj {
2159 match v {
2160 PdfObj::Bool(b) => PdfObj::Bool(*b),
2161 PdfObj::Int(n) => PdfObj::Int(*n),
2162 PdfObj::Real(r) => PdfObj::Real(*r),
2163 PdfObj::Name(n) => PdfObj::Name(n.clone()),
2164 PdfObj::Ref(r) => PdfObj::Ref(*r),
2165 PdfObj::Null => PdfObj::Null,
2166 _ => panic!("clone_pdfobj_shallow: unsupported PdfObj variant in /Group dict"),
2167 }
2168}
2169
2170fn collect_ocg_ids<F>(visibility: &stet_graphics::display_list::OcgVisibility, mut visit: F)
2175where
2176 F: FnMut(u32, bool),
2177{
2178 use stet_graphics::display_list::OcgVisibility;
2179 fn walk_expr(
2180 e: &stet_graphics::display_list::VisibilityExpr,
2181 v: &mut impl FnMut(u32, bool),
2182 default_visible: bool,
2183 ) {
2184 use stet_graphics::display_list::VisibilityExpr;
2185 match e {
2186 VisibilityExpr::And(xs) | VisibilityExpr::Or(xs) => {
2187 for x in xs {
2188 walk_expr(x, v, default_visible);
2189 }
2190 }
2191 VisibilityExpr::Not(x) => walk_expr(x, v, default_visible),
2192 VisibilityExpr::Layer(id) => v(*id, default_visible),
2193 }
2194 }
2195 match visibility {
2196 OcgVisibility::Single {
2197 ocg_id,
2198 default_visible,
2199 } => visit(*ocg_id, *default_visible),
2200 OcgVisibility::Membership {
2201 ocg_ids,
2202 default_visible,
2203 ..
2204 } => {
2205 for &id in ocg_ids {
2206 visit(id, *default_visible);
2207 }
2208 }
2209 OcgVisibility::Expression {
2210 expr,
2211 default_visible,
2212 } => walk_expr(expr, &mut visit, *default_visible),
2213 }
2214}
2215
2216fn build_ocg_property_ref(
2223 writer: &mut PdfWriter,
2224 visibility: &stet_graphics::display_list::OcgVisibility,
2225 ocg_id_to_ref: &HashMap<u32, u32>,
2226) -> u32 {
2227 use stet_graphics::display_list::OcgVisibility;
2228 match visibility {
2229 OcgVisibility::Single { ocg_id, .. } => *ocg_id_to_ref.get(ocg_id).unwrap_or(&0),
2230 OcgVisibility::Membership {
2231 ocg_ids, policy, ..
2232 } => {
2233 use stet_graphics::display_list::MembershipPolicy;
2234 let policy_name: &[u8] = match policy {
2235 MembershipPolicy::AllOn => b"AllOn",
2236 MembershipPolicy::AnyOn => b"AnyOn",
2237 MembershipPolicy::AllOff => b"AllOff",
2238 MembershipPolicy::AnyOff => b"AnyOff",
2239 };
2240 let ocgs: Vec<PdfObj> = ocg_ids
2241 .iter()
2242 .filter_map(|id| ocg_id_to_ref.get(id).map(|&r| PdfObj::Ref(r)))
2243 .collect();
2244 writer.add_object(&PdfObj::Dict(vec![
2245 (b"Type".to_vec(), PdfObj::name("OCMD")),
2246 (b"OCGs".to_vec(), PdfObj::Array(ocgs)),
2247 (b"P".to_vec(), PdfObj::Name(policy_name.to_vec())),
2248 ]))
2249 }
2250 OcgVisibility::Expression { expr, .. } => {
2251 let ve = build_ve_array(writer, expr, ocg_id_to_ref);
2252 writer.add_object(&PdfObj::Dict(vec![
2253 (b"Type".to_vec(), PdfObj::name("OCMD")),
2254 (b"VE".to_vec(), ve),
2255 ]))
2256 }
2257 }
2258}
2259
2260fn build_ve_array(
2264 writer: &mut PdfWriter,
2265 expr: &stet_graphics::display_list::VisibilityExpr,
2266 ocg_id_to_ref: &HashMap<u32, u32>,
2267) -> PdfObj {
2268 use stet_graphics::display_list::VisibilityExpr;
2269 match expr {
2270 VisibilityExpr::And(xs) => {
2271 let mut arr = vec![PdfObj::name("And")];
2272 for x in xs {
2273 arr.push(build_ve_array(writer, x, ocg_id_to_ref));
2274 }
2275 PdfObj::Array(arr)
2276 }
2277 VisibilityExpr::Or(xs) => {
2278 let mut arr = vec![PdfObj::name("Or")];
2279 for x in xs {
2280 arr.push(build_ve_array(writer, x, ocg_id_to_ref));
2281 }
2282 PdfObj::Array(arr)
2283 }
2284 VisibilityExpr::Not(x) => PdfObj::Array(vec![
2285 PdfObj::name("Not"),
2286 build_ve_array(writer, x, ocg_id_to_ref),
2287 ]),
2288 VisibilityExpr::Layer(id) => match ocg_id_to_ref.get(id) {
2289 Some(&r) => PdfObj::Ref(r),
2290 None => PdfObj::Null,
2291 },
2292 }
2293}