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,
heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(),
heading_func: meta::HeadingElem::func(),
list_item: |body| layout::ListItem::new(body).pack(),
enum_item: |number, body| {
let mut elem = layout::EnumItem::new(body);

View File

@ -173,14 +173,14 @@ impl Synthesize for FigureElem {
// Determine the figure's kind.
let kind = match self.kind(styles) {
Smart::Auto => self
.find_figurable(vt, styles)
.find_figurable(styles)
.map(|elem| FigureKind::Elem(elem.func()))
.unwrap_or_else(|| FigureKind::Elem(ImageElem::func())),
Smart::Custom(kind) => 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,
}
.unwrap_or_else(|| self.body());
@ -303,9 +303,9 @@ impl Refable for FigureElem {
impl FigureElem {
/// Determines the type of the figure by looking at the content, finding all
/// [`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()
.query(vt.introspector, Selector::can::<dyn Figurable>())
.query(Selector::can::<dyn Figurable>())
.into_iter()
.max_by_key(|elem| elem.with::<dyn Figurable>().unwrap().priority(styles))
.cloned()
@ -313,9 +313,9 @@ impl FigureElem {
/// Finds the element with the given function in the figure's content.
/// 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()
.query(vt.introspector, Selector::Elem(func, None))
.query(Selector::Elem(func, None))
.into_iter()
.next()
.cloned()

View File

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

View File

@ -5,7 +5,7 @@ use crate::prelude::*;
///
/// Display: Space
/// Category: text
#[element(Unlabellable, Behave)]
#[element(Behave, Unlabellable, PlainText)]
pub struct SpaceElem {}
impl Behave for SpaceElem {
@ -16,6 +16,12 @@ impl Behave for SpaceElem {
impl Unlabellable for SpaceElem {}
impl PlainText for SpaceElem {
fn plain_text(&self, text: &mut EcoString) {
text.push(' ');
}
}
/// Inserts a line break.
///
/// 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
/// Category: text
#[element(Construct)]
#[element(Construct, PlainText)]
pub struct TextElem {
/// 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".
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct FontFamily(EcoString);

View File

@ -72,6 +72,8 @@ pub struct LangItems {
) -> Vec<(EcoString, Option<EcoString>)>,
/// A section heading: `= Introduction`.
pub heading: fn(level: NonZeroUsize, body: Content) -> Content,
/// The heading function.
pub heading_func: ElemFunc,
/// An item in a bullet list: `- ...`.
pub list_item: fn(body: Content) -> Content,
/// 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 xmp_writer::{LangId, RenditionClass, XmpWriter};
use self::outline::HeadingNode;
use self::page::Page;
use crate::doc::{Document, Lang};
use crate::font::Font;
@ -54,7 +53,6 @@ pub struct PdfContext<'a> {
image_map: Remapper<Image>,
glyph_sets: HashMap<Font, HashSet<u16>>,
languages: HashMap<Lang, usize>,
heading_tree: Vec<HeadingNode>,
}
impl<'a> PdfContext<'a> {
@ -76,36 +74,12 @@ impl<'a> PdfContext<'a> {
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()
@ -118,6 +92,9 @@ fn write_catalog(ctx: &mut PdfContext) {
Direction::L2R
};
// Write the outline tree.
let outline_root_id = outline::write_outline(ctx);
// Write the document information.
let mut info = ctx.writer.document_info(ctx.alloc.bump());
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 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.
#[derive(Debug, Clone)]
pub struct HeadingNode {
pub content: EcoString,
pub level: usize,
pub position: Point,
pub page: Ref,
pub children: Vec<HeadingNode>,
struct HeadingNode {
element: Content,
level: NonZeroUsize,
children: Vec<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>()
}
#[allow(unused)]
pub fn try_insert(&mut self, child: Self, level: usize) -> bool {
fn try_insert(&mut self, child: Self, level: NonZeroUsize) -> bool {
if level >= child.level {
return false;
}
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;
}
}
@ -37,7 +81,7 @@ impl HeadingNode {
}
/// Write an outline item and all its children.
pub fn write_outline_item(
fn write_outline_item(
ctx: &mut PdfContext,
node: &HeadingNode,
parent_ref: Ref,
@ -65,12 +109,19 @@ pub fn write_outline_item(
outline.count(-(node.children.len() as i32));
}
outline.title(TextStr(&node.content));
outline.dest_direct().page(node.page).xyz(
node.position.x.to_f32(),
(node.position.y + Abs::pt(3.0)).to_f32(),
None,
);
outline.title(TextStr(node.element.plain_text().trim()));
let loc = node.element.location().unwrap();
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,
);
}
outline.finish();

View File

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

View File

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