Write PDF outline

This commit is contained in:
Laurenz 2023-04-17 13:25:31 +02:00
parent 428c55b6ee
commit 9bdc4a7de0
13 changed files with 144 additions and 94 deletions

View File

@ -211,6 +211,7 @@ fn items() -> LangItems {
}, },
bibliography_keys: meta::BibliographyElem::keys, bibliography_keys: meta::BibliographyElem::keys,
heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(), heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(),
heading_func: meta::HeadingElem::func(),
list_item: |body| layout::ListItem::new(body).pack(), list_item: |body| layout::ListItem::new(body).pack(),
enum_item: |number, body| { enum_item: |number, body| {
let mut elem = layout::EnumItem::new(body); let mut elem = layout::EnumItem::new(body);

View File

@ -173,14 +173,14 @@ impl Synthesize for FigureElem {
// Determine the figure's kind. // Determine the figure's kind.
let kind = match self.kind(styles) { let kind = match self.kind(styles) {
Smart::Auto => self Smart::Auto => self
.find_figurable(vt, styles) .find_figurable(styles)
.map(|elem| FigureKind::Elem(elem.func())) .map(|elem| FigureKind::Elem(elem.func()))
.unwrap_or_else(|| FigureKind::Elem(ImageElem::func())), .unwrap_or_else(|| FigureKind::Elem(ImageElem::func())),
Smart::Custom(kind) => kind, Smart::Custom(kind) => kind,
}; };
let content = match &kind { let content = match &kind {
FigureKind::Elem(func) => self.find_of_elem(vt, *func), FigureKind::Elem(func) => self.find_of_elem(*func),
FigureKind::Name(_) => None, FigureKind::Name(_) => None,
} }
.unwrap_or_else(|| self.body()); .unwrap_or_else(|| self.body());
@ -303,9 +303,9 @@ impl Refable for FigureElem {
impl FigureElem { impl FigureElem {
/// Determines the type of the figure by looking at the content, finding all /// Determines the type of the figure by looking at the content, finding all
/// [`Figurable`] elements and sorting them by priority then returning the highest. /// [`Figurable`] elements and sorting them by priority then returning the highest.
pub fn find_figurable(&self, vt: &Vt, styles: StyleChain) -> Option<Content> { pub fn find_figurable(&self, styles: StyleChain) -> Option<Content> {
self.body() self.body()
.query(vt.introspector, Selector::can::<dyn Figurable>()) .query(Selector::can::<dyn Figurable>())
.into_iter() .into_iter()
.max_by_key(|elem| elem.with::<dyn Figurable>().unwrap().priority(styles)) .max_by_key(|elem| elem.with::<dyn Figurable>().unwrap().priority(styles))
.cloned() .cloned()
@ -313,9 +313,9 @@ impl FigureElem {
/// Finds the element with the given function in the figure's content. /// Finds the element with the given function in the figure's content.
/// Returns `None` if no element with the given function is found. /// Returns `None` if no element with the given function is found.
pub fn find_of_elem(&self, vt: &Vt, func: ElemFunc) -> Option<Content> { pub fn find_of_elem(&self, func: ElemFunc) -> Option<Content> {
self.body() self.body()
.query(vt.introspector, Selector::Elem(func, None)) .query(Selector::Elem(func, None))
.into_iter() .into_iter()
.next() .next()
.cloned() .cloned()

View File

@ -23,9 +23,9 @@ pub use typst::geom::*;
#[doc(no_inline)] #[doc(no_inline)]
pub use typst::model::{ pub use typst::model::{
element, Behave, Behaviour, Construct, Content, ElemFunc, Element, Finalize, Fold, element, Behave, Behaviour, Construct, Content, ElemFunc, Element, Finalize, Fold,
Introspector, Label, Locatable, LocatableSelector, Location, MetaElem, Resolve, Introspector, Label, Locatable, LocatableSelector, Location, MetaElem, PlainText,
Selector, Set, Show, StabilityProvider, StyleChain, StyleVec, Styles, Synthesize, Resolve, Selector, Set, Show, StabilityProvider, StyleChain, StyleVec, Styles,
Unlabellable, Vt, Synthesize, Unlabellable, Vt,
}; };
#[doc(no_inline)] #[doc(no_inline)]
pub use typst::syntax::{Span, Spanned}; pub use typst::syntax::{Span, Spanned};

View File

@ -5,7 +5,7 @@ use crate::prelude::*;
/// ///
/// Display: Space /// Display: Space
/// Category: text /// Category: text
#[element(Unlabellable, Behave)] #[element(Behave, Unlabellable, PlainText)]
pub struct SpaceElem {} pub struct SpaceElem {}
impl Behave for SpaceElem { impl Behave for SpaceElem {
@ -16,6 +16,12 @@ impl Behave for SpaceElem {
impl Unlabellable for SpaceElem {} impl Unlabellable for SpaceElem {}
impl PlainText for SpaceElem {
fn plain_text(&self, text: &mut EcoString) {
text.push(' ');
}
}
/// Inserts a line break. /// Inserts a line break.
/// ///
/// Advances the paragraph to the next line. A single trailing line break at the /// Advances the paragraph to the next line. A single trailing line break at the

View File

@ -40,7 +40,7 @@ use crate::prelude::*;
/// ///
/// Display: Text /// Display: Text
/// Category: text /// Category: text
#[element(Construct)] #[element(Construct, PlainText)]
pub struct TextElem { pub struct TextElem {
/// A prioritized sequence of font families. /// A prioritized sequence of font families.
/// ///
@ -497,6 +497,12 @@ impl Construct for TextElem {
} }
} }
impl PlainText for TextElem {
fn plain_text(&self, text: &mut EcoString) {
text.push_str(&self.text());
}
}
/// A lowercased font family like "arial". /// A lowercased font family like "arial".
#[derive(Clone, Eq, PartialEq, Hash)] #[derive(Clone, Eq, PartialEq, Hash)]
pub struct FontFamily(EcoString); pub struct FontFamily(EcoString);

View File

@ -72,6 +72,8 @@ pub struct LangItems {
) -> Vec<(EcoString, Option<EcoString>)>, ) -> Vec<(EcoString, Option<EcoString>)>,
/// A section heading: `= Introduction`. /// A section heading: `= Introduction`.
pub heading: fn(level: NonZeroUsize, body: Content) -> Content, pub heading: fn(level: NonZeroUsize, body: Content) -> Content,
/// The heading function.
pub heading_func: ElemFunc,
/// An item in a bullet list: `- ...`. /// An item in a bullet list: `- ...`.
pub list_item: fn(body: Content) -> Content, pub list_item: fn(body: Content) -> Content,
/// An item in an enumeration (numbered list): `+ ...` or `1. ...`. /// An item in an enumeration (numbered list): `+ ...` or `1. ...`.

View File

@ -13,7 +13,6 @@ use pdf_writer::types::Direction;
use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr}; use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr};
use xmp_writer::{LangId, RenditionClass, XmpWriter}; use xmp_writer::{LangId, RenditionClass, XmpWriter};
use self::outline::HeadingNode;
use self::page::Page; use self::page::Page;
use crate::doc::{Document, Lang}; use crate::doc::{Document, Lang};
use crate::font::Font; use crate::font::Font;
@ -54,7 +53,6 @@ pub struct PdfContext<'a> {
image_map: Remapper<Image>, image_map: Remapper<Image>,
glyph_sets: HashMap<Font, HashSet<u16>>, glyph_sets: HashMap<Font, HashSet<u16>>,
languages: HashMap<Lang, usize>, languages: HashMap<Lang, usize>,
heading_tree: Vec<HeadingNode>,
} }
impl<'a> PdfContext<'a> { impl<'a> PdfContext<'a> {
@ -76,36 +74,12 @@ impl<'a> PdfContext<'a> {
image_map: Remapper::new(), image_map: Remapper::new(),
glyph_sets: HashMap::new(), glyph_sets: HashMap::new(),
languages: HashMap::new(), languages: HashMap::new(),
heading_tree: vec![],
} }
} }
} }
/// Write the document catalog. /// Write the document catalog.
fn write_catalog(ctx: &mut PdfContext) { 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 let lang = ctx
.languages .languages
.iter() .iter()
@ -118,6 +92,9 @@ fn write_catalog(ctx: &mut PdfContext) {
Direction::L2R Direction::L2R
}; };
// Write the outline tree.
let outline_root_id = outline::write_outline(ctx);
// Write the document information. // Write the document information.
let mut info = ctx.writer.document_info(ctx.alloc.bump()); let mut info = ctx.writer.document_info(ctx.alloc.bump());
let mut xmp = XmpWriter::new(); let mut xmp = XmpWriter::new();

View File

@ -1,32 +1,76 @@
use ecow::EcoString; use std::num::NonZeroUsize;
use pdf_writer::{Finish, Ref, TextStr}; use pdf_writer::{Finish, Ref, TextStr};
use super::{AbsExt, PdfContext, RefExt}; use super::{AbsExt, PdfContext, RefExt};
use crate::geom::{Abs, Point}; use crate::geom::Abs;
use crate::model::Content;
use crate::util::NonZeroExt;
/// Construct the outline for the document.
pub fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
let mut tree: Vec<HeadingNode> = vec![];
for heading in ctx.introspector.query(&item!(heading_func).select()) {
let leaf = HeadingNode::leaf(heading);
if let Some(last) = tree.last_mut() {
if last.try_insert(leaf.clone(), NonZeroUsize::ONE) {
continue;
}
}
tree.push(leaf);
}
if tree.is_empty() {
return None;
}
let root_id = ctx.alloc.bump();
let start_ref = ctx.alloc;
let len = tree.len();
let mut prev_ref = None;
for (i, node) in tree.iter().enumerate() {
prev_ref = Some(write_outline_item(ctx, node, root_id, prev_ref, i + 1 == len));
}
ctx.writer
.outline(root_id)
.first(start_ref)
.last(Ref::new(ctx.alloc.get() - 1))
.count(tree.len() as i32);
Some(root_id)
}
/// A heading in the outline panel. /// A heading in the outline panel.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct HeadingNode { struct HeadingNode {
pub content: EcoString, element: Content,
pub level: usize, level: NonZeroUsize,
pub position: Point, children: Vec<HeadingNode>,
pub page: Ref,
pub children: Vec<HeadingNode>,
} }
impl HeadingNode { impl HeadingNode {
pub fn len(&self) -> usize { fn leaf(element: Content) -> Self {
HeadingNode {
level: element.expect_field::<NonZeroUsize>("level"),
element,
children: Vec::new(),
}
}
fn len(&self) -> usize {
1 + self.children.iter().map(Self::len).sum::<usize>() 1 + self.children.iter().map(Self::len).sum::<usize>()
} }
#[allow(unused)] fn try_insert(&mut self, child: Self, level: NonZeroUsize) -> bool {
pub fn try_insert(&mut self, child: Self, level: usize) -> bool {
if level >= child.level { if level >= child.level {
return false; return false;
} }
if let Some(last) = self.children.last_mut() { if let Some(last) = self.children.last_mut() {
if last.try_insert(child.clone(), level + 1) { if last.try_insert(child.clone(), level.saturating_add(1)) {
return true; return true;
} }
} }
@ -37,7 +81,7 @@ impl HeadingNode {
} }
/// Write an outline item and all its children. /// Write an outline item and all its children.
pub fn write_outline_item( fn write_outline_item(
ctx: &mut PdfContext, ctx: &mut PdfContext,
node: &HeadingNode, node: &HeadingNode,
parent_ref: Ref, parent_ref: Ref,
@ -65,12 +109,19 @@ pub fn write_outline_item(
outline.count(-(node.children.len() as i32)); outline.count(-(node.children.len() as i32));
} }
outline.title(TextStr(&node.content)); outline.title(TextStr(node.element.plain_text().trim()));
outline.dest_direct().page(node.page).xyz(
node.position.x.to_f32(), let loc = node.element.location().unwrap();
(node.position.y + Abs::pt(3.0)).to_f32(), let pos = ctx.introspector.position(loc);
let index = pos.page.get() - 1;
if let Some(&height) = ctx.page_heights.get(index) {
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
outline.dest_direct().page(ctx.page_refs[index]).xyz(
pos.point.x.to_f32(),
height - y.to_f32(),
None, None,
); );
}
outline.finish(); outline.finish();

View File

@ -96,16 +96,16 @@ pub fn analyze_labels(
// Labels in the document. // Labels in the document.
for elem in introspector.all() { for elem in introspector.all() {
let Some(label) = elem.label() else { continue }; let Some(label) = elem.label().cloned() else { continue };
let details = elem let details = elem
.field("caption") .field("caption")
.or_else(|| elem.field("body"))
.and_then(|field| match field { .and_then(|field| match field {
Value::Content(content) => Some(content), Value::Content(content) => Some(content),
_ => None, _ => None,
}) })
.and_then(|content| (items.text_str)(&content)); .unwrap_or(elem)
output.push((label.clone(), details)); .plain_text();
output.push((label, Some(details)));
} }
let split = output.len(); let split = output.len();

View File

@ -3,12 +3,12 @@ use std::fmt::{self, Debug, Formatter, Write};
use std::iter::Sum; use std::iter::Sum;
use std::ops::{Add, AddAssign}; use std::ops::{Add, AddAssign};
use comemo::{Prehashed, Tracked}; use comemo::Prehashed;
use ecow::{eco_format, EcoString, EcoVec}; use ecow::{eco_format, EcoString, EcoVec};
use super::{ use super::{
element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Introspector, Label, element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Label, Locatable,
Locatable, Location, Recipe, Selector, Style, Styles, Synthesize, Location, PlainText, Recipe, Selector, Style, Styles, Synthesize,
}; };
use crate::diag::{SourceResult, StrResult}; use crate::diag::{SourceResult, StrResult};
use crate::doc::Meta; use crate::doc::Meta;
@ -359,52 +359,53 @@ impl Content {
/// Queries the content tree for all elements that match the given selector. /// Queries the content tree for all elements that match the given selector.
/// ///
/// # Show rules
/// Elements produced in `show` rules will not be included in the results. /// Elements produced in `show` rules will not be included in the results.
pub fn query( pub fn query(&self, selector: Selector) -> Vec<&Content> {
&self,
introspector: Tracked<Introspector>,
selector: Selector,
) -> Vec<&Content> {
let mut results = Vec::new(); let mut results = Vec::new();
self.query_into(introspector, &selector, &mut results); self.traverse(&mut |element| {
if selector.matches(element) {
results.push(element);
}
});
results results
} }
/// Queries the content tree for all elements that match the given selector /// Extracts the plain text of this content.
/// and stores the results inside of the `results` vec. pub fn plain_text(&self) -> EcoString {
fn query_into<'a>( let mut text = EcoString::new();
&'a self, self.traverse(&mut |element| {
introspector: Tracked<Introspector>, if let Some(textable) = element.with::<dyn PlainText>() {
selector: &Selector, textable.plain_text(&mut text);
results: &mut Vec<&'a Content>,
) {
if selector.matches(self) {
results.push(self);
} }
});
text
}
/// Traverse this content.
fn traverse<'a, F>(&'a self, f: &mut F)
where
F: FnMut(&'a Content),
{
f(self);
for attr in &self.attrs { for attr in &self.attrs {
match attr { match attr {
Attr::Child(child) => child.query_into(introspector, selector, results), Attr::Child(child) => child.traverse(f),
Attr::Value(value) => walk_value(introspector, value, selector, results), Attr::Value(value) => walk_value(value, f),
_ => {} _ => {}
} }
} }
/// Walks a given value to find any content that matches the selector. /// Walks a given value to find any content that matches the selector.
fn walk_value<'a>( fn walk_value<'a, F>(value: &'a Value, f: &mut F)
introspector: Tracked<Introspector>, where
value: &'a Value, F: FnMut(&'a Content),
selector: &Selector, {
results: &mut Vec<&'a Content>,
) {
match value { match value {
Value::Content(content) => { Value::Content(content) => content.traverse(f),
content.query_into(introspector, selector, results)
}
Value::Array(array) => { Value::Array(array) => {
for value in array { for value in array {
walk_value(introspector, value, selector, results); walk_value(value, f);
} }
} }
_ => {} _ => {}

View File

@ -151,3 +151,9 @@ impl Debug for Label {
/// Indicates that an element cannot be labelled. /// Indicates that an element cannot be labelled.
pub trait Unlabellable {} pub trait Unlabellable {}
/// Tries to extract the plain-text representation of the element.
pub trait PlainText {
/// Write this element's plain text into the given buffer.
fn plain_text(&self, text: &mut EcoString);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -32,5 +32,5 @@ Ok ...
#set heading(numbering: "(I)") #set heading(numbering: "(I)")
= Zusammenfassung = #text(blue)[Zusammen]fassung
#lorem(10) #lorem(10)