summaryrefslogtreecommitdiff
path: root/src/export/pdf.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-06-08 19:31:07 +0200
committerGitHub <noreply@github.com>2022-06-08 19:31:07 +0200
commitcd5a14bc240b21f8f538ff7fc1d116d23611d6c5 (patch)
treef4b8a7e8a941f8653d5061d2f7acc0bfd8d92b13 /src/export/pdf.rs
parentb905048d4bb497252028df6d21b525fefb6b64c3 (diff)
parent72d3f3fffabe6872eb7839585bea925b89aac6a4 (diff)
Merge pull request #75 from typst/semantics
Frame Role and PDF outline
Diffstat (limited to 'src/export/pdf.rs')
-rw-r--r--src/export/pdf.rs345
1 files changed, 241 insertions, 104 deletions
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index aa7acd41..b8fc1e39 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -16,12 +16,13 @@ use ttf_parser::{name_id, GlyphId, Tag};
use super::subset::subset;
use crate::font::{find_name, FaceId, FontStore};
-use crate::frame::{Destination, Element, Frame, Group, Text};
+use crate::frame::{Destination, Element, Frame, Group, Role, Text};
use crate::geom::{
self, Color, Dir, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size,
Stroke, Transform,
};
use crate::image::{Image, ImageId, ImageStore, RasterImage};
+use crate::library::prelude::EcoString;
use crate::library::text::Lang;
use crate::Context;
@@ -42,31 +43,43 @@ const SRGB_GRAY: Name<'static> = Name(b"srgbgray");
/// An exporter for a whole PDF document.
struct PdfExporter<'a> {
+ writer: PdfWriter,
fonts: &'a FontStore,
images: &'a ImageStore,
- writer: PdfWriter,
- alloc: Ref,
pages: Vec<Page>,
- face_map: Remapper<FaceId>,
+ page_heights: Vec<f32>,
+ alloc: Ref,
+ page_tree_ref: Ref,
face_refs: Vec<Ref>,
- glyph_sets: HashMap<FaceId, HashSet<u16>>,
- image_map: Remapper<ImageId>,
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> PdfExporter<'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,
- writer: PdfWriter::new(),
- alloc: Ref::new(1),
pages: vec![],
- face_map: Remapper::new(),
+ page_heights: vec![],
+ alloc,
+ page_tree_ref,
+ page_refs: vec![],
face_refs: vec![],
- glyph_sets: HashMap::new(),
- image_map: Remapper::new(),
image_refs: vec![],
+ face_map: Remapper::new(),
+ image_map: Remapper::new(),
+ glyph_sets: HashMap::new(),
+ languages: HashMap::new(),
+ heading_tree: vec![],
}
}
@@ -74,12 +87,24 @@ impl<'a> PdfExporter<'a> {
self.build_pages(frames);
self.write_fonts();
self.write_images();
- self.write_structure()
+
+ // The root page tree.
+ for page in std::mem::take(&mut self.pages).into_iter() {
+ self.write_page(page);
+ }
+
+ self.write_page_tree();
+ self.write_catalog();
+
+ self.writer.finish()
}
fn build_pages(&mut self, frames: &[Arc<Frame>]) {
for frame in frames {
- let page = PageExporter::new(self).export(frame);
+ let page_id = self.alloc.bump();
+ self.page_refs.push(page_id);
+ let page = PageExporter::new(self, page_id).export(frame);
+ self.page_heights.push(page.size.y.to_f32());
self.pages.push(page);
}
}
@@ -299,70 +324,52 @@ impl<'a> PdfExporter<'a> {
}
}
- fn write_structure(mut self) -> Vec<u8> {
- // The root page tree.
- let page_tree_ref = self.alloc.bump();
-
- // The page objects (non-root nodes in the page tree).
- let mut page_refs = vec![];
- let mut page_heights = vec![];
- for page in &self.pages {
- let page_id = self.alloc.bump();
- page_refs.push(page_id);
- page_heights.push(page.size.y.to_f32());
- }
-
- let mut languages = HashMap::new();
- for (page, page_id) in self.pages.into_iter().zip(page_refs.iter()) {
- let content_id = self.alloc.bump();
-
- let mut page_writer = self.writer.page(*page_id);
- page_writer.parent(page_tree_ref);
-
- let w = page.size.x.to_f32();
- let h = page.size.y.to_f32();
- page_writer.media_box(Rect::new(0.0, 0.0, w, h));
- page_writer.contents(content_id);
-
- let mut annotations = page_writer.annotations();
- for (dest, rect) in page.links {
- let mut link = annotations.push();
- link.subtype(AnnotationType::Link).rect(rect);
- match dest {
- Destination::Url(uri) => {
- link.action()
- .action_type(ActionType::Uri)
- .uri(Str(uri.as_str().as_bytes()));
- }
- Destination::Internal(loc) => {
- let index = loc.page - 1;
- let height = page_heights[index];
- link.action()
- .action_type(ActionType::GoTo)
- .destination_direct()
- .page(page_refs[index])
- .xyz(loc.pos.x.to_f32(), height - loc.pos.y.to_f32(), None);
- }
+ fn write_page(&mut self, page: Page) {
+ let content_id = self.alloc.bump();
+
+ let mut page_writer = self.writer.page(page.id);
+ page_writer.parent(self.page_tree_ref);
+
+ let w = page.size.x.to_f32();
+ let h = page.size.y.to_f32();
+ page_writer.media_box(Rect::new(0.0, 0.0, w, h));
+ page_writer.contents(content_id);
+
+ let mut annotations = page_writer.annotations();
+ for (dest, rect) in page.links {
+ let mut link = annotations.push();
+ link.subtype(AnnotationType::Link).rect(rect);
+ match dest {
+ Destination::Url(uri) => {
+ link.action()
+ .action_type(ActionType::Uri)
+ .uri(Str(uri.as_str().as_bytes()));
+ }
+ Destination::Internal(loc) => {
+ let index = loc.page - 1;
+ let height = self.page_heights[index];
+ link.action()
+ .action_type(ActionType::GoTo)
+ .destination_direct()
+ .page(self.page_refs[index])
+ .xyz(loc.pos.x.to_f32(), height - loc.pos.y.to_f32(), None);
}
}
+ }
- annotations.finish();
- page_writer.finish();
+ annotations.finish();
+ page_writer.finish();
- for (lang, count) in page.languages {
- languages
- .entry(lang)
- .and_modify(|x| *x += count)
- .or_insert_with(|| count);
- }
-
- self.writer
- .stream(content_id, &deflate(&page.content.finish()))
- .filter(Filter::FlateDecode);
- }
+ self.writer
+ .stream(content_id, &deflate(&page.content.finish()))
+ .filter(Filter::FlateDecode);
+ }
- let mut pages = self.writer.pages(page_tree_ref);
- pages.count(page_refs.len() as i32).kids(page_refs);
+ fn write_page_tree(&mut self) {
+ let mut pages = self.writer.pages(self.page_tree_ref);
+ pages
+ .count(self.page_refs.len() as i32)
+ .kids(self.page_refs.iter().copied());
let mut resources = pages.resources();
let mut spaces = resources.color_spaces();
@@ -387,11 +394,36 @@ impl<'a> PdfExporter<'a> {
images.finish();
resources.finish();
pages.finish();
+ }
- let lang = languages
- .into_iter()
- .max_by(|(_, v1), (_, v2)| v1.cmp(v2))
- .map(|(k, _)| k);
+ fn write_catalog(&mut self) {
+ // Build the outline tree.
+ let outline_root_id = (!self.heading_tree.is_empty()).then(|| self.alloc.bump());
+ let outline_start_ref = self.alloc;
+ let len = self.heading_tree.len();
+ let mut prev_ref = None;
+
+ for (i, node) in std::mem::take(&mut self.heading_tree).iter().enumerate() {
+ prev_ref = Some(self.write_outline_item(
+ node,
+ outline_root_id.unwrap(),
+ prev_ref,
+ i + 1 == len,
+ ));
+ }
+
+ if let Some(outline_root_id) = outline_root_id {
+ let mut outline_root = self.writer.outline(outline_root_id);
+ outline_root.first(outline_start_ref);
+ outline_root.last(Ref::new(self.alloc.get() - 1));
+ outline_root.count(self.heading_tree.len() as i32);
+ }
+
+ let lang = self
+ .languages
+ .iter()
+ .max_by_key(|(&lang, &count)| (count, lang))
+ .map(|(&k, _)| k);
let dir = if lang.map(Lang::dir) == Some(Dir::RTL) {
Direction::R2L
@@ -402,38 +434,88 @@ impl<'a> PdfExporter<'a> {
// Write the document information, catalog and wrap it up!
self.writer.document_info(self.alloc.bump()).creator(TextStr("Typst"));
let mut catalog = self.writer.catalog(self.alloc.bump());
- catalog.pages(page_tree_ref);
+ catalog.pages(self.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();
- self.writer.finish()
+ }
+
+ fn write_outline_item(
+ &mut self,
+ node: &HeadingNode,
+ parent_ref: Ref,
+ prev_ref: Option<Ref>,
+ is_last: bool,
+ ) -> Ref {
+ let id = self.alloc.bump();
+ let next_ref = Ref::new(id.get() + node.len() as i32);
+
+ let mut outline = self.writer.outline_item(id);
+ outline.parent(parent_ref);
+
+ if !is_last {
+ outline.next(next_ref);
+ }
+
+ if let Some(prev_rev) = prev_ref {
+ outline.prev(prev_rev);
+ }
+
+ if !node.children.is_empty() {
+ let current_child = Ref::new(id.get() + 1);
+ outline.first(current_child);
+ outline.last(Ref::new(next_ref.get() - 1));
+ outline.count(-1 * node.children.len() as i32);
+ }
+
+ outline.title(TextStr(&node.heading.content));
+ outline.dest_direct().page(node.heading.page).xyz(
+ node.heading.position.x.to_f32(),
+ (node.heading.position.y + Length::pt(3.0)).to_f32(),
+ None,
+ );
+
+ outline.finish();
+
+ let mut prev_ref = None;
+ for (i, child) in node.children.iter().enumerate() {
+ prev_ref = Some(self.write_outline_item(
+ child,
+ id,
+ prev_ref,
+ i + 1 == node.children.len(),
+ ));
+ }
+
+ id
}
}
/// An exporter for the contents of a single PDF page.
-struct PageExporter<'a> {
- fonts: &'a FontStore,
- font_map: &'a mut Remapper<FaceId>,
- image_map: &'a mut Remapper<ImageId>,
- glyphs: &'a mut HashMap<FaceId, HashSet<u16>>,
- languages: HashMap<Lang, usize>,
- bottom: f32,
+struct PageExporter<'a, 'b> {
+ exporter: &'a mut PdfExporter<'b>,
+ page_ref: Ref,
content: Content,
- links: Vec<(Destination, Rect)>,
state: State,
saves: Vec<State>,
+ bottom: f32,
+ links: Vec<(Destination, Rect)>,
}
/// Data for an exported page.
struct Page {
+ id: Ref,
size: Size,
content: Content,
links: Vec<(Destination, Rect)>,
- languages: HashMap<Lang, usize>,
}
/// A simulated graphics state used to deduplicate graphics state changes and
@@ -448,19 +530,56 @@ struct State {
stroke_space: Option<Name<'static>>,
}
-impl<'a> PageExporter<'a> {
- fn new(exporter: &'a mut PdfExporter) -> Self {
+/// A heading that can later be linked in the outline panel.
+#[derive(Debug, Clone)]
+struct Heading {
+ content: EcoString,
+ level: usize,
+ position: Point,
+ page: Ref,
+}
+
+#[derive(Debug, Clone)]
+struct HeadingNode {
+ heading: Heading,
+ children: Vec<HeadingNode>,
+}
+
+impl HeadingNode {
+ fn leaf(heading: Heading) -> Self {
+ HeadingNode { heading, children: Vec::new() }
+ }
+
+ fn len(&self) -> usize {
+ 1 + self.children.iter().map(Self::len).sum::<usize>()
+ }
+
+ fn insert(&mut self, other: Heading, level: usize) -> bool {
+ if level >= other.level {
+ return false;
+ }
+
+ if let Some(child) = self.children.last_mut() {
+ if child.insert(other.clone(), level + 1) {
+ return true;
+ }
+ }
+
+ self.children.push(Self::leaf(other));
+ true
+ }
+}
+
+impl<'a, 'b> PageExporter<'a, 'b> {
+ fn new(exporter: &'a mut PdfExporter<'b>, page_ref: Ref) -> Self {
Self {
- fonts: exporter.fonts,
- font_map: &mut exporter.face_map,
- image_map: &mut exporter.image_map,
- glyphs: &mut exporter.glyph_sets,
- languages: HashMap::new(),
- bottom: 0.0,
+ exporter,
+ page_ref,
content: Content::new(),
- links: vec![],
state: State::default(),
saves: vec![],
+ bottom: 0.0,
+ links: vec![],
}
}
@@ -479,12 +598,29 @@ impl<'a> PageExporter<'a> {
Page {
size: frame.size,
content: self.content,
+ id: self.page_ref,
links: self.links,
- languages: self.languages,
}
}
fn write_frame(&mut self, frame: &Frame) {
+ if let Some(Role::Heading(level)) = frame.role() {
+ let heading = Heading {
+ position: Point::new(self.state.transform.tx, self.state.transform.ty),
+ content: frame.text(),
+ page: self.page_ref,
+ level,
+ };
+
+ if let Some(last) = self.exporter.heading_tree.last_mut() {
+ if !last.insert(heading.clone(), 1) {
+ self.exporter.heading_tree.push(HeadingNode::leaf(heading))
+ }
+ } else {
+ self.exporter.heading_tree.push(HeadingNode::leaf(heading))
+ }
+ }
+
for &(pos, ref element) in &frame.elements {
let x = pos.x.to_f32();
let y = pos.y.to_f32();
@@ -521,13 +657,14 @@ impl<'a> PageExporter<'a> {
}
fn write_text(&mut self, x: f32, y: f32, text: &Text) {
- *self.languages.entry(text.lang).or_insert(0) += text.glyphs.len();
- self.glyphs
+ *self.exporter.languages.entry(text.lang).or_insert(0) += text.glyphs.len();
+ self.exporter
+ .glyph_sets
.entry(text.face_id)
.or_default()
.extend(text.glyphs.iter().map(|g| g.id));
- let face = self.fonts.get(text.face_id);
+ let face = self.exporter.fonts.get(text.face_id);
self.set_fill(text.fill);
self.set_font(text.face_id, text.size);
@@ -641,8 +778,8 @@ impl<'a> PageExporter<'a> {
}
fn write_image(&mut self, x: f32, y: f32, id: ImageId, size: Size) {
- self.image_map.insert(id);
- let name = format_eco!("Im{}", self.image_map.map(id));
+ self.exporter.image_map.insert(id);
+ let name = format_eco!("Im{}", self.exporter.image_map.map(id));
let w = size.x.to_f32();
let h = size.y.to_f32();
self.content.save_state();
@@ -705,8 +842,8 @@ impl<'a> PageExporter<'a> {
fn set_font(&mut self, face_id: FaceId, size: Length) {
if self.state.font != Some((face_id, size)) {
- self.font_map.insert(face_id);
- let name = format_eco!("F{}", self.font_map.map(face_id));
+ self.exporter.face_map.insert(face_id);
+ let name = format_eco!("F{}", self.exporter.face_map.map(face_id));
self.content.set_font(Name(name.as_bytes()), size.to_f32());
self.state.font = Some((face_id, size));
}