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