diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-06-14 21:36:33 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-06-14 21:36:33 +0200 |
| commit | 4817c62dfbaf8cd200ed3582665995fd01fac263 (patch) | |
| tree | aa5dd0143743c5b7013e5d47eb7fdbe7621318ca /src/export/pdf/mod.rs | |
| parent | 7fb19d5ef8dc3b183d7de811e373768de56f7ee8 (diff) | |
Split up PDF exporter
Diffstat (limited to 'src/export/pdf/mod.rs')
| -rw-r--r-- | src/export/pdf/mod.rs | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/src/export/pdf/mod.rs b/src/export/pdf/mod.rs new file mode 100644 index 00000000..4afe749f --- /dev/null +++ b/src/export/pdf/mod.rs @@ -0,0 +1,230 @@ +//! Exporting into PDF documents. + +mod font; +mod image; +mod outline; +mod page; + +use std::cmp::Eq; +use std::collections::{HashMap, HashSet}; +use std::hash::Hash; + +use pdf_writer::types::Direction; +use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr}; + +use self::outline::{Heading, HeadingNode}; +use self::page::Page; +use crate::font::{FaceId, FontStore}; +use crate::frame::Frame; +use crate::geom::{Dir, Em, Length}; +use crate::image::{ImageId, ImageStore}; +use crate::library::text::Lang; +use crate::Context; + +/// Export a collection of frames into a PDF file. +/// +/// This creates one page per frame. In addition to the frames, you need to pass +/// in the context used during compilation so that fonts and images can be +/// included in the PDF. +/// +/// Returns the raw bytes making up the PDF file. +pub fn pdf(ctx: &Context, frames: &[Frame]) -> Vec<u8> { + let mut ctx = PdfContext::new(ctx); + page::construct_pages(&mut ctx, frames); + font::write_fonts(&mut ctx); + image::write_images(&mut ctx); + page::write_page_tree(&mut ctx); + write_catalog(&mut ctx); + ctx.writer.finish() +} + +/// Identifies the color space definitions. +const SRGB: Name<'static> = Name(b"srgb"); +const D65_GRAY: Name<'static> = Name(b"d65gray"); + +/// Context for exporting a whole PDF document. +pub struct PdfContext<'a> { + writer: PdfWriter, + fonts: &'a FontStore, + images: &'a ImageStore, + pages: Vec<Page>, + page_heights: Vec<f32>, + alloc: Ref, + page_tree_ref: Ref, + face_refs: Vec<Ref>, + image_refs: Vec<Ref>, + page_refs: Vec<Ref>, + face_map: Remapper<FaceId>, + image_map: Remapper<ImageId>, + glyph_sets: HashMap<FaceId, HashSet<u16>>, + languages: HashMap<Lang, usize>, + heading_tree: Vec<HeadingNode>, +} + +impl<'a> PdfContext<'a> { + fn new(ctx: &'a Context) -> Self { + let mut alloc = Ref::new(1); + let page_tree_ref = alloc.bump(); + Self { + writer: PdfWriter::new(), + fonts: &ctx.fonts, + images: &ctx.images, + pages: vec![], + page_heights: vec![], + alloc, + page_tree_ref, + page_refs: vec![], + face_refs: vec![], + image_refs: vec![], + face_map: Remapper::new(), + image_map: Remapper::new(), + glyph_sets: HashMap::new(), + languages: HashMap::new(), + heading_tree: vec![], + } + } +} + +/// Write the document catalog. +fn write_catalog(ctx: &mut PdfContext) { + // Build the outline tree. + let outline_root_id = (!ctx.heading_tree.is_empty()).then(|| ctx.alloc.bump()); + let outline_start_ref = ctx.alloc; + let len = ctx.heading_tree.len(); + let mut prev_ref = None; + + for (i, node) in std::mem::take(&mut ctx.heading_tree).iter().enumerate() { + prev_ref = Some(outline::write_outline_item( + ctx, + node, + outline_root_id.unwrap(), + prev_ref, + i + 1 == len, + )); + } + + if let Some(outline_root_id) = outline_root_id { + let mut outline_root = ctx.writer.outline(outline_root_id); + outline_root.first(outline_start_ref); + outline_root.last(Ref::new(ctx.alloc.get() - 1)); + outline_root.count(ctx.heading_tree.len() as i32); + } + + let lang = ctx + .languages + .iter() + .max_by_key(|(&lang, &count)| (count, lang)) + .map(|(&k, _)| k); + + let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { + Direction::R2L + } else { + Direction::L2R + }; + + // Write the document information. + ctx.writer.document_info(ctx.alloc.bump()).creator(TextStr("Typst")); + + // Write the document catalog. + let mut catalog = ctx.writer.catalog(ctx.alloc.bump()); + catalog.pages(ctx.page_tree_ref); + catalog.viewer_preferences().direction(dir); + + if let Some(outline_root_id) = outline_root_id { + catalog.outlines(outline_root_id); + } + + if let Some(lang) = lang { + catalog.lang(TextStr(lang.as_str())); + } + + catalog.finish(); +} + +/// Compress data with the DEFLATE algorithm. +fn deflate(data: &[u8]) -> Vec<u8> { + const COMPRESSION_LEVEL: u8 = 6; + miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL) +} + +/// Assigns new, consecutive PDF-internal indices to things. +struct Remapper<Index> { + /// Forwards from the old indices to the new pdf indices. + to_pdf: HashMap<Index, usize>, + /// Backwards from the pdf indices to the old indices. + to_layout: Vec<Index>, +} + +impl<Index> Remapper<Index> +where + Index: Copy + Eq + Hash, +{ + fn new() -> Self { + Self { + to_pdf: HashMap::new(), + to_layout: vec![], + } + } + + fn insert(&mut self, index: Index) { + let to_layout = &mut self.to_layout; + self.to_pdf.entry(index).or_insert_with(|| { + let pdf_index = to_layout.len(); + to_layout.push(index); + pdf_index + }); + } + + fn map(&self, index: Index) -> usize { + self.to_pdf[&index] + } + + fn pdf_indices<'a>( + &'a self, + refs: &'a [Ref], + ) -> impl Iterator<Item = (Ref, usize)> + 'a { + refs.iter().copied().zip(0 .. self.to_pdf.len()) + } + + fn layout_indices(&self) -> impl Iterator<Item = Index> + '_ { + self.to_layout.iter().copied() + } +} + +/// Additional methods for [`Length`]. +trait LengthExt { + /// Convert an em length to a number of points. + fn to_f32(self) -> f32; +} + +impl LengthExt for Length { + fn to_f32(self) -> f32 { + self.to_pt() as f32 + } +} + +/// Additional methods for [`Em`]. +trait EmExt { + /// Convert an em length to a number of PDF font units. + fn to_font_units(self) -> f32; +} + +impl EmExt for Em { + fn to_font_units(self) -> f32 { + 1000.0 * self.get() as f32 + } +} + +/// Additional methods for [`Ref`]. +trait RefExt { + /// Bump the reference up by one and return the previous one. + fn bump(&mut self) -> Self; +} + +impl RefExt for Ref { + fn bump(&mut self) -> Self { + let prev = *self; + *self = Self::new(prev.get() + 1); + prev + } +} |
