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;
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}
49
50impl PdfDevice {
51 pub fn new(width: u32, height: u32, dpi: f64) -> Self {
53 Self {
54 pages: Vec::new(),
55 page_w: width,
56 page_h: height,
57 dpi,
58 output_path: None,
59 pending_trim_box: None,
60 output_profile: None,
61 }
62 }
63
64 pub fn set_trim_box(&mut self, llx: f64, lly: f64, urx: f64, ury: f64) {
66 self.pending_trim_box = Some((llx, lly, urx, ury));
67 }
68
69 #[deprecated(
78 note = "OutputIntent emission is temporarily disabled pending PDF/X-4 support; calling this has no effect"
79 )]
80 pub fn set_output_profile(&mut self, bytes: Vec<u8>) {
81 self.output_profile = Some(bytes);
82 }
83
84 pub fn take_pdf_bytes(&self) -> Option<Vec<u8>> {
89 if self.pages.is_empty() {
90 return None;
91 }
92 let (writer, catalog_ref, info_ref) = self.build_pdf(None).ok()?;
93 let mut buf = Vec::new();
94 writer
95 .write_pdf(&mut buf, catalog_ref, Some(info_ref))
96 .ok()?;
97 Some(buf)
98 }
99
100 pub fn take_pdf_bytes_with_context(&self, ctx: &Context) -> Option<Vec<u8>> {
102 if self.pages.is_empty() {
103 return None;
104 }
105 let (writer, catalog_ref, info_ref) = self.build_pdf(Some(ctx)).ok()?;
106 let mut buf = Vec::new();
107 writer
108 .write_pdf(&mut buf, catalog_ref, Some(info_ref))
109 .ok()?;
110 Some(buf)
111 }
112
113 fn write_pdf(&self, ctx: Option<&Context>) -> Result<(), String> {
115 let path = self.output_path.as_deref().ok_or("no output path set")?;
116 let (writer, catalog_ref, info_ref) = self.build_pdf(ctx)?;
117
118 let file = std::fs::File::create(path).map_err(|e| format!("create {}: {}", path, e))?;
119 let mut bw = std::io::BufWriter::new(file);
120 writer
121 .write_pdf(&mut bw, catalog_ref, Some(info_ref))
122 .map_err(|e| format!("write {}: {}", path, e))?;
123
124 eprintln!("PDF written: {} ({} pages)", path, self.pages.len());
125 Ok(())
126 }
127
128 fn build_info_dict(&self, ctx: Option<&Context>) -> Vec<(Vec<u8>, PdfObj)> {
134 let docinfo = ctx.map(|c| collect_docinfo(c)).unwrap_or_default();
135
136 let producer = docinfo
137 .producer
138 .clone()
139 .unwrap_or_else(|| "stet".to_string());
140 let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![(
141 b"Producer".to_vec(),
142 PdfObj::LitString(producer.into_bytes()),
143 )];
144
145 let title = docinfo.title.clone().or_else(|| {
147 self.output_path
148 .as_deref()
149 .and_then(|p| std::path::Path::new(p).file_stem())
150 .and_then(|s| s.to_str())
151 .map(|s| s.to_string())
152 });
153 if let Some(t) = title {
154 entries.push((b"Title".to_vec(), PdfObj::LitString(t.into_bytes())));
155 }
156
157 for (key, value) in [
158 (&b"Author"[..], &docinfo.author),
159 (&b"Subject"[..], &docinfo.subject),
160 (&b"Keywords"[..], &docinfo.keywords),
161 (&b"Creator"[..], &docinfo.creator),
162 ] {
163 if let Some(v) = value {
164 entries.push((key.to_vec(), PdfObj::LitString(v.clone().into_bytes())));
165 }
166 }
167
168 let creation_date = docinfo
170 .creation_date_string()
171 .unwrap_or_else(default_now_pdf_date);
172 entries.push((
173 b"CreationDate".to_vec(),
174 PdfObj::LitString(creation_date.into_bytes()),
175 ));
176
177 if let Some(md) = docinfo.mod_date_string() {
178 entries.push((b"ModDate".to_vec(), PdfObj::LitString(md.into_bytes())));
179 }
180
181 if let Some(t) = docinfo.trapped {
182 let name: &[u8] = match t {
183 stet_core::pdfmark::TrappedState::True => b"True",
184 stet_core::pdfmark::TrappedState::False => b"False",
185 stet_core::pdfmark::TrappedState::Unknown => b"Unknown",
186 _ => b"Unknown",
187 };
188 entries.push((b"Trapped".to_vec(), PdfObj::Name(name.to_vec())));
189 }
190
191 entries
192 }
193
194 fn build_pdf(&self, ctx: Option<&Context>) -> Result<(PdfWriter, u32, u32), String> {
196 let mut writer = PdfWriter::new();
197
198 let catalog_ref = writer.alloc_obj();
200 let pages_ref = writer.alloc_obj();
201
202 let mut font_tracker = FontTracker::new();
204
205 let mut page_results: Vec<(ContentStreamResult, &PageData)> = Vec::new();
207 for page in &self.pages {
208 let result = content_stream::build_content_stream(
209 &page.display_list,
210 page.page_w,
211 page.page_h,
212 page.dpi,
213 ctx,
214 &mut font_tracker,
215 );
216 page_results.push((result, page));
217 }
218
219 let font_obj_map: HashMap<String, u32> =
221 self.embed_all_fonts(&mut writer, &font_tracker, ctx);
222
223 let page_refs: Vec<u32> = (0..page_results.len())
227 .map(|_| writer.alloc_obj())
228 .collect();
229
230 let mut per_page_annots: Vec<Vec<u32>> = ctx
236 .map(|c| {
237 let records: Vec<stet_core::pdfmark::AnnotationRecord> = c
238 .pdfmark_buffer
239 .records()
240 .iter()
241 .filter_map(|r| match r {
242 stet_core::pdfmark::PdfMarkRecord::Annotation(rec) => Some(rec.clone()),
243 _ => None,
244 })
245 .collect();
246 if records.is_empty() {
247 return vec![Vec::new(); page_refs.len()];
248 }
249 crate::annotations::collect_per_page(&mut writer, &records, &page_refs)
250 })
251 .unwrap_or_else(|| vec![Vec::new(); page_refs.len()]);
252
253 let acroform_output = ctx.and_then(|c| {
258 let widgets: Vec<(usize, stet_core::pdfmark::AnnotationRecord)> = c
259 .pdfmark_buffer
260 .records()
261 .iter()
262 .enumerate()
263 .filter_map(|(i, r)| match r {
264 stet_core::pdfmark::PdfMarkRecord::Annotation(rec)
265 if matches!(
266 rec.subtype,
267 stet_core::pdfmark::AnnotationSubtype::Widget(_)
268 ) =>
269 {
270 Some((i, rec.clone()))
271 }
272 _ => None,
273 })
274 .collect();
275 let form_record = c
276 .pdfmark_buffer
277 .records()
278 .iter()
279 .filter_map(|r| match r {
280 stet_core::pdfmark::PdfMarkRecord::Form(rec) => Some(rec.clone()),
281 _ => None,
282 })
283 .reduce(|acc, next| next.merge_over(&acc));
284 crate::form_fields::write_form(
285 &mut writer,
286 &widgets,
287 form_record.as_ref(),
288 page_refs.len(),
289 )
290 });
291 if let Some(out) = &acroform_output {
292 for (i, refs) in out.per_page_widget_refs.iter().enumerate() {
293 per_page_annots[i].extend(refs);
294 }
295 }
296
297 let per_page_overrides = compute_page_overrides(ctx, page_refs.len());
302
303 for (i, (result, page)) in page_results.iter().enumerate() {
305 self.build_page(
306 &mut writer,
307 page,
308 pages_ref,
309 page_refs[i],
310 result,
311 &font_obj_map,
312 &mut font_tracker,
313 &per_page_annots[i],
314 &per_page_overrides[i],
315 )?;
316 }
317
318 writer.set_object(
320 pages_ref,
321 &PdfObj::Dict(vec![
322 (b"Type".to_vec(), PdfObj::name("Pages")),
323 (
324 b"Kids".to_vec(),
325 PdfObj::Array(page_refs.iter().map(|&r| PdfObj::Ref(r)).collect()),
326 ),
327 (b"Count".to_vec(), PdfObj::Int(page_refs.len() as i64)),
328 ]),
329 );
330
331 let outlines_ref = ctx.and_then(|c| {
335 let records: Vec<stet_core::pdfmark::OutlineRecord> = c
336 .pdfmark_buffer
337 .records()
338 .iter()
339 .filter_map(|r| match r {
340 stet_core::pdfmark::PdfMarkRecord::Outline(rec) => Some(rec.clone()),
341 _ => None,
342 })
343 .collect();
344 if records.is_empty() {
345 return None;
346 }
347 let tree = stet_core::pdfmark::build_outline_tree(&records);
348 crate::outline::write_outline_tree(&mut writer, &tree, &page_refs)
349 });
350
351 let dests_leaf = ctx.and_then(|c| {
356 let records: Vec<stet_core::pdfmark::DestRecord> = c
357 .pdfmark_buffer
358 .records()
359 .iter()
360 .filter_map(|r| match r {
361 stet_core::pdfmark::PdfMarkRecord::Dest(rec) => Some(rec.clone()),
362 _ => None,
363 })
364 .collect();
365 crate::names::build_dests_leaf(&mut writer, &records, &page_refs)
366 });
367 let embedded_files_leaf = ctx.and_then(|c| {
368 let records: Vec<stet_core::pdfmark::EmbedRecord> = c
369 .pdfmark_buffer
370 .records()
371 .iter()
372 .filter_map(|r| match r {
373 stet_core::pdfmark::PdfMarkRecord::Embed(rec) => Some(rec.clone()),
374 _ => None,
375 })
376 .collect();
377 crate::attachments::build_embedded_files_leaf(&mut writer, &records)
378 });
379 let names_ref =
380 crate::names::write_names_root(&mut writer, dests_leaf, embedded_files_leaf);
381
382 let merged_prefs = ctx.map(collect_viewer_prefs).unwrap_or_default();
387 let viewer_prefs_ref = crate::metadata::write_viewer_prefs(&mut writer, &merged_prefs);
388
389 let metadata_ref = ctx.and_then(|c| {
391 c.pdfmark_buffer
392 .records()
393 .iter()
394 .rev()
395 .find_map(|r| match r {
396 stet_core::pdfmark::PdfMarkRecord::Metadata(rec) => Some(rec.clone()),
397 _ => None,
398 })
399 .map(|rec| crate::metadata::write_xmp_metadata(&mut writer, &rec))
400 });
401
402 let mut catalog_entries = vec![
404 (b"Type".to_vec(), PdfObj::name("Catalog")),
405 (b"Pages".to_vec(), PdfObj::Ref(pages_ref)),
406 ];
407 if let Some(outline_ref) = outlines_ref {
408 catalog_entries.push((b"Outlines".to_vec(), PdfObj::Ref(outline_ref)));
409 }
410 if let Some(names_ref) = names_ref {
411 catalog_entries.push((b"Names".to_vec(), PdfObj::Ref(names_ref)));
412 }
413 if let Some(viewer_prefs_ref) = viewer_prefs_ref {
414 catalog_entries.push((b"ViewerPreferences".to_vec(), PdfObj::Ref(viewer_prefs_ref)));
415 }
416 if let Some(layout_bytes) = merged_prefs
418 .page_layout
419 .as_deref()
420 .and_then(crate::metadata::validated_page_layout)
421 {
422 catalog_entries.push((b"PageLayout".to_vec(), PdfObj::Name(layout_bytes.to_vec())));
423 }
424 let effective_page_mode: Option<Vec<u8>> = merged_prefs
428 .page_mode
429 .as_deref()
430 .and_then(crate::metadata::validated_page_mode)
431 .map(|v| v.to_vec())
432 .or_else(|| outlines_ref.map(|_| b"UseOutlines".to_vec()));
433 if let Some(mode) = effective_page_mode {
434 catalog_entries.push((b"PageMode".to_vec(), PdfObj::Name(mode)));
435 }
436 if let Some(metadata_ref) = metadata_ref {
437 catalog_entries.push((b"Metadata".to_vec(), PdfObj::Ref(metadata_ref)));
438 }
439 if let Some(out) = &acroform_output {
440 catalog_entries.push((b"AcroForm".to_vec(), PdfObj::Ref(out.acroform_ref)));
441 }
442
443 writer.set_object(catalog_ref, &PdfObj::Dict(catalog_entries));
444
445 let info_ref = writer.alloc_obj();
448 let info_entries = self.build_info_dict(ctx);
449 writer.set_object(info_ref, &PdfObj::Dict(info_entries));
450
451 Ok((writer, catalog_ref, info_ref))
452 }
453
454 fn embed_all_fonts(
457 &self,
458 writer: &mut PdfWriter,
459 font_tracker: &FontTracker,
460 ctx: Option<&Context>,
461 ) -> HashMap<String, u32> {
462 let mut map = HashMap::new();
463 for usage in font_tracker.fonts() {
464 let font_ref = if let Some(c) = ctx {
465 font_embedder::build_font_resource(writer, usage, c).unwrap_or_else(|| {
466 let tu = font_embedder::build_tounicode_for_fallback(writer, usage, c);
467 self.build_font_reference(writer, usage, tu)
468 })
469 } else {
470 self.build_font_reference(writer, usage, None)
471 };
472 map.insert(usage.pdf_name.clone(), font_ref);
473 }
474 map
475 }
476
477 #[allow(clippy::too_many_arguments)]
483 fn build_page(
484 &self,
485 writer: &mut PdfWriter,
486 page: &PageData,
487 pages_ref: u32,
488 page_ref: u32,
489 result: &ContentStreamResult,
490 font_obj_map: &HashMap<String, u32>,
491 font_tracker: &mut FontTracker,
492 annot_refs: &[u32],
493 overrides: &EffectivePageOverride,
494 ) -> Result<(), String> {
495 let ContentStreamResult {
496 content,
497 images,
498 shading_refs,
499 used_font_names,
500 ext_gstate_dicts,
501 color_spaces,
502 pattern_refs,
503 pattern_cs_entries,
504 transfer_refs,
505 halftone_refs,
506 bg_ucr_refs,
507 } = result;
508
509 let mut xobject_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
511 for (i, img) in images.iter().enumerate() {
512 let img_ref = self.build_image_xobject(writer, img);
513 xobject_entries.push((format!("Im{}", i).into_bytes(), PdfObj::Ref(img_ref)));
514 }
515
516 let mut shading_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
518 for (i, sh_ref) in shading_refs.iter().enumerate() {
519 let sh_obj = match sh_ref {
520 ShadingRef::Axial(p) => shading_ops::build_axial_shading(writer, p),
521 ShadingRef::Radial(p) => shading_ops::build_radial_shading(writer, p),
522 ShadingRef::Mesh(p) => shading_ops::build_mesh_shading(writer, p),
523 ShadingRef::Patch(p) => shading_ops::build_patch_shading(writer, p),
524 };
525 shading_entries.push((format!("Sh{}", i).into_bytes(), PdfObj::Ref(sh_obj)));
526 }
527
528 let mut font_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
530 for name in used_font_names {
531 if let Some(&obj_ref) = font_obj_map.get(name) {
532 font_entries.push((name.clone().into_bytes(), PdfObj::Ref(obj_ref)));
533 }
534 }
535
536 let mut resources: Vec<(Vec<u8>, PdfObj)> = Vec::new();
538 if !font_entries.is_empty() {
539 resources.push((b"Font".to_vec(), PdfObj::Dict(font_entries)));
540 }
541 if !xobject_entries.is_empty() {
542 resources.push((b"XObject".to_vec(), PdfObj::Dict(xobject_entries)));
543 }
544 if !shading_entries.is_empty() {
545 resources.push((b"Shading".to_vec(), PdfObj::Dict(shading_entries)));
546 }
547
548 if !ext_gstate_dicts.is_empty() {
550 let mut gs_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
551 for (i, gs_dict) in ext_gstate_dicts.iter().enumerate() {
552 let mut entries: Vec<(Vec<u8>, PdfObj)> = gs_dict
554 .entries
555 .iter()
556 .map(|(k, v)| {
557 let obj = match v {
558 PdfObj::Bool(b) => PdfObj::Bool(*b),
559 PdfObj::Int(n) => PdfObj::Int(*n),
560 PdfObj::Name(n) => PdfObj::Name(n.clone()),
561 _ => PdfObj::Null,
562 };
563 (k.clone(), obj)
564 })
565 .collect();
566
567 if let Some(tr) = transfer_refs.iter().find(|r| r.ext_gstate_idx == i) {
569 let tr2_value = build_transfer_tr2(writer, &tr.tables, tr.is_color);
570 entries.push((b"TR2".to_vec(), tr2_value));
571 }
572
573 if let Some(hr) = halftone_refs.iter().find(|r| r.ext_gstate_idx == i) {
575 let ht_value = build_halftone_ht(writer, &hr.state);
576 entries.push((b"HT".to_vec(), ht_value));
577 }
578
579 if let Some(br) = bg_ucr_refs.iter().find(|r| r.ext_gstate_idx == i) {
581 if let Some(ref bg) = br.state.bg {
582 let func_ref = build_type0_function(writer, bg);
583 entries.push((b"BG2".to_vec(), PdfObj::Ref(func_ref)));
584 }
585 if let Some(ref ucr) = br.state.ucr {
586 let func_ref = build_type0_function_signed(writer, ucr);
587 entries.push((b"UCR2".to_vec(), PdfObj::Ref(func_ref)));
588 }
589 }
590
591 let gs_ref = writer.add_object(&PdfObj::Dict(entries));
592 gs_entries.push((format!("GS{}", i).into_bytes(), PdfObj::Ref(gs_ref)));
593 }
594 resources.push((b"ExtGState".to_vec(), PdfObj::Dict(gs_entries)));
595 }
596
597 let mut cs_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
599 for (name, spot_cs) in color_spaces {
600 let cs_obj = build_spot_colorspace(spot_cs, writer);
601 cs_entries.push((name.clone().into_bytes(), cs_obj));
602 }
603 for (name, cs_obj) in pattern_cs_entries {
605 let obj = match cs_obj {
607 PdfObj::Array(items) => {
608 let cloned: Vec<PdfObj> = items
609 .iter()
610 .map(|item| match item {
611 PdfObj::Name(n) => PdfObj::Name(n.clone()),
612 PdfObj::Int(n) => PdfObj::Int(*n),
613 PdfObj::Real(n) => PdfObj::Real(*n),
614 PdfObj::Ref(r) => PdfObj::Ref(*r),
615 _ => PdfObj::Null,
616 })
617 .collect();
618 PdfObj::Array(cloned)
619 }
620 _ => PdfObj::Null,
621 };
622 cs_entries.push((name.clone().into_bytes(), obj));
623 }
624 if !cs_entries.is_empty() {
625 resources.push((b"ColorSpace".to_vec(), PdfObj::Dict(cs_entries)));
626 }
627
628 if !pattern_refs.is_empty() {
630 let mut pattern_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
631 for (i, pat_ref) in pattern_refs.iter().enumerate() {
632 let tile_result =
633 content_stream::build_tile_content_stream(&pat_ref.tile, font_tracker);
634
635 let mut tile_resources: Vec<(Vec<u8>, PdfObj)> = Vec::new();
637
638 if !tile_result.images.is_empty() {
640 let mut tile_xobj: Vec<(Vec<u8>, PdfObj)> = Vec::new();
641 for (j, img) in tile_result.images.iter().enumerate() {
642 let img_ref = self.build_image_xobject(writer, img);
643 tile_xobj.push((format!("Im{}", j).into_bytes(), PdfObj::Ref(img_ref)));
644 }
645 tile_resources.push((b"XObject".to_vec(), PdfObj::Dict(tile_xobj)));
646 }
647
648 if !tile_result.shading_refs.is_empty() {
650 let mut tile_sh: Vec<(Vec<u8>, PdfObj)> = Vec::new();
651 for (j, sh_ref) in tile_result.shading_refs.iter().enumerate() {
652 let sh_obj = match sh_ref {
653 ShadingRef::Axial(p) => shading_ops::build_axial_shading(writer, p),
654 ShadingRef::Radial(p) => shading_ops::build_radial_shading(writer, p),
655 ShadingRef::Mesh(p) => shading_ops::build_mesh_shading(writer, p),
656 ShadingRef::Patch(p) => shading_ops::build_patch_shading(writer, p),
657 };
658 tile_sh.push((format!("Sh{}", j).into_bytes(), PdfObj::Ref(sh_obj)));
659 }
660 tile_resources.push((b"Shading".to_vec(), PdfObj::Dict(tile_sh)));
661 }
662
663 if !tile_result.used_font_names.is_empty() {
665 let mut tile_fonts: Vec<(Vec<u8>, PdfObj)> = Vec::new();
666 for name in &tile_result.used_font_names {
667 if let Some(&obj_ref) = font_obj_map.get(name) {
668 tile_fonts.push((name.clone().into_bytes(), PdfObj::Ref(obj_ref)));
669 }
670 }
671 if !tile_fonts.is_empty() {
672 tile_resources.push((b"Font".to_vec(), PdfObj::Dict(tile_fonts)));
673 }
674 }
675
676 if !tile_result.ext_gstate_dicts.is_empty() {
678 let mut tile_gs: Vec<(Vec<u8>, PdfObj)> = Vec::new();
679 for (j, gs_dict) in tile_result.ext_gstate_dicts.iter().enumerate() {
680 let mut entries: Vec<(Vec<u8>, PdfObj)> = gs_dict
681 .entries
682 .iter()
683 .map(|(k, v)| {
684 let obj = match v {
685 PdfObj::Bool(b) => PdfObj::Bool(*b),
686 PdfObj::Int(n) => PdfObj::Int(*n),
687 PdfObj::Name(n) => PdfObj::Name(n.clone()),
688 _ => PdfObj::Null,
689 };
690 (k.clone(), obj)
691 })
692 .collect();
693 if let Some(tr) = tile_result
694 .transfer_refs
695 .iter()
696 .find(|r| r.ext_gstate_idx == j)
697 {
698 let tr2_value = build_transfer_tr2(writer, &tr.tables, tr.is_color);
699 entries.push((b"TR2".to_vec(), tr2_value));
700 }
701 if let Some(hr) = tile_result
702 .halftone_refs
703 .iter()
704 .find(|r| r.ext_gstate_idx == j)
705 {
706 let ht_value = build_halftone_ht(writer, &hr.state);
707 entries.push((b"HT".to_vec(), ht_value));
708 }
709 if let Some(br) = tile_result
710 .bg_ucr_refs
711 .iter()
712 .find(|r| r.ext_gstate_idx == j)
713 {
714 if let Some(ref bg) = br.state.bg {
715 let func_ref = build_type0_function(writer, bg);
716 entries.push((b"BG2".to_vec(), PdfObj::Ref(func_ref)));
717 }
718 if let Some(ref ucr) = br.state.ucr {
719 let func_ref = build_type0_function_signed(writer, ucr);
720 entries.push((b"UCR2".to_vec(), PdfObj::Ref(func_ref)));
721 }
722 }
723 let gs_ref = writer.add_object(&PdfObj::Dict(entries));
724 tile_gs.push((format!("GS{}", j).into_bytes(), PdfObj::Ref(gs_ref)));
725 }
726 tile_resources.push((b"ExtGState".to_vec(), PdfObj::Dict(tile_gs)));
727 }
728
729 if !tile_result.color_spaces.is_empty() {
731 let mut tile_cs: Vec<(Vec<u8>, PdfObj)> = Vec::new();
732 for (name, spot_cs) in &tile_result.color_spaces {
733 let cs_obj = build_spot_colorspace(spot_cs, writer);
734 tile_cs.push((name.clone().into_bytes(), cs_obj));
735 }
736 tile_resources.push((b"ColorSpace".to_vec(), PdfObj::Dict(tile_cs)));
737 }
738
739 let m = &pat_ref.pattern_matrix;
741 let pat_dict = vec![
742 (b"Type".to_vec(), PdfObj::name("Pattern")),
743 (b"PatternType".to_vec(), PdfObj::Int(1)),
744 (
745 b"PaintType".to_vec(),
746 PdfObj::Int(pat_ref.paint_type as i64),
747 ),
748 (b"TilingType".to_vec(), PdfObj::Int(1)),
749 (
750 b"BBox".to_vec(),
751 PdfObj::Array(vec![
754 PdfObj::Real(pat_ref.bbox[0] - 0.5),
755 PdfObj::Real(pat_ref.bbox[1] - 0.5),
756 PdfObj::Real(pat_ref.bbox[2] + 0.5),
757 PdfObj::Real(pat_ref.bbox[3] + 0.5),
758 ]),
759 ),
760 (b"XStep".to_vec(), PdfObj::Real(pat_ref.xstep)),
761 (b"YStep".to_vec(), PdfObj::Real(pat_ref.ystep)),
762 (
763 b"Matrix".to_vec(),
764 PdfObj::Array(vec![
765 PdfObj::Real(m.a),
766 PdfObj::Real(m.b),
767 PdfObj::Real(m.c),
768 PdfObj::Real(m.d),
769 PdfObj::Real(m.tx),
770 PdfObj::Real(m.ty),
771 ]),
772 ),
773 (b"Resources".to_vec(), PdfObj::Dict(tile_resources)),
774 ];
775
776 let pat_obj = writer.add_stream(pat_dict, &tile_result.content, true);
777 pattern_entries.push((format!("P{}", i).into_bytes(), PdfObj::Ref(pat_obj)));
778 }
779
780 resources.push((b"Pattern".to_vec(), PdfObj::Dict(pattern_entries)));
781 }
782
783 let content_ref = writer.add_stream(Vec::new(), content, true);
785
786 let mut page_entries = vec![
788 (b"Type".to_vec(), PdfObj::name("Page")),
789 (b"Parent".to_vec(), PdfObj::Ref(pages_ref)),
790 (
791 b"MediaBox".to_vec(),
792 PdfObj::Array(vec![
793 PdfObj::Int(0),
794 PdfObj::Int(0),
795 PdfObj::Real(page.width_pts),
796 PdfObj::Real(page.height_pts),
797 ]),
798 ),
799 (b"Contents".to_vec(), PdfObj::Ref(content_ref)),
800 (b"Resources".to_vec(), PdfObj::Dict(resources)),
801 ];
802 let effective_trim = overrides.boxes.trim_box.or_else(|| {
806 page.trim_box
807 .map(|(llx, lly, urx, ury)| [llx, lly, urx, ury])
808 });
809 for (name, b) in [
810 (b"CropBox".as_slice(), overrides.boxes.crop_box),
811 (b"BleedBox".as_slice(), overrides.boxes.bleed_box),
812 (b"TrimBox".as_slice(), effective_trim),
813 (b"ArtBox".as_slice(), overrides.boxes.art_box),
814 ] {
815 if let Some([llx, lly, urx, ury]) = b {
816 page_entries.push((
817 name.to_vec(),
818 PdfObj::Array(vec![
819 PdfObj::Real(llx),
820 PdfObj::Real(lly),
821 PdfObj::Real(urx),
822 PdfObj::Real(ury),
823 ]),
824 ));
825 }
826 }
827 if let Some(rotate) = overrides.rotate
828 && matches!(rotate, 0 | 90 | 180 | 270 | -90 | -180 | -270)
829 {
830 page_entries.push((b"Rotate".to_vec(), PdfObj::Int(rotate as i64)));
831 }
832 if !annot_refs.is_empty() {
833 page_entries.push((
834 b"Annots".to_vec(),
835 PdfObj::Array(annot_refs.iter().map(|r| PdfObj::Ref(*r)).collect()),
836 ));
837 }
838 if let Some(aa) = &overrides.additional_actions
839 && !aa.is_empty()
840 {
841 let mut aa_entries: Vec<(Vec<u8>, PdfObj)> = Vec::new();
846 if let Some(action) = &aa.on_open
847 && let Some(dict) = crate::outline::encode_action(action, &[])
848 {
849 aa_entries.push((b"O".to_vec(), dict));
850 }
851 if let Some(action) = &aa.on_close
852 && let Some(dict) = crate::outline::encode_action(action, &[])
853 {
854 aa_entries.push((b"C".to_vec(), dict));
855 }
856 if !aa_entries.is_empty() {
857 page_entries.push((b"AA".to_vec(), PdfObj::Dict(aa_entries)));
858 }
859 }
860 writer.set_object(page_ref, &PdfObj::Dict(page_entries));
861
862 Ok(())
863 }
864
865 fn build_font_reference(
871 &self,
872 writer: &mut PdfWriter,
873 usage: &crate::font_tracker::FontUsage,
874 tounicode_override: Option<u32>,
875 ) -> u32 {
876 let tounicode_ref = tounicode_override.or_else(|| self.build_tounicode_cmap(writer, usage));
878
879 let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![
880 (b"Type".to_vec(), PdfObj::name("Font")),
881 (b"Subtype".to_vec(), PdfObj::name("Type1")),
882 (b"BaseFont".to_vec(), PdfObj::Name(usage.font_name.clone())),
883 ];
884
885 if !usage.is_standard_14 {
886 entries.push((b"Encoding".to_vec(), PdfObj::name("WinAnsiEncoding")));
888 }
889
890 if let Some(tu_ref) = tounicode_ref {
891 entries.push((b"ToUnicode".to_vec(), PdfObj::Ref(tu_ref)));
892 }
893
894 writer.add_object(&PdfObj::Dict(entries))
895 }
896
897 fn build_tounicode_cmap(
904 &self,
905 writer: &mut PdfWriter,
906 usage: &crate::font_tracker::FontUsage,
907 ) -> Option<u32> {
908 use std::collections::HashMap;
909
910 let mut map: HashMap<u16, u16> = HashMap::new();
911
912 for &code in &usage.used_codes {
913 if code <= 255 {
914 if (0x20..=0x7E).contains(&code) {
916 map.insert(code, code);
917 }
918 }
919 }
920
921 if map.is_empty() {
922 return None;
923 }
924
925 let font_name = String::from_utf8_lossy(&usage.font_name);
926 let cmap_data = generate_tounicode_cmap(&map, &font_name);
927 Some(writer.add_stream(Vec::new(), &cmap_data, true))
928 }
929
930 fn build_image_xobject(&self, writer: &mut PdfWriter, img: &ImageXObject) -> u32 {
932 let smask_ref = img.smask_data.as_ref().map(|smask_data| {
934 writer.add_stream(
935 vec![
936 (b"Type".to_vec(), PdfObj::name("XObject")),
937 (b"Subtype".to_vec(), PdfObj::name("Image")),
938 (b"Width".to_vec(), PdfObj::Int(img.width as i64)),
939 (b"Height".to_vec(), PdfObj::Int(img.height as i64)),
940 (b"ColorSpace".to_vec(), PdfObj::name("DeviceGray")),
941 (b"BitsPerComponent".to_vec(), PdfObj::Int(8)),
942 (b"Interpolate".to_vec(), PdfObj::Bool(false)),
943 ],
944 smask_data,
945 true,
946 )
947 });
948
949 let icc_ref = img.icc_profile.as_ref().map(|icc| {
951 writer.add_stream(
952 vec![(b"N".to_vec(), PdfObj::Int(icc.n as i64))],
953 &icc.data,
954 true,
955 )
956 });
957
958 let cs_obj = build_pdf_colorspace(&img.pdf_color_space, icc_ref, writer);
960
961 let mut entries = vec![
963 (b"Type".to_vec(), PdfObj::name("XObject")),
964 (b"Subtype".to_vec(), PdfObj::name("Image")),
965 (b"Width".to_vec(), PdfObj::Int(img.width as i64)),
966 (b"Height".to_vec(), PdfObj::Int(img.height as i64)),
967 ];
968
969 if img.is_imagemask {
970 entries.push((b"ImageMask".to_vec(), PdfObj::Bool(true)));
971 entries.push((
973 b"Decode".to_vec(),
974 PdfObj::Array(vec![PdfObj::Int(1), PdfObj::Int(0)]),
975 ));
976 } else {
977 entries.push((b"ColorSpace".to_vec(), cs_obj));
978 entries.push((
979 b"BitsPerComponent".to_vec(),
980 PdfObj::Int(img.bits_per_component as i64),
981 ));
982 }
983
984 entries.push((b"Interpolate".to_vec(), PdfObj::Bool(false)));
985
986 if let Some(smask) = smask_ref {
987 entries.push((b"SMask".to_vec(), PdfObj::Ref(smask)));
988 }
989
990 if let Some(ref ckm) = img.color_key_mask {
992 let ncomp = img.pdf_color_space.num_components();
993 let mask_ints: Vec<PdfObj> = if ckm.len() == ncomp {
994 ckm.iter()
996 .flat_map(|&v| [PdfObj::Int(v as i64), PdfObj::Int(v as i64)])
997 .collect()
998 } else {
999 ckm.iter().map(|&v| PdfObj::Int(v as i64)).collect()
1001 };
1002 entries.push((b"Mask".to_vec(), PdfObj::Array(mask_ints)));
1003 }
1004
1005 writer.add_stream(entries, &img.sample_data, true)
1006 }
1007}
1008
1009fn build_pdf_colorspace(
1011 cs: &crate::image_ops::PdfColorSpace,
1012 icc_ref: Option<u32>,
1013 writer: &mut PdfWriter,
1014) -> PdfObj {
1015 use crate::image_ops::PdfColorSpace;
1016 match cs {
1017 PdfColorSpace::DeviceGray => PdfObj::name("DeviceGray"),
1018 PdfColorSpace::DeviceRGB => PdfObj::name("DeviceRGB"),
1019 PdfColorSpace::DeviceCMYK => PdfObj::name("DeviceCMYK"),
1020 PdfColorSpace::ICCBased { .. } => {
1021 if let Some(ref_num) = icc_ref {
1022 PdfObj::Array(vec![PdfObj::name("ICCBased"), PdfObj::Ref(ref_num)])
1023 } else {
1024 PdfObj::name("DeviceRGB") }
1026 }
1027 PdfColorSpace::Indexed {
1028 base,
1029 hival,
1030 lookup,
1031 } => {
1032 let base_obj = build_pdf_colorspace(base, None, writer);
1033 let lookup_ref = writer.add_stream(Vec::new(), lookup, true);
1035 PdfObj::Array(vec![
1036 PdfObj::name("Indexed"),
1037 base_obj,
1038 PdfObj::Int(*hival as i64),
1039 PdfObj::Ref(lookup_ref),
1040 ])
1041 }
1042 PdfColorSpace::Separation {
1043 name,
1044 alt,
1045 tint_table,
1046 } => {
1047 let alt_obj = build_pdf_colorspace(alt, None, writer);
1048 let func_ref = build_tint_function(tint_table, writer);
1049 PdfObj::Array(vec![
1050 PdfObj::name("Separation"),
1051 PdfObj::Name(name.clone()),
1052 alt_obj,
1053 PdfObj::Ref(func_ref),
1054 ])
1055 }
1056 PdfColorSpace::DeviceN {
1057 names,
1058 alt,
1059 tint_table,
1060 } => {
1061 let alt_obj = build_pdf_colorspace(alt, None, writer);
1062 let func_ref = build_tint_function(tint_table, writer);
1063 let names_arr = PdfObj::Array(names.iter().map(|n| PdfObj::Name(n.clone())).collect());
1064 PdfObj::Array(vec![
1065 PdfObj::name("DeviceN"),
1066 names_arr,
1067 alt_obj,
1068 PdfObj::Ref(func_ref),
1069 ])
1070 }
1071 }
1072}
1073
1074fn build_tint_function(
1077 table: &stet_graphics::device::TintLookupTable,
1078 writer: &mut PdfWriter,
1079) -> u32 {
1080 let ni = table.num_inputs as usize;
1081 let no = table.num_outputs as usize;
1082
1083 let spd = table.samples_per_dim as usize;
1088 let total_entries = spd.pow(ni as u32);
1089 let samples: Vec<u8> = if ni <= 1 {
1090 table
1091 .data
1092 .iter()
1093 .map(|&v| (v.clamp(0.0, 1.0) * 255.0) as u8)
1094 .collect()
1095 } else {
1096 let mut out = Vec::with_capacity(total_entries * no);
1098 for pdf_idx in 0..total_entries {
1099 let mut coords = vec![0usize; ni];
1101 let mut rem = pdf_idx;
1102 for coord in coords.iter_mut() {
1103 *coord = rem % spd;
1104 rem /= spd;
1105 }
1106 let mut our_idx = 0;
1108 for coord in coords.iter() {
1109 our_idx = our_idx * spd + coord;
1110 }
1111 let base = our_idx * no;
1112 for c in 0..no {
1113 out.push((table.data[base + c].clamp(0.0, 1.0) * 255.0) as u8);
1114 }
1115 }
1116 out
1117 };
1118
1119 let mut domain = Vec::with_capacity(ni * 2);
1121 for _ in 0..ni {
1122 domain.push(PdfObj::Int(0));
1123 domain.push(PdfObj::Int(1));
1124 }
1125
1126 let mut range = Vec::with_capacity(no * 2);
1128 for _ in 0..no {
1129 range.push(PdfObj::Int(0));
1130 range.push(PdfObj::Int(1));
1131 }
1132
1133 let size: Vec<PdfObj> = (0..ni)
1135 .map(|_| PdfObj::Int(table.samples_per_dim as i64))
1136 .collect();
1137
1138 let dict_entries = vec![
1139 (b"FunctionType".to_vec(), PdfObj::Int(0)),
1140 (b"Domain".to_vec(), PdfObj::Array(domain)),
1141 (b"Range".to_vec(), PdfObj::Array(range)),
1142 (b"Size".to_vec(), PdfObj::Array(size)),
1143 (b"BitsPerSample".to_vec(), PdfObj::Int(8)),
1144 ];
1145
1146 writer.add_stream(dict_entries, &samples, true)
1147}
1148
1149fn build_spot_colorspace(
1152 spot_cs: &stet_graphics::device::SpotColorSpace,
1153 writer: &mut PdfWriter,
1154) -> PdfObj {
1155 use stet_graphics::device::{SimpleColorSpace, SpotColorSpace};
1156 match spot_cs {
1157 SpotColorSpace::Separation {
1158 name,
1159 alt,
1160 tint_table,
1161 } => {
1162 let alt_obj = match alt {
1163 SimpleColorSpace::DeviceGray => PdfObj::name("DeviceGray"),
1164 SimpleColorSpace::DeviceRGB => PdfObj::name("DeviceRGB"),
1165 SimpleColorSpace::DeviceCMYK => PdfObj::name("DeviceCMYK"),
1166 };
1167 let func_ref = build_tint_function(tint_table, writer);
1168 PdfObj::Array(vec![
1169 PdfObj::name("Separation"),
1170 PdfObj::Name(name.clone()),
1171 alt_obj,
1172 PdfObj::Ref(func_ref),
1173 ])
1174 }
1175 SpotColorSpace::DeviceN {
1176 names,
1177 alt,
1178 tint_table,
1179 } => {
1180 let alt_obj = match alt {
1181 SimpleColorSpace::DeviceGray => PdfObj::name("DeviceGray"),
1182 SimpleColorSpace::DeviceRGB => PdfObj::name("DeviceRGB"),
1183 SimpleColorSpace::DeviceCMYK => PdfObj::name("DeviceCMYK"),
1184 };
1185 let func_ref = build_tint_function(tint_table, writer);
1186 let names_arr = PdfObj::Array(names.iter().map(|n| PdfObj::Name(n.clone())).collect());
1187 PdfObj::Array(vec![
1188 PdfObj::name("DeviceN"),
1189 names_arr,
1190 alt_obj,
1191 PdfObj::Ref(func_ref),
1192 ])
1193 }
1194 _ => PdfObj::name("DeviceRGB"),
1195 }
1196}
1197
1198fn generate_tounicode_cmap(map: &std::collections::HashMap<u16, u16>, font_name: &str) -> Vec<u8> {
1200 use std::io::Write;
1201 let mut buf = Vec::new();
1202
1203 writeln!(buf, "/CIDInit /ProcSet findresource begin").unwrap();
1204 writeln!(buf, "12 dict begin").unwrap();
1205 writeln!(buf, "begincmap").unwrap();
1206 writeln!(buf, "/CIDSystemInfo <<").unwrap();
1207 writeln!(buf, " /Registry (Adobe)").unwrap();
1208 writeln!(buf, " /Ordering (UCS)").unwrap();
1209 writeln!(buf, " /Supplement 0").unwrap();
1210 writeln!(buf, ">> def").unwrap();
1211 writeln!(buf, "/CMapName /{}-UCS def", font_name).unwrap();
1212 writeln!(buf, "/CMapType 2 def").unwrap();
1213 writeln!(buf, "1 begincodespacerange").unwrap();
1214 writeln!(buf, "<00> <FF>").unwrap();
1215 writeln!(buf, "endcodespacerange").unwrap();
1216
1217 let mut sorted: Vec<_> = map.iter().collect();
1218 sorted.sort_by_key(|&(&code, _)| code);
1219
1220 for chunk in sorted.chunks(100) {
1221 writeln!(buf, "{} beginbfchar", chunk.len()).unwrap();
1222 for &(&code, &unicode) in chunk {
1223 writeln!(buf, "<{:02X}> <{:04X}>", code, unicode).unwrap();
1224 }
1225 writeln!(buf, "endbfchar").unwrap();
1226 }
1227
1228 writeln!(buf, "endcmap").unwrap();
1229 writeln!(buf, "CMapName currentdict /CMap defineresource pop").unwrap();
1230 writeln!(buf, "end").unwrap();
1231 writeln!(buf, "end").unwrap();
1232
1233 buf
1234}
1235
1236impl OutputDevice for PdfDevice {
1237 fn fill_path(&mut self, _path: &PsPath, _params: &FillParams) {}
1238 fn stroke_path(&mut self, _path: &PsPath, _params: &StrokeParams) {}
1239 fn clip_path(&mut self, _path: &PsPath, _params: &ClipParams) {}
1240 fn init_clip(&mut self) {}
1241 fn erase_page(&mut self) {}
1242
1243 fn set_trim_box(&mut self, llx: f64, lly: f64, urx: f64, ury: f64) {
1244 self.pending_trim_box = Some((llx, lly, urx, ury));
1245 }
1246
1247 fn show_page(&mut self, _output_path: &str) -> Result<(), String> {
1248 Ok(())
1249 }
1250
1251 fn draw_image(&mut self, _sample_data: &[u8], _params: &ImageParams) {}
1252
1253 fn page_size(&self) -> (u32, u32) {
1254 (self.page_w, self.page_h)
1255 }
1256
1257 fn replay_and_show(&mut self, list: DisplayList, output_path: &str) -> Result<(), String> {
1258 if self.output_path.is_none() {
1260 let base = if let Some(pos) = output_path.rfind('.') {
1262 &output_path[..pos]
1263 } else {
1264 output_path
1265 };
1266 let base = if base.len() >= 5 && base.as_bytes()[base.len() - 5] == b'-' {
1268 let suffix = &base[base.len() - 4..];
1269 if suffix.bytes().all(|b| b.is_ascii_digit()) {
1270 &base[..base.len() - 5]
1271 } else {
1272 base
1273 }
1274 } else {
1275 base
1276 };
1277 self.output_path = Some(format!("{}.pdf", base));
1278 }
1279
1280 let scale = 72.0 / self.dpi;
1281
1282 self.pages.push(PageData {
1283 display_list: list,
1284 width_pts: self.page_w as f64 * scale,
1285 height_pts: self.page_h as f64 * scale,
1286 page_w: self.page_w,
1287 page_h: self.page_h,
1288 dpi: self.dpi,
1289 trim_box: self.pending_trim_box.take(),
1290 });
1291
1292 Ok(())
1293 }
1294
1295 fn finish(&mut self) -> Result<(), String> {
1296 if self.pages.is_empty() {
1297 return Ok(());
1298 }
1299 self.write_pdf(None)
1300 }
1301
1302 fn finish_with_context(&mut self, ctx: &Context) -> Result<(), String> {
1303 if self.pages.is_empty() {
1304 return Ok(());
1305 }
1306 self.write_pdf(Some(ctx))
1307 }
1308
1309 fn as_any(&self) -> &dyn std::any::Any {
1310 self
1311 }
1312}
1313
1314#[allow(dead_code)]
1322fn parse_icc_header(data: &[u8]) -> (u32, String) {
1323 let n = if data.len() >= 20 {
1324 match &data[16..20] {
1325 b"CMYK" => 4,
1326 b"RGB " => 3,
1327 b"GRAY" => 1,
1328 b"Lab " => 3,
1329 _ => 4, }
1331 } else {
1332 4
1333 };
1334 let desc = extract_icc_description(data).unwrap_or_else(|| "Custom".to_string());
1335 (n, desc)
1336}
1337
1338#[allow(dead_code)]
1342fn extract_icc_description(data: &[u8]) -> Option<String> {
1343 if data.len() < 132 {
1344 return None;
1345 }
1346 let tag_count = u32::from_be_bytes(data[128..132].try_into().ok()?) as usize;
1347 let tag_table_start = 132;
1348
1349 for i in 0..tag_count {
1350 let offset = tag_table_start + i * 12;
1351 if offset + 12 > data.len() {
1352 break;
1353 }
1354 let tag_sig = &data[offset..offset + 4];
1355 let tag_offset = u32::from_be_bytes(data[offset + 4..offset + 8].try_into().ok()?) as usize;
1356 let tag_size = u32::from_be_bytes(data[offset + 8..offset + 12].try_into().ok()?) as usize;
1357
1358 if tag_sig != b"desc" {
1359 continue;
1360 }
1361 if tag_offset + tag_size > data.len() || tag_size < 12 {
1362 return None;
1363 }
1364
1365 let type_sig = &data[tag_offset..tag_offset + 4];
1366 if type_sig == b"desc" {
1367 let count =
1369 u32::from_be_bytes(data[tag_offset + 8..tag_offset + 12].try_into().ok()?) as usize;
1370 if count == 0 {
1371 return None;
1372 }
1373 let str_end = (tag_offset + 12 + count).min(tag_offset + tag_size);
1374 let s = &data[tag_offset + 12..str_end];
1375 let s = s.split(|&b| b == 0).next().unwrap_or(s);
1377 return Some(String::from_utf8_lossy(s).to_string());
1378 } else if type_sig == b"mluc" {
1379 if tag_size < 20 {
1381 return None;
1382 }
1383 let record_count =
1384 u32::from_be_bytes(data[tag_offset + 8..tag_offset + 12].try_into().ok()?) as usize;
1385 if record_count == 0 {
1386 return None;
1387 }
1388 let rec_base = tag_offset + 16;
1390 if rec_base + 12 > data.len() {
1391 return None;
1392 }
1393 let str_len =
1394 u32::from_be_bytes(data[rec_base + 4..rec_base + 8].try_into().ok()?) as usize;
1395 let str_off =
1396 u32::from_be_bytes(data[rec_base + 8..rec_base + 12].try_into().ok()?) as usize;
1397 let abs_off = tag_offset + str_off;
1398 if abs_off + str_len > data.len() || str_len < 2 {
1399 return None;
1400 }
1401 let utf16: Vec<u16> = data[abs_off..abs_off + str_len]
1403 .chunks_exact(2)
1404 .map(|c| u16::from_be_bytes([c[0], c[1]]))
1405 .collect();
1406 return Some(
1407 String::from_utf16_lossy(&utf16)
1408 .trim_end_matches('\0')
1409 .to_string(),
1410 );
1411 }
1412
1413 break;
1414 }
1415 None
1416}
1417
1418fn build_type0_function(writer: &mut PdfWriter, table: &[f64]) -> u32 {
1421 let dict_entries = vec![
1422 (b"FunctionType".to_vec(), PdfObj::Int(0)),
1423 (
1424 b"Domain".to_vec(),
1425 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1426 ),
1427 (
1428 b"Range".to_vec(),
1429 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1430 ),
1431 (
1432 b"Size".to_vec(),
1433 PdfObj::Array(vec![PdfObj::Int(table.len() as i64)]),
1434 ),
1435 (b"BitsPerSample".to_vec(), PdfObj::Int(8)),
1436 ];
1437 let data: Vec<u8> = table
1438 .iter()
1439 .map(|&v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
1440 .collect();
1441 writer.add_stream(dict_entries, &data, false)
1442}
1443
1444fn build_transfer_tr2(
1447 writer: &mut PdfWriter,
1448 tables: &[Option<std::sync::Arc<Vec<f64>>>],
1449 is_color: bool,
1450) -> PdfObj {
1451 if is_color && tables.len() == 4 {
1452 let refs: Vec<PdfObj> = tables
1454 .iter()
1455 .map(|t| {
1456 if let Some(table) = t {
1457 let func_ref = build_type0_function(writer, table);
1458 PdfObj::Ref(func_ref)
1459 } else {
1460 PdfObj::name("Identity")
1461 }
1462 })
1463 .collect();
1464 PdfObj::Array(refs)
1465 } else if !is_color && tables.len() == 1 {
1466 if let Some(ref table) = tables[0] {
1467 let func_ref = build_type0_function(writer, table);
1468 PdfObj::Ref(func_ref)
1469 } else {
1470 PdfObj::name("Identity")
1471 }
1472 } else {
1473 PdfObj::name("Identity")
1474 }
1475}
1476
1477fn build_type0_function_signed(writer: &mut PdfWriter, table: &[f64]) -> u32 {
1480 let dict_entries = vec![
1481 (b"FunctionType".to_vec(), PdfObj::Int(0)),
1482 (
1483 b"Domain".to_vec(),
1484 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1485 ),
1486 (
1487 b"Range".to_vec(),
1488 PdfObj::Array(vec![PdfObj::Int(-1), PdfObj::Int(1)]),
1489 ),
1490 (
1491 b"Size".to_vec(),
1492 PdfObj::Array(vec![PdfObj::Int(table.len() as i64)]),
1493 ),
1494 (b"BitsPerSample".to_vec(), PdfObj::Int(8)),
1495 ];
1496 let data: Vec<u8> = table
1498 .iter()
1499 .map(|&v| ((v.clamp(-1.0, 1.0) + 1.0) / 2.0 * 255.0).round() as u8)
1500 .collect();
1501 writer.add_stream(dict_entries, &data, false)
1502}
1503
1504fn build_type4_function(writer: &mut PdfWriter, tokens: &[u8]) -> u32 {
1507 let dict_entries = vec![
1508 (b"FunctionType".to_vec(), PdfObj::Int(4)),
1509 (
1510 b"Domain".to_vec(),
1511 PdfObj::Array(vec![
1512 PdfObj::Int(-1),
1513 PdfObj::Int(1),
1514 PdfObj::Int(-1),
1515 PdfObj::Int(1),
1516 ]),
1517 ),
1518 (
1519 b"Range".to_vec(),
1520 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1521 ),
1522 ];
1523 writer.add_stream(dict_entries, tokens, false)
1524}
1525
1526fn build_type0_function_2d(writer: &mut PdfWriter, table: &[f64]) -> u32 {
1529 let dict_entries = vec![
1530 (b"FunctionType".to_vec(), PdfObj::Int(0)),
1531 (
1532 b"Domain".to_vec(),
1533 PdfObj::Array(vec![
1534 PdfObj::Int(-1),
1535 PdfObj::Int(1),
1536 PdfObj::Int(-1),
1537 PdfObj::Int(1),
1538 ]),
1539 ),
1540 (
1541 b"Range".to_vec(),
1542 PdfObj::Array(vec![PdfObj::Int(0), PdfObj::Int(1)]),
1543 ),
1544 (
1545 b"Size".to_vec(),
1546 PdfObj::Array(vec![PdfObj::Int(64), PdfObj::Int(64)]),
1547 ),
1548 (b"BitsPerSample".to_vec(), PdfObj::Int(8)),
1549 ];
1550 let data: Vec<u8> = table
1551 .iter()
1552 .map(|&v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
1553 .collect();
1554 writer.add_stream(dict_entries, &data, false)
1555}
1556
1557fn build_halftone_screen(
1560 writer: &mut PdfWriter,
1561 screen: &stet_graphics::device::HalftoneScreen,
1562) -> PdfObj {
1563 let spot_func = if let Some(ref tokens) = screen.type4_tokens {
1564 let func_ref = build_type4_function(writer, tokens);
1565 PdfObj::Ref(func_ref)
1566 } else if let Some(ref table) = screen.sampled_2d {
1567 let func_ref = build_type0_function_2d(writer, table);
1568 PdfObj::Ref(func_ref)
1569 } else {
1570 PdfObj::name("Default")
1571 };
1572
1573 let entries = vec![
1574 (b"Type".to_vec(), PdfObj::name("Halftone")),
1575 (b"HalftoneType".to_vec(), PdfObj::Int(1)),
1576 (b"Frequency".to_vec(), PdfObj::Real(screen.frequency)),
1577 (b"Angle".to_vec(), PdfObj::Real(screen.angle)),
1578 (b"SpotFunction".to_vec(), spot_func),
1579 ];
1580 let obj_ref = writer.add_object(&PdfObj::Dict(entries));
1581 PdfObj::Ref(obj_ref)
1582}
1583
1584fn build_halftone_ht(
1586 writer: &mut PdfWriter,
1587 state: &stet_graphics::device::HalftoneState,
1588) -> PdfObj {
1589 if let Some(ref color) = state.color {
1590 let mut entries = vec![
1592 (b"Type".to_vec(), PdfObj::name("Halftone")),
1593 (b"HalftoneType".to_vec(), PdfObj::Int(5)),
1594 ];
1595 let component_names: [&[u8]; 4] = [b"Red", b"Green", b"Blue", b"Default"];
1596 for (i, screen_opt) in color.iter().enumerate() {
1597 if let Some(screen) = screen_opt {
1598 let ht_obj = build_halftone_screen(writer, screen);
1599 entries.push((component_names[i].to_vec(), ht_obj));
1600 }
1601 }
1602 let obj_ref = writer.add_object(&PdfObj::Dict(entries));
1603 PdfObj::Ref(obj_ref)
1604 } else if let Some(ref gray) = state.gray {
1605 build_halftone_screen(writer, gray)
1606 } else {
1607 PdfObj::name("Default")
1608 }
1609}
1610
1611#[derive(Default, Clone)]
1613struct EffectivePageOverride {
1614 boxes: stet_core::pdfmark::PageBoxes,
1615 rotate: Option<i32>,
1616 additional_actions: Option<stet_core::pdfmark::PageAdditionalActions>,
1617}
1618
1619fn compute_page_overrides(ctx: Option<&Context>, page_count: usize) -> Vec<EffectivePageOverride> {
1624 use stet_core::pdfmark::{PageOverrideScope, PdfMarkRecord};
1625 let mut out = vec![EffectivePageOverride::default(); page_count];
1626 let Some(c) = ctx else {
1627 return out;
1628 };
1629 let mut all_boxes = stet_core::pdfmark::PageBoxes::default();
1630 let mut all_rotate: Option<i32> = None;
1631 let mut all_aa: Option<stet_core::pdfmark::PageAdditionalActions> = None;
1632 let mut per_page_boxes: Vec<stet_core::pdfmark::PageBoxes> =
1633 vec![stet_core::pdfmark::PageBoxes::default(); page_count];
1634 let mut per_page_rotate: Vec<Option<i32>> = vec![None; page_count];
1635 let mut per_page_aa: Vec<Option<stet_core::pdfmark::PageAdditionalActions>> =
1636 vec![None; page_count];
1637
1638 for record in c.pdfmark_buffer.records() {
1639 let PdfMarkRecord::PageOverride(rec) = record else {
1640 continue;
1641 };
1642 match rec.scope {
1643 PageOverrideScope::All => {
1644 all_boxes = rec.boxes.merge_over(&all_boxes);
1645 if rec.rotate.is_some() {
1646 all_rotate = rec.rotate;
1647 }
1648 if let Some(new_aa) = &rec.additional_actions {
1649 all_aa = Some(match all_aa {
1650 Some(prev) => new_aa.merge_over(&prev),
1651 None => new_aa.clone(),
1652 });
1653 }
1654 }
1655 PageOverrideScope::Single(page) => {
1656 let idx = page as usize;
1657 if idx == 0 || idx > page_count {
1658 continue;
1659 }
1660 let i = idx - 1;
1661 per_page_boxes[i] = rec.boxes.merge_over(&per_page_boxes[i]);
1662 if rec.rotate.is_some() {
1663 per_page_rotate[i] = rec.rotate;
1664 }
1665 if let Some(new_aa) = &rec.additional_actions {
1666 per_page_aa[i] = Some(match per_page_aa[i].clone() {
1667 Some(prev) => new_aa.merge_over(&prev),
1668 None => new_aa.clone(),
1669 });
1670 }
1671 }
1672 _ => continue,
1673 }
1674 }
1675
1676 for i in 0..page_count {
1677 out[i].boxes = per_page_boxes[i].merge_over(&all_boxes);
1678 out[i].rotate = per_page_rotate[i].or(all_rotate);
1679 out[i].additional_actions = match (&per_page_aa[i], &all_aa) {
1680 (Some(p), Some(a)) => Some(p.merge_over(a)),
1681 (Some(p), None) => Some(p.clone()),
1682 (None, Some(a)) => Some(a.clone()),
1683 (None, None) => None,
1684 };
1685 }
1686 out
1687}
1688
1689fn days_to_ymd(days: u64) -> (u64, u64, u64) {
1691 let z = days + 719468;
1693 let era = z / 146097;
1694 let doe = z - era * 146097;
1695 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
1696 let y = yoe + era * 400;
1697 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1698 let mp = (5 * doy + 2) / 153;
1699 let d = doy - (153 * mp + 2) / 5 + 1;
1700 let m = if mp < 10 { mp + 3 } else { mp - 9 };
1701 let y = if m <= 2 { y + 1 } else { y };
1702 (y, m, d)
1703}
1704
1705fn default_now_pdf_date() -> String {
1707 use std::time::SystemTime;
1708 let now = SystemTime::now()
1709 .duration_since(SystemTime::UNIX_EPOCH)
1710 .unwrap_or_default()
1711 .as_secs();
1712 let secs_per_day = 86400u64;
1713 let days = now / secs_per_day;
1714 let time_of_day = now % secs_per_day;
1715 let hours = time_of_day / 3600;
1716 let minutes = (time_of_day % 3600) / 60;
1717 let seconds = time_of_day % 60;
1718 let (year, month, day) = days_to_ymd(days);
1719 format!(
1720 "D:{:04}{:02}{:02}{:02}{:02}{:02}Z",
1721 year, month, day, hours, minutes, seconds
1722 )
1723}
1724
1725fn collect_viewer_prefs(ctx: &Context) -> stet_core::pdfmark::ViewerPrefsRecord {
1733 use stet_core::pdfmark::{PdfMarkRecord, ViewerPrefsRecord};
1734 let mut acc = ViewerPrefsRecord::default();
1735 for record in ctx.pdfmark_buffer.records() {
1736 if let PdfMarkRecord::ViewerPrefs(rec) = record {
1737 acc = rec.merge_over(&acc);
1738 }
1739 }
1740 acc
1741}
1742
1743fn collect_docinfo(ctx: &Context) -> stet_core::pdfmark::DocInfoRecord {
1744 let mut acc = stet_core::pdfmark::DocInfoRecord::default();
1745 for record in ctx.pdfmark_buffer.records() {
1746 let stet_core::pdfmark::PdfMarkRecord::DocInfo(rec) = record else {
1747 continue;
1748 };
1749 if let Some(v) = &rec.title {
1750 acc.title = Some(v.clone());
1751 }
1752 if let Some(v) = &rec.author {
1753 acc.author = Some(v.clone());
1754 }
1755 if let Some(v) = &rec.subject {
1756 acc.subject = Some(v.clone());
1757 }
1758 if let Some(v) = &rec.keywords {
1759 acc.keywords = Some(v.clone());
1760 }
1761 if let Some(v) = &rec.creator {
1762 acc.creator = Some(v.clone());
1763 }
1764 if let Some(v) = &rec.producer {
1765 acc.producer = Some(v.clone());
1766 }
1767 if let Some(v) = &rec.creation_date {
1768 acc.creation_date = Some(v.clone());
1769 }
1770 if let Some(v) = &rec.mod_date {
1771 acc.mod_date = Some(v.clone());
1772 }
1773 if let Some(v) = rec.trapped {
1774 acc.trapped = Some(v);
1775 }
1776 }
1777 acc
1778}