1mod catalog;
4mod color;
5mod color_font;
6mod content;
7mod embed;
8mod extg;
9mod font;
10mod gradient;
11mod image;
12mod named_destination;
13mod outline;
14mod page;
15mod resources;
16mod tiling;
17
18use std::collections::{BTreeMap, HashMap};
19use std::fmt::{self, Debug, Formatter};
20use std::hash::Hash;
21use std::ops::{Deref, DerefMut};
22
23use base64::Engine;
24use ecow::EcoString;
25use pdf_writer::{Chunk, Name, Pdf, Ref, Str, TextStr};
26use serde::{Deserialize, Serialize};
27use typst_library::diag::{bail, SourceResult, StrResult};
28use typst_library::foundations::{Datetime, Smart};
29use typst_library::layout::{Abs, Em, PageRanges, PagedDocument, Transform};
30use typst_library::text::Font;
31use typst_library::visualize::Image;
32use typst_syntax::Span;
33use typst_utils::Deferred;
34
35use crate::catalog::write_catalog;
36use crate::color::{alloc_color_functions_refs, ColorFunctionRefs};
37use crate::color_font::{write_color_fonts, ColorFontSlice};
38use crate::embed::write_embedded_files;
39use crate::extg::{write_graphic_states, ExtGState};
40use crate::font::write_fonts;
41use crate::gradient::{write_gradients, PdfGradient};
42use crate::image::write_images;
43use crate::named_destination::{write_named_destinations, NamedDestinations};
44use crate::page::{alloc_page_refs, traverse_pages, write_page_tree, EncodedPage};
45use crate::resources::{
46 alloc_resources_refs, write_resource_dictionaries, Resources, ResourcesRefs,
47};
48use crate::tiling::{write_tilings, PdfTiling};
49
50#[typst_macros::time(name = "pdf")]
54pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult<Vec<u8>> {
55 PdfBuilder::new(document, options)
56 .phase(|builder| builder.run(traverse_pages))?
57 .phase(|builder| {
58 Ok(GlobalRefs {
59 color_functions: builder.run(alloc_color_functions_refs)?,
60 pages: builder.run(alloc_page_refs)?,
61 resources: builder.run(alloc_resources_refs)?,
62 })
63 })?
64 .phase(|builder| {
65 Ok(References {
66 named_destinations: builder.run(write_named_destinations)?,
67 fonts: builder.run(write_fonts)?,
68 color_fonts: builder.run(write_color_fonts)?,
69 images: builder.run(write_images)?,
70 gradients: builder.run(write_gradients)?,
71 tilings: builder.run(write_tilings)?,
72 ext_gs: builder.run(write_graphic_states)?,
73 embedded_files: builder.run(write_embedded_files)?,
74 })
75 })?
76 .phase(|builder| builder.run(write_page_tree))?
77 .phase(|builder| builder.run(write_resource_dictionaries))?
78 .export_with(write_catalog)
79}
80
81#[derive(Debug, Default)]
83pub struct PdfOptions<'a> {
84 pub ident: Smart<&'a str>,
96 pub timestamp: Option<Timestamp>,
99 pub page_ranges: Option<PageRanges>,
102 pub standards: PdfStandards,
104}
105
106#[derive(Debug, Clone, Copy)]
108pub struct Timestamp {
109 pub(crate) datetime: Datetime,
111 pub(crate) timezone: Timezone,
113}
114
115impl Timestamp {
116 pub fn new_utc(datetime: Datetime) -> Self {
118 Self { datetime, timezone: Timezone::UTC }
119 }
120
121 pub fn new_local(datetime: Datetime, whole_minute_offset: i32) -> Option<Self> {
123 let hour_offset = (whole_minute_offset / 60).try_into().ok()?;
124 let minute_offset = (whole_minute_offset % 60).abs().try_into().ok()?;
129 match (hour_offset, minute_offset) {
130 (-23..=23, 0..=59) => Some(Self {
133 datetime,
134 timezone: Timezone::Local { hour_offset, minute_offset },
135 }),
136 _ => None,
137 }
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum Timezone {
144 UTC,
146 Local { hour_offset: i8, minute_offset: u8 },
149}
150
151#[derive(Clone)]
153pub struct PdfStandards {
154 pub(crate) pdfa: bool,
157 pub(crate) embedded_files: bool,
161 pub(crate) pdfa_part: Option<(i32, &'static str)>,
163}
164
165impl PdfStandards {
166 pub fn new(list: &[PdfStandard]) -> StrResult<Self> {
169 let a2b = list.contains(&PdfStandard::A_2b);
170 let a3b = list.contains(&PdfStandard::A_3b);
171
172 if a2b && a3b {
173 bail!("PDF cannot conform to A-2B and A-3B at the same time")
174 }
175
176 let pdfa = a2b || a3b;
177 Ok(Self {
178 pdfa,
179 embedded_files: !a2b,
180 pdfa_part: pdfa.then_some((if a2b { 2 } else { 3 }, "B")),
181 })
182 }
183}
184
185impl Debug for PdfStandards {
186 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
187 f.pad("PdfStandards(..)")
188 }
189}
190
191impl Default for PdfStandards {
192 fn default() -> Self {
193 Self { pdfa: false, embedded_files: true, pdfa_part: None }
194 }
195}
196
197#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
201#[allow(non_camel_case_types)]
202#[non_exhaustive]
203pub enum PdfStandard {
204 #[serde(rename = "1.7")]
206 V_1_7,
207 #[serde(rename = "a-2b")]
209 A_2b,
210 #[serde(rename = "a-3b")]
212 A_3b,
213}
214
215struct PdfBuilder<S> {
234 state: S,
236 alloc: Ref,
238 pdf: Pdf,
240}
241
242struct WithDocument<'a> {
246 document: &'a PagedDocument,
248 options: &'a PdfOptions<'a>,
250}
251
252struct WithResources<'a> {
257 document: &'a PagedDocument,
258 options: &'a PdfOptions<'a>,
259 pages: Vec<Option<EncodedPage>>,
265 resources: Resources<()>,
267}
268
269struct GlobalRefs {
271 color_functions: ColorFunctionRefs,
273 pages: Vec<Option<Ref>>,
278 resources: ResourcesRefs,
280}
281
282impl<'a> From<(WithDocument<'a>, (Vec<Option<EncodedPage>>, Resources<()>))>
283 for WithResources<'a>
284{
285 fn from(
286 (previous, (pages, resources)): (
287 WithDocument<'a>,
288 (Vec<Option<EncodedPage>>, Resources<()>),
289 ),
290 ) -> Self {
291 Self {
292 document: previous.document,
293 options: previous.options,
294 pages,
295 resources,
296 }
297 }
298}
299
300struct WithGlobalRefs<'a> {
306 document: &'a PagedDocument,
307 options: &'a PdfOptions<'a>,
308 pages: Vec<Option<EncodedPage>>,
309 resources: Resources,
311 globals: GlobalRefs,
313}
314
315impl<'a> From<(WithResources<'a>, GlobalRefs)> for WithGlobalRefs<'a> {
316 fn from((previous, globals): (WithResources<'a>, GlobalRefs)) -> Self {
317 Self {
318 document: previous.document,
319 options: previous.options,
320 pages: previous.pages,
321 resources: previous.resources.with_refs(&globals.resources),
322 globals,
323 }
324 }
325}
326
327struct References {
329 named_destinations: NamedDestinations,
331 fonts: HashMap<Font, Ref>,
333 color_fonts: HashMap<ColorFontSlice, Ref>,
335 images: HashMap<Image, Ref>,
337 gradients: HashMap<PdfGradient, Ref>,
339 tilings: HashMap<PdfTiling, Ref>,
341 ext_gs: HashMap<ExtGState, Ref>,
343 embedded_files: BTreeMap<EcoString, Ref>,
345}
346
347struct WithRefs<'a> {
351 document: &'a PagedDocument,
352 options: &'a PdfOptions<'a>,
353 globals: GlobalRefs,
354 pages: Vec<Option<EncodedPage>>,
355 resources: Resources,
356 references: References,
358}
359
360impl<'a> From<(WithGlobalRefs<'a>, References)> for WithRefs<'a> {
361 fn from((previous, references): (WithGlobalRefs<'a>, References)) -> Self {
362 Self {
363 document: previous.document,
364 options: previous.options,
365 globals: previous.globals,
366 pages: previous.pages,
367 resources: previous.resources,
368 references,
369 }
370 }
371}
372
373struct WithEverything<'a> {
377 document: &'a PagedDocument,
378 options: &'a PdfOptions<'a>,
379 globals: GlobalRefs,
380 pages: Vec<Option<EncodedPage>>,
381 resources: Resources,
382 references: References,
383 page_tree_ref: Ref,
385}
386
387impl<'a> From<(WithEverything<'a>, ())> for WithEverything<'a> {
388 fn from((this, _): (WithEverything<'a>, ())) -> Self {
389 this
390 }
391}
392
393impl<'a> From<(WithRefs<'a>, Ref)> for WithEverything<'a> {
394 fn from((previous, page_tree_ref): (WithRefs<'a>, Ref)) -> Self {
395 Self {
396 document: previous.document,
397 options: previous.options,
398 globals: previous.globals,
399 resources: previous.resources,
400 references: previous.references,
401 pages: previous.pages,
402 page_tree_ref,
403 }
404 }
405}
406
407impl<'a> PdfBuilder<WithDocument<'a>> {
408 fn new(document: &'a PagedDocument, options: &'a PdfOptions<'a>) -> Self {
410 Self {
411 alloc: Ref::new(1),
412 pdf: Pdf::new(),
413 state: WithDocument { document, options },
414 }
415 }
416}
417
418impl<S> PdfBuilder<S> {
419 fn phase<NS, B, O>(mut self, builder: B) -> SourceResult<PdfBuilder<NS>>
421 where
422 NS: From<(S, O)>,
424 B: Fn(&mut Self) -> SourceResult<O>,
426 {
427 let output = builder(&mut self)?;
428 Ok(PdfBuilder {
429 state: NS::from((self.state, output)),
430 alloc: self.alloc,
431 pdf: self.pdf,
432 })
433 }
434
435 fn run<P, O>(&mut self, process: P) -> SourceResult<O>
438 where
439 P: Fn(&S) -> SourceResult<(PdfChunk, O)>,
441 O: Renumber,
443 {
444 let (chunk, mut output) = process(&self.state)?;
445 let allocated = chunk.alloc.get() - TEMPORARY_REFS_START;
447 let offset = TEMPORARY_REFS_START - self.alloc.get();
448
449 chunk.renumber_into(&mut self.pdf, |mut r| {
451 r.renumber(offset);
452
453 r
454 });
455
456 output.renumber(offset);
458
459 self.alloc = Ref::new(self.alloc.get() + allocated);
460
461 Ok(output)
462 }
463
464 fn export_with<P>(mut self, process: P) -> SourceResult<Vec<u8>>
467 where
468 P: Fn(S, &mut Pdf, &mut Ref) -> SourceResult<()>,
469 {
470 process(self.state, &mut self.pdf, &mut self.alloc)?;
471 Ok(self.pdf.finish())
472 }
473}
474
475trait Renumber {
478 fn renumber(&mut self, offset: i32);
480}
481
482impl Renumber for () {
483 fn renumber(&mut self, _offset: i32) {}
484}
485
486impl Renumber for Ref {
487 fn renumber(&mut self, offset: i32) {
488 if self.get() >= TEMPORARY_REFS_START {
489 *self = Ref::new(self.get() - offset);
490 }
491 }
492}
493
494impl<R: Renumber> Renumber for Vec<R> {
495 fn renumber(&mut self, offset: i32) {
496 for item in self {
497 item.renumber(offset);
498 }
499 }
500}
501
502impl<T: Eq + Hash, R: Renumber> Renumber for HashMap<T, R> {
503 fn renumber(&mut self, offset: i32) {
504 for v in self.values_mut() {
505 v.renumber(offset);
506 }
507 }
508}
509
510impl<T: Ord, R: Renumber> Renumber for BTreeMap<T, R> {
511 fn renumber(&mut self, offset: i32) {
512 for v in self.values_mut() {
513 v.renumber(offset);
514 }
515 }
516}
517
518impl<R: Renumber> Renumber for Option<R> {
519 fn renumber(&mut self, offset: i32) {
520 if let Some(r) = self {
521 r.renumber(offset)
522 }
523 }
524}
525
526impl<T, R: Renumber> Renumber for (T, R) {
527 fn renumber(&mut self, offset: i32) {
528 self.1.renumber(offset)
529 }
530}
531
532struct PdfChunk {
534 chunk: Chunk,
536 alloc: Ref,
538}
539
540const TEMPORARY_REFS_START: i32 = 1_000_000_000;
548
549impl PdfChunk {
551 fn new() -> Self {
553 PdfChunk {
554 chunk: Chunk::new(),
555 alloc: Ref::new(TEMPORARY_REFS_START),
556 }
557 }
558
559 fn alloc(&mut self) -> Ref {
565 self.alloc.bump()
566 }
567}
568
569impl Deref for PdfChunk {
570 type Target = Chunk;
571
572 fn deref(&self) -> &Self::Target {
573 &self.chunk
574 }
575}
576
577impl DerefMut for PdfChunk {
578 fn deref_mut(&mut self) -> &mut Self::Target {
579 &mut self.chunk
580 }
581}
582
583fn deflate(data: &[u8]) -> Vec<u8> {
585 const COMPRESSION_LEVEL: u8 = 6;
586 miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL)
587}
588
589#[comemo::memoize]
592fn deflate_deferred(content: Vec<u8>) -> Deferred<Vec<u8>> {
593 Deferred::new(move || deflate(&content))
594}
595
596fn hash_base64<T: Hash>(value: &T) -> String {
598 base64::engine::general_purpose::STANDARD
599 .encode(typst_utils::hash128(value).to_be_bytes())
600}
601
602trait AbsExt {
604 fn to_f32(self) -> f32;
606}
607
608impl AbsExt for Abs {
609 fn to_f32(self) -> f32 {
610 self.to_pt() as f32
611 }
612}
613
614trait EmExt {
616 fn to_font_units(self) -> f32;
618}
619
620impl EmExt for Em {
621 fn to_font_units(self) -> f32 {
622 1000.0 * self.get() as f32
623 }
624}
625
626trait NameExt<'a> {
627 const PDFA_LIMIT: usize = 127;
629}
630
631impl<'a> NameExt<'a> for Name<'a> {}
632
633trait StrExt<'a>: Sized {
635 const PDFA_LIMIT: usize = 32767;
637
638 #[allow(unused)]
640 fn trimmed(string: &'a [u8]) -> Self;
641}
642
643impl<'a> StrExt<'a> for Str<'a> {
644 fn trimmed(string: &'a [u8]) -> Self {
645 Self(&string[..string.len().min(Self::PDFA_LIMIT)])
646 }
647}
648
649trait TextStrExt<'a>: Sized {
651 const PDFA_LIMIT: usize = Str::PDFA_LIMIT;
653
654 fn trimmed(string: &'a str) -> Self;
656}
657
658impl<'a> TextStrExt<'a> for TextStr<'a> {
659 fn trimmed(string: &'a str) -> Self {
660 Self(&string[..string.len().min(Self::PDFA_LIMIT)])
661 }
662}
663
664trait ContentExt {
666 fn save_state_checked(&mut self) -> SourceResult<()>;
667}
668
669impl ContentExt for pdf_writer::Content {
670 fn save_state_checked(&mut self) -> SourceResult<()> {
671 self.save_state();
672 if self.state_nesting_depth() > 28 {
673 bail!(
674 Span::detached(),
675 "maximum PDF grouping depth exceeding";
676 hint: "try to avoid excessive nesting of layout containers",
677 );
678 }
679 Ok(())
680 }
681}
682
683fn transform_to_array(ts: Transform) -> [f32; 6] {
685 [
686 ts.sx.get() as f32,
687 ts.ky.get() as f32,
688 ts.kx.get() as f32,
689 ts.sy.get() as f32,
690 ts.tx.to_f32(),
691 ts.ty.to_f32(),
692 ]
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698
699 #[test]
700 fn test_timestamp_new_local() {
701 let dummy_datetime = Datetime::from_ymd_hms(2024, 12, 17, 10, 10, 10).unwrap();
702 let test = |whole_minute_offset, expect_timezone| {
703 assert_eq!(
704 Timestamp::new_local(dummy_datetime, whole_minute_offset)
705 .unwrap()
706 .timezone,
707 expect_timezone
708 );
709 };
710
711 test(0, Timezone::Local { hour_offset: 0, minute_offset: 0 });
713 test(480, Timezone::Local { hour_offset: 8, minute_offset: 0 });
714 test(-480, Timezone::Local { hour_offset: -8, minute_offset: 0 });
715 test(330, Timezone::Local { hour_offset: 5, minute_offset: 30 });
716 test(-210, Timezone::Local { hour_offset: -3, minute_offset: 30 });
717 test(-720, Timezone::Local { hour_offset: -12, minute_offset: 0 }); test(315, Timezone::Local { hour_offset: 5, minute_offset: 15 });
721 test(-225, Timezone::Local { hour_offset: -3, minute_offset: 45 });
722 test(1439, Timezone::Local { hour_offset: 23, minute_offset: 59 });
723 test(-1439, Timezone::Local { hour_offset: -23, minute_offset: 59 });
724
725 assert!(Timestamp::new_local(dummy_datetime, 1440).is_none());
727 assert!(Timestamp::new_local(dummy_datetime, -1440).is_none());
728 assert!(Timestamp::new_local(dummy_datetime, i32::MAX).is_none());
729 assert!(Timestamp::new_local(dummy_datetime, i32::MIN).is_none());
730 }
731}