From 4640585fbdf72df993dbed46799844aa78996cce Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Sat, 4 Jun 2022 12:57:45 +0200 Subject: [PATCH 1/6] First iteration of outline items --- src/export/pdf.rs | 190 ++++++++++++++++++++++++++++++- src/frame.rs | 99 +++++++++++++++- src/library/graphics/hide.rs | 6 +- src/library/graphics/shape.rs | 3 + src/library/layout/flow.rs | 7 +- src/library/layout/grid.rs | 44 ++++++- src/library/layout/page.rs | 23 +++- src/library/layout/stack.rs | 3 + src/library/math/rex.rs | 1 + src/library/structure/heading.rs | 12 +- src/library/structure/list.rs | 20 +++- src/library/structure/table.rs | 16 ++- src/library/text/par.rs | 5 + src/library/text/raw.rs | 2 +- src/model/layout.rs | 11 +- src/model/styles.rs | 28 ++++- 16 files changed, 445 insertions(+), 25 deletions(-) diff --git a/src/export/pdf.rs b/src/export/pdf.rs index aa7acd41d..843e6f374 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -16,7 +16,7 @@ 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, @@ -313,6 +313,8 @@ impl<'a> PdfExporter<'a> { } let mut languages = HashMap::new(); + let mut heading_tree: Vec = vec![]; + for (page, page_id) in self.pages.into_iter().zip(page_refs.iter()) { let content_id = self.alloc.bump(); @@ -356,6 +358,20 @@ impl<'a> PdfExporter<'a> { .or_insert_with(|| count); } + for heading in page.headings.into_iter() { + if let Some(last) = heading_tree.pop() { + let new = last.clone().insert(heading.clone(), *page_id, 1); + if let Some(new) = new { + heading_tree.push(new); + } else { + heading_tree.push(last); + heading_tree.push(HeadingNode::Leaf(heading, *page_id)) + } + } else { + heading_tree.push(HeadingNode::Leaf(heading, *page_id)) + } + } + self.writer .stream(content_id, &deflate(&page.content.finish())) .filter(Filter::FlateDecode); @@ -388,6 +404,39 @@ impl<'a> PdfExporter<'a> { resources.finish(); pages.finish(); + // Build the heading tree. + let outline_root_id = self.alloc.bump(); + + let start_ref = self.alloc.bump(); + let mut current_ref = start_ref; + let mut prev_ref = None; + + for (i, node) in heading_tree.iter().enumerate() { + let next = write_outline_item( + &mut self.writer, + node, + current_ref, + prev_ref, + i == heading_tree.len() - 1, + outline_root_id, + ); + prev_ref = Some(current_ref); + current_ref = next; + } + + + self.alloc = Ref::new( + start_ref.get() + + heading_tree.iter().map(HeadingNode::len).sum::() as i32, + ); + + if let Some(prev_ref) = prev_ref { + let mut outline_root = self.writer.outline(outline_root_id); + outline_root.first(start_ref); + outline_root.last(prev_ref); + outline_root.count(heading_tree.len() as i32); + } + let lang = languages .into_iter() .max_by(|(_, v1), (_, v2)| v1.cmp(v2)) @@ -405,6 +454,11 @@ impl<'a> PdfExporter<'a> { catalog.pages(page_tree_ref); catalog.viewer_preferences().direction(dir); + + if !heading_tree.is_empty() { + catalog.outlines(outline_root_id); + } + if let Some(lang) = lang { catalog.lang(TextStr(lang.as_str())); } @@ -426,6 +480,7 @@ struct PageExporter<'a> { links: Vec<(Destination, Rect)>, state: State, saves: Vec, + headings: Vec, } /// Data for an exported page. @@ -434,6 +489,7 @@ struct Page { content: Content, links: Vec<(Destination, Rect)>, languages: HashMap, + headings: Vec, } /// A simulated graphics state used to deduplicate graphics state changes and @@ -448,6 +504,64 @@ struct State { stroke_space: Option>, } +/// A heading that can later be linked in the outline panel. +#[derive(Debug, Clone)] +struct Heading { + content: String, + level: usize, + position: Point, +} + +#[derive(Debug, Clone)] +enum HeadingNode { + Leaf(Heading, Ref), + Branch(Heading, Ref, Vec), +} + +impl HeadingNode { + fn heading(&self) -> &Heading { + match self { + HeadingNode::Leaf(h, _) => h, + HeadingNode::Branch(h, _, _) => h, + } + } + + fn reference(&self) -> Ref { + match self { + HeadingNode::Leaf(_, r) => *r, + HeadingNode::Branch(_, r, _) => *r, + } + } + + fn len(&self) -> usize { + match self { + HeadingNode::Leaf(_, _) => 1, + HeadingNode::Branch(_, _, children) => { + 1 + children.iter().map(|c| c.len()).sum::() + } + } + } + + fn insert(self, other: Heading, page: Ref, level: usize) -> Option { + if level >= other.level { + return None; + } + + let mut node = match self { + HeadingNode::Leaf(h, r) => (h, r, vec![]), + HeadingNode::Branch(h, r, v) if level + 1 == other.level => (h, r, v), + HeadingNode::Branch(h, r, mut v) => { + let new = v.pop().unwrap().insert(other, page, level + 1).unwrap(); + v.push(new); + return Some(HeadingNode::Branch(h, r, v)); + } + }; + + node.2.push(HeadingNode::Leaf(other, page)); + Some(HeadingNode::Branch(node.0, node.1, node.2)) + } +} + impl<'a> PageExporter<'a> { fn new(exporter: &'a mut PdfExporter) -> Self { Self { @@ -461,6 +575,7 @@ impl<'a> PageExporter<'a> { links: vec![], state: State::default(), saves: vec![], + headings: vec![], } } @@ -481,10 +596,22 @@ impl<'a> PageExporter<'a> { content: self.content, links: self.links, languages: self.languages, + headings: self.headings, } } fn write_frame(&mut self, frame: &Frame) { + if let Some(Role::Heading(level)) = frame.role() { + self.headings.push(Heading { + position: Point::new( + self.state.transform.tx, + self.state.transform.ty + Length::pt(3.0), + ), + content: frame.inner_text().to_string(), + level, + }) + } + for &(pos, ref element) in &frame.elements { let x = pos.x.to_f32(); let y = pos.y.to_f32(); @@ -815,6 +942,67 @@ fn encode_image(img: &RasterImage) -> ImageResult<(Vec, Filter, bool)> { }) } +fn write_outline_item( + writer: &mut PdfWriter, + node: &HeadingNode, + current_ref: Ref, + prev_ref: Option, + is_last: bool, + parent_ref: Ref, +) -> Ref { + let mut outline = writer.outline_item(current_ref); + let next = Ref::new(current_ref.get() + node.len() as i32); + outline.parent(parent_ref); + + if !is_last { + outline.next(next); + } + + if let Some(prev_ref) = prev_ref { + outline.prev(prev_ref); + } + + if let HeadingNode::Branch(_, _, children) = node { + let current_child = Ref::new(current_ref.get() + 1); + if children.len() > 0 { + outline.first(current_child); + outline.last(Ref::new(next.get() - 1)); + } + + outline.count(-1 * children.len() as i32); + } + + let heading = node.heading(); + outline.title(TextStr(&heading.content)); + outline.dest_direct().page(node.reference()).xyz( + heading.position.x.to_f32(), + heading.position.y.to_f32(), + None, + ); + + outline.finish(); + + if let HeadingNode::Branch(_, _, children) = node { + let mut current_child = Ref::new(current_ref.get() + 1); + let mut prev_ref = None; + + for (i, child) in children.iter().enumerate() { + write_outline_item( + writer, + child, + current_child, + prev_ref, + i == children.len() - 1, + current_ref, + ); + prev_ref = Some(current_child); + current_child = Ref::new(current_child.get() + 1); + } + } + + next +} + /// Encode an image's alpha channel if present. fn encode_alpha(img: &RasterImage) -> (Vec, Filter) { let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); diff --git a/src/frame.rs b/src/frame.rs index 1bd1f4548..8b14b2b16 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -22,6 +22,8 @@ pub struct Frame { pub baseline: Option, /// The elements composing this layout. pub elements: Vec<(Point, Element)>, + /// The semantic role of the frame. + role: Option, } impl Frame { @@ -29,7 +31,12 @@ impl Frame { #[track_caller] pub fn new(size: Size) -> Self { assert!(size.is_finite()); - Self { size, baseline: None, elements: vec![] } + Self { + size, + baseline: None, + elements: vec![], + role: None, + } } /// The baseline of the frame. @@ -43,6 +50,11 @@ impl Frame { self.elements.len() } + /// The role of the frame. + pub fn role(&self) -> Option { + self.role + } + /// Whether the frame has comparatively few elements. pub fn is_light(&self) -> bool { self.elements.len() <= 5 @@ -58,7 +70,12 @@ impl Frame { /// Automatically decides whether to inline the frame or to include it as a /// group based on the number of elements in the frame. pub fn push_frame(&mut self, pos: Point, frame: impl FrameRepr) { - if self.elements.is_empty() || frame.as_ref().is_light() { + if (self.elements.is_empty() || frame.as_ref().is_light()) + && (frame.as_ref().role().is_none() || self.role.is_none()) + { + if self.role.is_none() { + self.role = frame.as_ref().role() + } frame.inline(self, self.layer(), pos); } else { self.elements.push((pos, Element::Group(Group::new(frame.share())))); @@ -80,7 +97,12 @@ impl Frame { /// Add a frame at a position in the background. pub fn prepend_frame(&mut self, pos: Point, frame: impl FrameRepr) { - if self.elements.is_empty() || frame.as_ref().is_light() { + if (self.elements.is_empty() || frame.as_ref().is_light()) + && (frame.as_ref().role().is_none() || self.role.is_none()) + { + if self.role.is_none() { + self.role = frame.as_ref().role() + } frame.inline(self, 0, pos); } else { self.elements @@ -125,6 +147,15 @@ impl Frame { self.group(|g| g.transform = transform); } + /// Apply the given role to the frame if it doesn't already have one. + pub fn apply_role(&mut self, role: Role) { + match self.role { + None => self.role = Some(role), + Some(old) if old.is_weak() => self.role = Some(role), + Some(_) => {} + } + } + /// Clip the contents of a frame to its size. pub fn clip(&mut self) { self.group(|g| g.clips = true); @@ -146,10 +177,26 @@ impl Frame { pub fn link(&mut self, dest: Destination) { self.push(Point::zero(), Element::Link(dest, self.size)); } + + /// Recover the text inside of the frame and its children. + pub fn inner_text(&self) -> EcoString { + let mut res = EcoString::new(); + for (_, element) in &self.elements { + match element { + Element::Text(text) => res.push_str( + &text.glyphs.iter().map(|glyph| glyph.c).collect::(), + ), + Element::Group(group) => res.push_str(&group.frame.inner_text()), + _ => {} + } + } + res + } } impl Debug for Frame { fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.role.fmt(f)?; f.debug_list() .entries(self.elements.iter().map(|(_, element)| element)) .finish() @@ -362,3 +409,49 @@ impl Location { } } } + +/// A semantic role of a frame. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Role { + /// A paragraph. + Paragraph, + /// A heading with some level. + Heading(usize), + /// A generic block-level subdivision. + GenericBlock, + /// A generic inline subdivision. + GenericInline, + /// A list. The boolean indicates whether it is ordered. + List(bool), + /// A list item. Must have a list parent. + ListItem, + /// The label of a list item. + ListLabel, + /// The body of a list item. + ListItemBody, + /// A mathematical formula. + Formula, + /// A table. + Table, + /// A table row. + TableRow, + /// A table cell. + TableCell, + /// A code fragment. + Code, + /// A page header. + Header, + /// A page footer. + Footer, + /// A page background. + Background, +} + +impl Role { + fn is_weak(&self) -> bool { + match self { + Self::Paragraph | Self::GenericBlock | Self::GenericInline => true, + _ => false, + } + } +} diff --git a/src/library/graphics/hide.rs b/src/library/graphics/hide.rs index 28afe3204..4ba5e023b 100644 --- a/src/library/graphics/hide.rs +++ b/src/library/graphics/hide.rs @@ -22,7 +22,11 @@ impl Layout for HideNode { // Clear the frames. for frame in &mut frames { - *frame = Arc::new(Frame { elements: vec![], ..**frame }); + *frame = Arc::new({ + let mut empty = Frame::new(frame.size); + empty.baseline = frame.baseline; + empty + }); } Ok(frames) diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index 9da8d8df6..8070231e4 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -91,6 +91,9 @@ impl Layout for ShapeNode { let child = child.clone().padded(inset.map(|side| side.map(RawLength::from))); let mut pod = Regions::one(regions.first, regions.base, regions.expand); + let role_map = StyleMap::with_role(Role::GenericBlock); + let styles = role_map.chain(&styles); + frames = child.layout(ctx, &pod, styles)?; // Relayout with full expansion into square region to make sure diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs index 6193a68f6..0ba84b091 100644 --- a/src/library/layout/flow.rs +++ b/src/library/layout/flow.rs @@ -182,7 +182,12 @@ impl FlowLayouter { let frames = node.layout(ctx, &self.regions, styles)?; let len = frames.len(); - for (i, frame) in frames.into_iter().enumerate() { + for (i, mut frame) in frames.into_iter().enumerate() { + // Set the generic block role. + if frame.role().is_none() { + Arc::make_mut(&mut frame).apply_role(Role::GenericBlock); + } + // Grow our size, shrink the region and save the frame for later. let size = frame.size; self.used.y += size.y; diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs index 4cad9de68..2517d1930 100644 --- a/src/library/layout/grid.rs +++ b/src/library/layout/grid.rs @@ -9,6 +9,8 @@ pub struct GridNode { pub gutter: Spec>, /// The nodes to be arranged in a grid. pub cells: Vec, + /// The role of the grid in the semantic tree. + pub semantic: GridSemantics, } #[node] @@ -26,6 +28,7 @@ impl GridNode { row_gutter.unwrap_or(base_gutter), ), cells: args.all()?, + semantic: GridSemantics::None, })) } } @@ -45,6 +48,7 @@ impl Layout for GridNode { &self.cells, regions, styles, + self.semantic, ); // Measure the columns and layout the grid row-by-row. @@ -65,6 +69,28 @@ pub enum TrackSizing { Fractional(Fraction), } +/// Defines what kind of semantics a grid should represent. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum GridSemantics { + /// The grid is transparent to the semantic tree. + None, + /// The grid is a list, its rows are list items. The bool indicates whether + /// the list is ordered. + List, + /// The grid is a table. + Table, +} + +impl GridSemantics { + fn row(self) -> Option { + match self { + Self::None => None, + Self::List => Some(Role::ListItem), + Self::Table => Some(Role::TableRow), + } + } +} + castable! { Vec, Expected: "integer, auto, relative length, fraction, or array of the latter three)", @@ -104,6 +130,8 @@ pub struct GridLayouter<'a> { regions: Regions, /// The inherited styles. styles: StyleChain<'a>, + /// The role of the grid in the semantic tree. + semantic: GridSemantics, /// Resolved column sizes. rcols: Vec, /// Rows in the current region. @@ -139,6 +167,7 @@ impl<'a> GridLayouter<'a> { cells: &'a [LayoutNode], regions: &Regions, styles: StyleChain<'a>, + semantic: GridSemantics, ) -> Self { let mut cols = vec![]; let mut rows = vec![]; @@ -193,6 +222,7 @@ impl<'a> GridLayouter<'a> { rows, regions, styles, + semantic, rcols, lrows, full, @@ -450,6 +480,10 @@ impl<'a> GridLayouter<'a> { /// Layout a row with fixed height and return its frame. fn layout_single_row(&mut self, height: Length, y: usize) -> TypResult { let mut output = Frame::new(Size::new(self.used.x, height)); + if let Some(role) = self.semantic.row() { + output.apply_role(role); + } + let mut pos = Point::zero(); for (x, &rcol) in self.rcols.iter().enumerate() { @@ -464,6 +498,7 @@ impl<'a> GridLayouter<'a> { let pod = Regions::one(size, base, Spec::splat(true)); let frame = node.layout(self.ctx, &pod, self.styles)?.remove(0); + output.push_frame(pos, frame); } @@ -482,7 +517,14 @@ impl<'a> GridLayouter<'a> { // Prepare frames. let mut outputs: Vec<_> = heights .iter() - .map(|&h| Frame::new(Size::new(self.used.x, h))) + .map(|&h| { + let mut f = Frame::new(Size::new(self.used.x, h)); + if let Some(role) = self.semantic.row() { + f.apply_role(role); + } + + f + }) .collect(); // Prepare regions. diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs index 8435e5103..d524839b9 100644 --- a/src/library/layout/page.rs +++ b/src/library/layout/page.rs @@ -110,15 +110,28 @@ impl PageNode { let pad = padding.resolve(styles).relative_to(size); let pw = size.x - pad.left - pad.right; let py = size.y - pad.bottom; - for (marginal, pos, area) in [ - (header, Point::with_x(pad.left), Size::new(pw, pad.top)), - (footer, Point::new(pad.left, py), Size::new(pw, pad.bottom)), - (foreground, Point::zero(), size), - (background, Point::zero(), size), + for (marginal, pos, area, role) in [ + ( + header, + Point::with_x(pad.left), + Size::new(pw, pad.top), + Role::Header, + ), + ( + footer, + Point::new(pad.left, py), + Size::new(pw, pad.bottom), + Role::Footer, + ), + (foreground, Point::zero(), size, Role::Background), + (background, Point::zero(), size, Role::Background), ] { if let Some(content) = marginal.resolve(ctx, page)? { let pod = Regions::one(area, area, Spec::splat(true)); + let role_map = StyleMap::with_role(role); + let styles = role_map.chain(&styles); let sub = content.layout(ctx, &pod, styles)?.remove(0); + if std::ptr::eq(marginal, background) { Arc::make_mut(frame).prepend_frame(pos, sub); } else { diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs index 828ff8e3e..7bad01d99 100644 --- a/src/library/layout/stack.rs +++ b/src/library/layout/stack.rs @@ -192,6 +192,9 @@ impl<'a> StackLayouter<'a> { self.dir.start().into() }); + let role_map = StyleMap::with_role(Role::GenericBlock); + let styles = role_map.chain(&styles); + let frames = node.layout(ctx, &self.regions, styles)?; let len = frames.len(); for (i, frame) in frames.into_iter().enumerate() { diff --git a/src/library/math/rex.rs b/src/library/math/rex.rs index 0268fb9c8..f839a9e83 100644 --- a/src/library/math/rex.rs +++ b/src/library/math/rex.rs @@ -66,6 +66,7 @@ impl Layout for RexNode { let mut backend = FrameBackend { frame: { let mut frame = Frame::new(size); + frame.apply_role(Role::Formula); frame.baseline = Some(baseline); frame }, diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs index a0973b90a..285793dd0 100644 --- a/src/library/structure/heading.rs +++ b/src/library/structure/heading.rs @@ -1,6 +1,7 @@ use crate::library::layout::BlockSpacing; use crate::library::prelude::*; use crate::library::text::{FontFamily, TextNode, TextSize}; +use crate::model::StyleEntry; /// A section heading. #[derive(Debug, Hash)] @@ -65,7 +66,13 @@ impl HeadingNode { impl Show for HeadingNode { fn unguard(&self, sel: Selector) -> ShowNode { - Self { body: self.body.unguard(sel), ..*self }.pack() + let mut map = StyleMap::with_role(Role::Heading(self.level.get())); + map.push(StyleEntry::Unguard(sel)); + Self { + body: self.body.clone().styled_with_map(map), + ..*self + } + .pack() } fn encode(&self, _: StyleChain) -> Dict { @@ -91,7 +98,8 @@ impl Show for HeadingNode { }; } - let mut map = StyleMap::new(); + let mut map = StyleMap::with_role(Role::Heading(self.level.get())); + map.set(TextNode::SIZE, resolve!(Self::SIZE)); if let Smart::Custom(family) = resolve!(Self::FAMILY) { diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs index 84603eb3b..563426b4e 100644 --- a/src/library/structure/list.rs +++ b/src/library/structure/list.rs @@ -2,10 +2,11 @@ use std::fmt::Write; use unscanny::Scanner; -use crate::library::layout::{BlockSpacing, GridNode, TrackSizing}; +use crate::library::layout::{BlockSpacing, GridNode, GridSemantics, TrackSizing}; use crate::library::prelude::*; use crate::library::text::ParNode; use crate::library::utility::Numbering; +use crate::model::StyleEntry; /// An unordered (bulleted) or ordered (numbered) list. #[derive(Debug, Hash)] @@ -76,9 +77,12 @@ impl ListNode { impl Show for ListNode { fn unguard(&self, sel: Selector) -> ShowNode { + let mut map = StyleMap::with_role(Role::ListItemBody); + map.push(StyleEntry::Unguard(sel)); + Self { items: self.items.map(|item| ListItem { - body: Box::new(item.body.unguard(sel)), + body: Box::new(item.body.clone().styled_with_map(map.clone())), ..*item }), ..*self @@ -108,9 +112,12 @@ impl Show for ListNode { for (item, map) in self.items.iter() { number = item.number.unwrap_or(number); + + let mut label_map = map.clone(); + label_map.push(StyleEntry::Role(Role::ListLabel)); + cells.push(LayoutNode::default()); - cells - .push(label.resolve(ctx, L, number)?.styled_with_map(map.clone()).pack()); + cells.push(label.resolve(ctx, L, number)?.styled_with_map(label_map).pack()); cells.push(LayoutNode::default()); cells.push((*item.body).clone().styled_with_map(map.clone()).pack()); number += 1; @@ -134,6 +141,7 @@ impl Show for ListNode { ]), gutter: Spec::with_y(vec![TrackSizing::Relative(gutter.into())]), cells, + semantic: GridSemantics::List, })) } @@ -155,7 +163,9 @@ impl Show for ListNode { } } - Ok(realized.spaced(above, below)) + Ok(realized + .styled_with_map(StyleMap::with_role(Role::List(L == ORDERED))) + .spaced(above, below)) } } diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs index cd70db306..601156126 100644 --- a/src/library/structure/table.rs +++ b/src/library/structure/table.rs @@ -1,5 +1,6 @@ -use crate::library::layout::{BlockSpacing, GridNode, TrackSizing}; +use crate::library::layout::{BlockSpacing, GridNode, GridSemantics, TrackSizing}; use crate::library::prelude::*; +use crate::model::StyleEntry; /// A table of items. #[derive(Debug, Hash)] @@ -49,10 +50,17 @@ impl TableNode { impl Show for TableNode { fn unguard(&self, sel: Selector) -> ShowNode { + let mut map = StyleMap::with_role(Role::TableCell); + map.push(StyleEntry::Unguard(sel)); + Self { tracks: self.tracks.clone(), gutter: self.gutter.clone(), - cells: self.cells.iter().map(|cell| cell.unguard(sel)).collect(), + cells: self + .cells + .iter() + .map(|cell| cell.clone().styled_with_map(map.clone())) + .collect(), } .pack() } @@ -100,7 +108,9 @@ impl Show for TableNode { tracks: self.tracks.clone(), gutter: self.gutter.clone(), cells, - })) + semantic: GridSemantics::Table, + }) + .styled_with_map(StyleMap::with_role(Role::Table))) } fn finalize( diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 695d80667..53bb798f5 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -551,6 +551,9 @@ fn prepare<'a>( } else { let size = Size::new(regions.first.x, regions.base.y); let pod = Regions::one(size, regions.base, Spec::splat(false)); + let role_map = StyleMap::with_role(Role::GenericInline); + let styles = role_map.chain(&styles); + let mut frame = node.layout(ctx, &pod, styles)?.remove(0); let shift = styles.get(TextNode::BASELINE); @@ -1063,6 +1066,7 @@ fn stack( let mut finished = vec![]; let mut first = true; let mut output = Frame::new(Size::with_x(width)); + output.apply_role(Role::Paragraph); // Stack the lines into one frame per region. for line in lines { @@ -1072,6 +1076,7 @@ fn stack( while !regions.first.y.fits(height) && !regions.in_last() { finished.push(Arc::new(output)); output = Frame::new(Size::with_x(width)); + output.apply_role(Role::Paragraph); regions.next(); first = true; } diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs index a24d21704..4d73b11bf 100644 --- a/src/library/text/raw.rs +++ b/src/library/text/raw.rs @@ -113,7 +113,7 @@ impl Show for RawNode { styles: StyleChain, mut realized: Content, ) -> TypResult { - let mut map = StyleMap::new(); + let mut map = StyleMap::with_role(Role::Code); map.set_family(styles.get(Self::FAMILY).clone(), styles); map.set(TextNode::OVERHANG, false); map.set(TextNode::HYPHENATE, Smart::Custom(Hyphenate(false))); diff --git a/src/model/layout.rs b/src/model/layout.rs index b0247258a..b4151c046 100644 --- a/src/model/layout.rs +++ b/src/model/layout.rs @@ -232,7 +232,16 @@ impl Layout for LayoutNode { let at = ctx.pins.cursor(); let entry = StyleEntry::Barrier(Barrier::new(node.id())); - let result = node.0.layout(ctx, regions, entry.chain(&styles)); + let mut result = node.0.layout(ctx, regions, entry.chain(&styles)); + + if let Some(role) = styles.role() { + result = result.map(|mut frames| { + for frame in frames.iter_mut() { + Arc::make_mut(frame).apply_role(role); + } + frames + }); + } let fresh = ctx.pins.from(at); let dirty = ctx.pins.dirty.get(); diff --git a/src/model/styles.rs b/src/model/styles.rs index 9e723171f..7db2df42f 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -5,6 +5,7 @@ use std::marker::PhantomData; use super::{Barrier, Content, Key, Property, Recipe, Selector, Show, Target}; use crate::diag::TypResult; +use crate::frame::Role; use crate::library::text::{FontFamily, TextNode}; use crate::util::ReadableTypeId; use crate::Context; @@ -36,6 +37,13 @@ impl StyleMap { styles } + /// Create a style map from a single role. + pub fn with_role(role: Role) -> Self { + let mut styles = Self::new(); + styles.push(StyleEntry::Role(role)); + styles + } + /// Set an inner value for a style property. /// /// If the property needs folding and the value is already contained in the @@ -170,6 +178,8 @@ pub enum StyleEntry { Property(Property), /// A show rule recipe. Recipe(Recipe), + /// A semantic role. + Role(Role), /// A barrier for scoped styles. Barrier(Barrier), /// Guards against recursive show rules. @@ -229,6 +239,7 @@ impl Debug for StyleEntry { match self { Self::Property(property) => property.fmt(f)?, Self::Recipe(recipe) => recipe.fmt(f)?, + Self::Role(role) => role.fmt(f)?, Self::Barrier(barrier) => barrier.fmt(f)?, Self::Guard(sel) => write!(f, "Guard against {sel:?}")?, Self::Unguard(sel) => write!(f, "Unguard against {sel:?}")?, @@ -324,8 +335,23 @@ impl<'a> StyleChain<'a> { Ok(realized) } + /// Retrieve the current role + pub fn role(self) -> Option { + let mut depth = 0; + + for entry in self.entries() { + match *entry { + StyleEntry::Role(role) => return Some(role), + StyleEntry::Barrier(_) if depth == 1 => return None, + StyleEntry::Barrier(_) => depth += 1, + _ => {} + } + } + None + } + /// Whether the recipe identified by the selector is guarded. - fn guarded(&self, sel: Selector) -> bool { + fn guarded(self, sel: Selector) -> bool { for entry in self.entries() { match *entry { StyleEntry::Guard(s) if s == sel => return true, From 6d8b65c4b24206a1482ea143791d7a1c410a4313 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Wed, 8 Jun 2022 12:30:10 +0200 Subject: [PATCH 2/6] More consistent role application --- src/frame.rs | 4 +++- src/library/graphics/shape.rs | 9 ++++++--- src/library/layout/flow.rs | 2 +- src/library/layout/page.rs | 3 ++- src/library/layout/stack.rs | 10 ++++++---- src/library/structure/heading.rs | 9 ++------- src/library/structure/list.rs | 20 +++++++++----------- src/library/structure/table.rs | 7 ++----- src/library/text/par.rs | 9 +++++---- src/library/text/raw.rs | 4 ++-- src/model/content.rs | 5 +++++ 11 files changed, 43 insertions(+), 39 deletions(-) diff --git a/src/frame.rs b/src/frame.rs index 8b14b2b16..0a8a20548 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -448,7 +448,9 @@ pub enum Role { } impl Role { - fn is_weak(&self) -> bool { + /// Whether the role describes a generic element and is not very + /// descriptive. + pub fn is_weak(self) -> bool { match self { Self::Paragraph | Self::GenericBlock | Self::GenericInline => true, _ => false, diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index 8070231e4..e972a3d52 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -91,11 +91,14 @@ impl Layout for ShapeNode { let child = child.clone().padded(inset.map(|side| side.map(RawLength::from))); let mut pod = Regions::one(regions.first, regions.base, regions.expand); - let role_map = StyleMap::with_role(Role::GenericBlock); - let styles = role_map.chain(&styles); - frames = child.layout(ctx, &pod, styles)?; + for frame in frames.iter_mut() { + if frame.role().map_or(true, Role::is_weak) { + Arc::make_mut(frame).apply_role(Role::GenericBlock); + } + } + // Relayout with full expansion into square region to make sure // the result is really a square or circle. if is_quadratic(S) { diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs index 0ba84b091..f779c8b14 100644 --- a/src/library/layout/flow.rs +++ b/src/library/layout/flow.rs @@ -184,7 +184,7 @@ impl FlowLayouter { let len = frames.len(); for (i, mut frame) in frames.into_iter().enumerate() { // Set the generic block role. - if frame.role().is_none() { + if frame.role().map_or(true, Role::is_weak) { Arc::make_mut(&mut frame).apply_role(Role::GenericBlock); } diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs index d524839b9..8bd507c43 100644 --- a/src/library/layout/page.rs +++ b/src/library/layout/page.rs @@ -130,7 +130,8 @@ impl PageNode { let pod = Regions::one(area, area, Spec::splat(true)); let role_map = StyleMap::with_role(role); let styles = role_map.chain(&styles); - let sub = content.layout(ctx, &pod, styles)?.remove(0); + let mut sub = content.layout(ctx, &pod, styles)?.remove(0); + Arc::make_mut(&mut sub).apply_role(role); if std::ptr::eq(marginal, background) { Arc::make_mut(frame).prepend_frame(pos, sub); diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs index 7bad01d99..9c2cbccd4 100644 --- a/src/library/layout/stack.rs +++ b/src/library/layout/stack.rs @@ -192,12 +192,14 @@ impl<'a> StackLayouter<'a> { self.dir.start().into() }); - let role_map = StyleMap::with_role(Role::GenericBlock); - let styles = role_map.chain(&styles); - let frames = node.layout(ctx, &self.regions, styles)?; let len = frames.len(); - for (i, frame) in frames.into_iter().enumerate() { + for (i, mut frame) in frames.into_iter().enumerate() { + // Set the generic block role. + if frame.role().map_or(true, Role::is_weak) { + Arc::make_mut(&mut frame).apply_role(Role::GenericBlock); + } + // Grow our size, shrink the region and save the frame for later. let size = frame.size.to_gen(self.axis); self.used.main += size.main; diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs index 285793dd0..a376bf095 100644 --- a/src/library/structure/heading.rs +++ b/src/library/structure/heading.rs @@ -66,13 +66,8 @@ impl HeadingNode { impl Show for HeadingNode { fn unguard(&self, sel: Selector) -> ShowNode { - let mut map = StyleMap::with_role(Role::Heading(self.level.get())); - map.push(StyleEntry::Unguard(sel)); - Self { - body: self.body.clone().styled_with_map(map), - ..*self - } - .pack() + let body = self.body.unguard(sel).role(Role::Heading(self.level.get())); + Self { body, ..*self }.pack() } fn encode(&self, _: StyleChain) -> Dict { diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs index 563426b4e..1c0e251f7 100644 --- a/src/library/structure/list.rs +++ b/src/library/structure/list.rs @@ -77,12 +77,9 @@ impl ListNode { impl Show for ListNode { fn unguard(&self, sel: Selector) -> ShowNode { - let mut map = StyleMap::with_role(Role::ListItemBody); - map.push(StyleEntry::Unguard(sel)); - Self { items: self.items.map(|item| ListItem { - body: Box::new(item.body.clone().styled_with_map(map.clone())), + body: Box::new(item.body.unguard(sel).role(Role::ListItemBody)), ..*item }), ..*self @@ -113,11 +110,14 @@ impl Show for ListNode { for (item, map) in self.items.iter() { number = item.number.unwrap_or(number); - let mut label_map = map.clone(); - label_map.push(StyleEntry::Role(Role::ListLabel)); - cells.push(LayoutNode::default()); - cells.push(label.resolve(ctx, L, number)?.styled_with_map(label_map).pack()); + cells.push( + label + .resolve(ctx, L, number)? + .styled_with_map(map.clone()) + .role(Role::ListLabel) + .pack(), + ); cells.push(LayoutNode::default()); cells.push((*item.body).clone().styled_with_map(map.clone()).pack()); number += 1; @@ -163,9 +163,7 @@ impl Show for ListNode { } } - Ok(realized - .styled_with_map(StyleMap::with_role(Role::List(L == ORDERED))) - .spaced(above, below)) + Ok(realized.role(Role::List(L == ORDERED)).spaced(above, below)) } } diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs index 601156126..118e48ca3 100644 --- a/src/library/structure/table.rs +++ b/src/library/structure/table.rs @@ -50,16 +50,13 @@ impl TableNode { impl Show for TableNode { fn unguard(&self, sel: Selector) -> ShowNode { - let mut map = StyleMap::with_role(Role::TableCell); - map.push(StyleEntry::Unguard(sel)); - Self { tracks: self.tracks.clone(), gutter: self.gutter.clone(), cells: self .cells .iter() - .map(|cell| cell.clone().styled_with_map(map.clone())) + .map(|cell| cell.unguard(sel).role(Role::TableCell)) .collect(), } .pack() @@ -110,7 +107,7 @@ impl Show for TableNode { cells, semantic: GridSemantics::Table, }) - .styled_with_map(StyleMap::with_role(Role::Table))) + .role(Role::Table)) } fn finalize( diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 53bb798f5..0fec11ad8 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -551,14 +551,15 @@ fn prepare<'a>( } else { let size = Size::new(regions.first.x, regions.base.y); let pod = Regions::one(size, regions.base, Spec::splat(false)); - let role_map = StyleMap::with_role(Role::GenericInline); - let styles = role_map.chain(&styles); let mut frame = node.layout(ctx, &pod, styles)?.remove(0); let shift = styles.get(TextNode::BASELINE); - if !shift.is_zero() { - Arc::make_mut(&mut frame).translate(Point::with_y(shift)); + if !shift.is_zero() || frame.role().map_or(true, Role::is_weak) { + let frame = Arc::make_mut(&mut frame); + + frame.translate(Point::with_y(shift)); + frame.apply_role(Role::GenericInline); } items.push(Item::Frame(frame)); diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs index 4d73b11bf..31db811a4 100644 --- a/src/library/text/raw.rs +++ b/src/library/text/raw.rs @@ -113,7 +113,7 @@ impl Show for RawNode { styles: StyleChain, mut realized: Content, ) -> TypResult { - let mut map = StyleMap::with_role(Role::Code); + let mut map = StyleMap::new(); map.set_family(styles.get(Self::FAMILY).clone(), styles); map.set(TextNode::OVERHANG, false); map.set(TextNode::HYPHENATE, Smart::Custom(Hyphenate(false))); @@ -123,7 +123,7 @@ impl Show for RawNode { realized = realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)); } - Ok(realized.styled_with_map(map)) + Ok(realized.styled_with_map(map).role(Role::Code)) } } diff --git a/src/model/content.rs b/src/model/content.rs index 21bf83695..2b9eb182a 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -204,6 +204,11 @@ impl Content { Self::Styled(Arc::new((self, styles))) } + /// Assign a role to this content by adding a style map. + pub fn role(self, role: Role) -> Self { + self.styled_with_map(StyleMap::with_role(role)) + } + /// Reenable the show rule identified by the selector. pub fn unguard(&self, sel: Selector) -> Self { self.clone().styled_with_entry(StyleEntry::Unguard(sel)) From 995a7882d2f8455429c368b97e699eca59359c44 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Wed, 8 Jun 2022 13:11:51 +0200 Subject: [PATCH 3/6] Auto-detect grid row semantics --- src/library/layout/grid.rs | 49 +++++++++++++++++++------------- src/library/structure/heading.rs | 1 - src/library/structure/list.rs | 4 +-- src/library/structure/table.rs | 4 +-- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs index 2517d1930..687091fff 100644 --- a/src/library/layout/grid.rs +++ b/src/library/layout/grid.rs @@ -9,8 +9,6 @@ pub struct GridNode { pub gutter: Spec>, /// The nodes to be arranged in a grid. pub cells: Vec, - /// The role of the grid in the semantic tree. - pub semantic: GridSemantics, } #[node] @@ -28,7 +26,6 @@ impl GridNode { row_gutter.unwrap_or(base_gutter), ), cells: args.all()?, - semantic: GridSemantics::None, })) } } @@ -48,7 +45,6 @@ impl Layout for GridNode { &self.cells, regions, styles, - self.semantic, ); // Measure the columns and layout the grid row-by-row. @@ -71,7 +67,7 @@ pub enum TrackSizing { /// Defines what kind of semantics a grid should represent. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum GridSemantics { +enum GridSemantics { /// The grid is transparent to the semantic tree. None, /// The grid is a list, its rows are list items. The bool indicates whether @@ -82,6 +78,7 @@ pub enum GridSemantics { } impl GridSemantics { + /// The role of a row in a grid with these semantics. fn row(self) -> Option { match self { Self::None => None, @@ -89,6 +86,18 @@ impl GridSemantics { Self::Table => Some(Role::TableRow), } } + + /// Returns the semantic role of a grid row given the previous semantics and + /// the cell's role. + fn determine(other: Option, role: Option) -> Self { + match (other, role) { + (None, Some(Role::ListItem | Role::ListLabel)) => Self::List, + (Some(Self::List), Some(Role::ListItem | Role::ListLabel)) => Self::List, + (None, Some(Role::TableCell)) => Self::Table, + (Some(Self::Table), Some(Role::TableCell)) => Self::Table, + _ => Self::None, + } + } } castable! { @@ -130,8 +139,6 @@ pub struct GridLayouter<'a> { regions: Regions, /// The inherited styles. styles: StyleChain<'a>, - /// The role of the grid in the semantic tree. - semantic: GridSemantics, /// Resolved column sizes. rcols: Vec, /// Rows in the current region. @@ -167,7 +174,6 @@ impl<'a> GridLayouter<'a> { cells: &'a [LayoutNode], regions: &Regions, styles: StyleChain<'a>, - semantic: GridSemantics, ) -> Self { let mut cols = vec![]; let mut rows = vec![]; @@ -222,7 +228,6 @@ impl<'a> GridLayouter<'a> { rows, regions, styles, - semantic, rcols, lrows, full, @@ -480,11 +485,9 @@ impl<'a> GridLayouter<'a> { /// Layout a row with fixed height and return its frame. fn layout_single_row(&mut self, height: Length, y: usize) -> TypResult { let mut output = Frame::new(Size::new(self.used.x, height)); - if let Some(role) = self.semantic.row() { - output.apply_role(role); - } let mut pos = Point::zero(); + let mut semantic = None; for (x, &rcol) in self.rcols.iter().enumerate() { if let Some(node) = self.cell(x, y) { @@ -498,6 +501,7 @@ impl<'a> GridLayouter<'a> { let pod = Regions::one(size, base, Spec::splat(true)); let frame = node.layout(self.ctx, &pod, self.styles)?.remove(0); + semantic = Some(GridSemantics::determine(semantic, frame.role())); output.push_frame(pos, frame); } @@ -505,6 +509,10 @@ impl<'a> GridLayouter<'a> { pos.x += rcol; } + if let Some(role) = semantic.and_then(GridSemantics::row) { + output.apply_role(role); + } + Ok(output) } @@ -517,14 +525,7 @@ impl<'a> GridLayouter<'a> { // Prepare frames. let mut outputs: Vec<_> = heights .iter() - .map(|&h| { - let mut f = Frame::new(Size::new(self.used.x, h)); - if let Some(role) = self.semantic.row() { - f.apply_role(role); - } - - f - }) + .map(|&h| Frame::new(Size::new(self.used.x, h))) .collect(); // Prepare regions. @@ -534,6 +535,7 @@ impl<'a> GridLayouter<'a> { // Layout the row. let mut pos = Point::zero(); + let mut semantic = None; for (x, &rcol) in self.rcols.iter().enumerate() { if let Some(node) = self.cell(x, y) { pod.first.x = rcol; @@ -547,6 +549,7 @@ impl<'a> GridLayouter<'a> { // Push the layouted frames into the individual output frames. let frames = node.layout(self.ctx, &pod, self.styles)?; for (output, frame) in outputs.iter_mut().zip(frames) { + semantic = Some(GridSemantics::determine(semantic, frame.role())); output.push_frame(pos, frame); } } @@ -554,6 +557,12 @@ impl<'a> GridLayouter<'a> { pos.x += rcol; } + for output in outputs.iter_mut() { + if let Some(role) = semantic.and_then(GridSemantics::row) { + output.apply_role(role); + } + } + Ok(outputs) } diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs index a376bf095..242912a3d 100644 --- a/src/library/structure/heading.rs +++ b/src/library/structure/heading.rs @@ -1,7 +1,6 @@ use crate::library::layout::BlockSpacing; use crate::library::prelude::*; use crate::library::text::{FontFamily, TextNode, TextSize}; -use crate::model::StyleEntry; /// A section heading. #[derive(Debug, Hash)] diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs index 1c0e251f7..077536d46 100644 --- a/src/library/structure/list.rs +++ b/src/library/structure/list.rs @@ -2,11 +2,10 @@ use std::fmt::Write; use unscanny::Scanner; -use crate::library::layout::{BlockSpacing, GridNode, GridSemantics, TrackSizing}; +use crate::library::layout::{BlockSpacing, GridNode, TrackSizing}; use crate::library::prelude::*; use crate::library::text::ParNode; use crate::library::utility::Numbering; -use crate::model::StyleEntry; /// An unordered (bulleted) or ordered (numbered) list. #[derive(Debug, Hash)] @@ -141,7 +140,6 @@ impl Show for ListNode { ]), gutter: Spec::with_y(vec![TrackSizing::Relative(gutter.into())]), cells, - semantic: GridSemantics::List, })) } diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs index 118e48ca3..0f74fc967 100644 --- a/src/library/structure/table.rs +++ b/src/library/structure/table.rs @@ -1,6 +1,5 @@ -use crate::library::layout::{BlockSpacing, GridNode, GridSemantics, TrackSizing}; +use crate::library::layout::{BlockSpacing, GridNode, TrackSizing}; use crate::library::prelude::*; -use crate::model::StyleEntry; /// A table of items. #[derive(Debug, Hash)] @@ -105,7 +104,6 @@ impl Show for TableNode { tracks: self.tracks.clone(), gutter: self.gutter.clone(), cells, - semantic: GridSemantics::Table, }) .role(Role::Table)) } From 55dce19f4925d6505a6382f4584a2532ce67bed2 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Wed, 8 Jun 2022 16:23:32 +0200 Subject: [PATCH 4/6] Restructure PDF structure writing --- src/export/pdf.rs | 359 ++++++++++++++++++++++------------------------ 1 file changed, 172 insertions(+), 187 deletions(-) diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 843e6f374..b5768755b 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -22,6 +22,7 @@ use crate::geom::{ Stroke, Transform, }; use crate::image::{Image, ImageId, ImageStore, RasterImage}; +use crate::library::prelude::EcoString; use crate::library::text::Lang; use crate::Context; @@ -74,7 +75,8 @@ impl<'a> PdfExporter<'a> { self.build_pages(frames); self.write_fonts(); self.write_images(); - self.write_structure() + self.write_structure(); + self.writer.finish() } fn build_pages(&mut self, frames: &[Arc]) { @@ -299,7 +301,7 @@ impl<'a> PdfExporter<'a> { } } - fn write_structure(mut self) -> Vec { + fn write_structure(&mut self) { // The root page tree. let page_tree_ref = self.alloc.bump(); @@ -315,70 +317,94 @@ impl<'a> PdfExporter<'a> { let mut languages = HashMap::new(); let mut heading_tree: Vec = vec![]; - 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); - } - } - } - - annotations.finish(); - page_writer.finish(); - - for (lang, count) in page.languages { - languages - .entry(lang) - .and_modify(|x| *x += count) - .or_insert_with(|| count); - } - - for heading in page.headings.into_iter() { - if let Some(last) = heading_tree.pop() { - let new = last.clone().insert(heading.clone(), *page_id, 1); - if let Some(new) = new { - heading_tree.push(new); - } else { - heading_tree.push(last); - heading_tree.push(HeadingNode::Leaf(heading, *page_id)) - } - } else { - heading_tree.push(HeadingNode::Leaf(heading, *page_id)) - } - } - - self.writer - .stream(content_id, &deflate(&page.content.finish())) - .filter(Filter::FlateDecode); + for (page, page_id) in + std::mem::take(&mut self.pages).into_iter().zip(page_refs.iter()) + { + self.write_page( + page, + *page_id, + &page_refs, + page_tree_ref, + &mut languages, + &mut heading_tree, + &mut page_heights, + ); } + self.write_page_tree(&page_refs, page_tree_ref); + self.write_catalog(page_tree_ref, &languages, &heading_tree); + } + + fn write_page( + &mut self, + page: Page, + page_id: Ref, + page_refs: &[Ref], + page_tree_ref: Ref, + languages: &mut HashMap, + heading_tree: &mut Vec, + page_heights: &mut Vec, + ) { + 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); + } + } + } + + annotations.finish(); + page_writer.finish(); + + for (lang, count) in page.languages { + languages + .entry(lang) + .and_modify(|x| *x += count) + .or_insert_with(|| count); + } + + for heading in page.headings.into_iter() { + if let Some(last) = heading_tree.last_mut() { + if !last.insert(heading.clone(), page_id, 1) { + heading_tree.push(HeadingNode::leaf(heading, page_id)) + } + } else { + heading_tree.push(HeadingNode::leaf(heading, page_id)) + } + } + + self.writer + .stream(content_id, &deflate(&page.content.finish())) + .filter(Filter::FlateDecode); + } + + fn write_page_tree(&mut self, page_refs: &[Ref], page_tree_ref: Ref) { let mut pages = self.writer.pages(page_tree_ref); - pages.count(page_refs.len() as i32).kids(page_refs); + pages.count(page_refs.len() as i32).kids(page_refs.iter().copied()); let mut resources = pages.resources(); let mut spaces = resources.color_spaces(); @@ -403,37 +429,32 @@ impl<'a> PdfExporter<'a> { images.finish(); resources.finish(); pages.finish(); + } - // Build the heading tree. + fn write_catalog( + &mut self, + page_tree_ref: Ref, + languages: &HashMap, + heading_tree: &Vec, + ) { + // Build the outline tree. let outline_root_id = self.alloc.bump(); - let start_ref = self.alloc.bump(); - let mut current_ref = start_ref; - let mut prev_ref = None; + let outline_start_ref = self.alloc; for (i, node) in heading_tree.iter().enumerate() { - let next = write_outline_item( - &mut self.writer, + self.write_outline_item( node, - current_ref, - prev_ref, - i == heading_tree.len() - 1, + i == 0, + i + 1 == heading_tree.len(), outline_root_id, ); - prev_ref = Some(current_ref); - current_ref = next; } - - self.alloc = Ref::new( - start_ref.get() - + heading_tree.iter().map(HeadingNode::len).sum::() as i32, - ); - - if let Some(prev_ref) = prev_ref { + if !heading_tree.is_empty() { let mut outline_root = self.writer.outline(outline_root_id); - outline_root.first(start_ref); - outline_root.last(prev_ref); + outline_root.first(outline_start_ref); + outline_root.last(Ref::new(self.alloc.get() - 1)); outline_root.count(heading_tree.len() as i32); } @@ -442,7 +463,7 @@ impl<'a> PdfExporter<'a> { .max_by(|(_, v1), (_, v2)| v1.cmp(v2)) .map(|(k, _)| k); - let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { + let dir = if lang.copied().map(Lang::dir) == Some(Dir::RTL) { Direction::R2L } else { Direction::L2R @@ -454,7 +475,6 @@ impl<'a> PdfExporter<'a> { catalog.pages(page_tree_ref); catalog.viewer_preferences().direction(dir); - if !heading_tree.is_empty() { catalog.outlines(outline_root_id); } @@ -464,7 +484,51 @@ impl<'a> PdfExporter<'a> { } catalog.finish(); - self.writer.finish() + } + + fn write_outline_item( + &mut self, + node: &HeadingNode, + is_first: bool, + is_last: bool, + parent_ref: Ref, + ) { + let id = self.alloc.bump(); + let next = 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); + } + + if !is_first { + outline.prev(Ref::new(id.get() - 1)); + } + + if !node.children.is_empty() { + let current_child = Ref::new(id.get() + 1); + outline.first(current_child); + outline.last(Ref::new(next.get() - 1)); + + outline.count(-1 * node.children.len() as i32); + } + + outline.title(TextStr(&node.heading.content)); + outline.dest_direct().page(node.page).xyz( + node.heading.position.x.to_f32(), + (node.heading.position.y + Length::pt(3.0)).to_f32(), + None, + ); + + outline.finish(); + + if !node.children.is_empty() { + for (i, child) in node.children.iter().enumerate() { + self.write_outline_item(child, i == 0, i + 1 == node.children.len(), id); + } + } } } @@ -507,58 +571,43 @@ struct State { /// A heading that can later be linked in the outline panel. #[derive(Debug, Clone)] struct Heading { - content: String, + content: EcoString, level: usize, position: Point, } #[derive(Debug, Clone)] -enum HeadingNode { - Leaf(Heading, Ref), - Branch(Heading, Ref, Vec), +struct HeadingNode { + heading: Heading, + page: Ref, + children: Vec, } impl HeadingNode { - fn heading(&self) -> &Heading { - match self { - HeadingNode::Leaf(h, _) => h, - HeadingNode::Branch(h, _, _) => h, - } - } - - fn reference(&self) -> Ref { - match self { - HeadingNode::Leaf(_, r) => *r, - HeadingNode::Branch(_, r, _) => *r, - } + fn leaf(heading: Heading, page: Ref) -> Self { + HeadingNode { heading, page, children: Vec::new() } } fn len(&self) -> usize { - match self { - HeadingNode::Leaf(_, _) => 1, - HeadingNode::Branch(_, _, children) => { - 1 + children.iter().map(|c| c.len()).sum::() - } - } + 1 + self.children.iter().map(|c| c.len()).sum::() } - fn insert(self, other: Heading, page: Ref, level: usize) -> Option { + fn insert(&mut self, other: Heading, page: Ref, level: usize) -> bool { if level >= other.level { - return None; + return false; } - let mut node = match self { - HeadingNode::Leaf(h, r) => (h, r, vec![]), - HeadingNode::Branch(h, r, v) if level + 1 == other.level => (h, r, v), - HeadingNode::Branch(h, r, mut v) => { - let new = v.pop().unwrap().insert(other, page, level + 1).unwrap(); - v.push(new); - return Some(HeadingNode::Branch(h, r, v)); - } - }; + if !self.children.is_empty() && level + 1 > other.level { + return self.children.last_mut().unwrap().insert(other, page, level + 1); + } - node.2.push(HeadingNode::Leaf(other, page)); - Some(HeadingNode::Branch(node.0, node.1, node.2)) + self.children.push(HeadingNode { + heading: other, + page, + children: Vec::new(), + }); + + true } } @@ -603,11 +652,8 @@ impl<'a> PageExporter<'a> { fn write_frame(&mut self, frame: &Frame) { if let Some(Role::Heading(level)) = frame.role() { self.headings.push(Heading { - position: Point::new( - self.state.transform.tx, - self.state.transform.ty + Length::pt(3.0), - ), - content: frame.inner_text().to_string(), + position: Point::new(self.state.transform.tx, self.state.transform.ty), + content: frame.inner_text(), level, }) } @@ -942,67 +988,6 @@ fn encode_image(img: &RasterImage) -> ImageResult<(Vec, Filter, bool)> { }) } -fn write_outline_item( - writer: &mut PdfWriter, - node: &HeadingNode, - current_ref: Ref, - prev_ref: Option, - is_last: bool, - parent_ref: Ref, -) -> Ref { - let mut outline = writer.outline_item(current_ref); - let next = Ref::new(current_ref.get() + node.len() as i32); - outline.parent(parent_ref); - - if !is_last { - outline.next(next); - } - - if let Some(prev_ref) = prev_ref { - outline.prev(prev_ref); - } - - if let HeadingNode::Branch(_, _, children) = node { - let current_child = Ref::new(current_ref.get() + 1); - if children.len() > 0 { - outline.first(current_child); - outline.last(Ref::new(next.get() - 1)); - } - - outline.count(-1 * children.len() as i32); - } - - let heading = node.heading(); - outline.title(TextStr(&heading.content)); - outline.dest_direct().page(node.reference()).xyz( - heading.position.x.to_f32(), - heading.position.y.to_f32(), - None, - ); - - outline.finish(); - - if let HeadingNode::Branch(_, _, children) = node { - let mut current_child = Ref::new(current_ref.get() + 1); - let mut prev_ref = None; - - for (i, child) in children.iter().enumerate() { - write_outline_item( - writer, - child, - current_child, - prev_ref, - i == children.len() - 1, - current_ref, - ); - prev_ref = Some(current_child); - current_child = Ref::new(current_child.get() + 1); - } - } - - next -} - /// Encode an image's alpha channel if present. fn encode_alpha(img: &RasterImage) -> (Vec, Filter) { let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); From 9dca4c2f7833055edd3c1682e98dcc3f86b7e31b Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Wed, 8 Jun 2022 17:05:07 +0200 Subject: [PATCH 5/6] Preallocate Page Refs --- src/export/pdf.rs | 108 +++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 59 deletions(-) diff --git a/src/export/pdf.rs b/src/export/pdf.rs index b5768755b..f8144ad93 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -53,6 +53,8 @@ struct PdfExporter<'a> { glyph_sets: HashMap>, image_map: Remapper, image_refs: Vec, + page_refs: Vec, + heading_tree: Vec, } impl<'a> PdfExporter<'a> { @@ -68,6 +70,8 @@ impl<'a> PdfExporter<'a> { glyph_sets: HashMap::new(), image_map: Remapper::new(), image_refs: vec![], + page_refs: vec![], + heading_tree: vec![], } } @@ -81,7 +85,9 @@ impl<'a> PdfExporter<'a> { fn build_pages(&mut self, frames: &[Arc]) { 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.pages.push(page); } } @@ -306,43 +312,34 @@ impl<'a> PdfExporter<'a> { 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 page_heights = + self.pages.iter().map(|page| page.size.y.to_f32()).collect(); let mut languages = HashMap::new(); - let mut heading_tree: Vec = vec![]; - for (page, page_id) in - std::mem::take(&mut self.pages).into_iter().zip(page_refs.iter()) + for (page, page_id) in std::mem::take(&mut self.pages) + .into_iter() + .zip(self.page_refs.clone().iter()) { self.write_page( page, *page_id, - &page_refs, page_tree_ref, &mut languages, - &mut heading_tree, &mut page_heights, ); } - self.write_page_tree(&page_refs, page_tree_ref); - self.write_catalog(page_tree_ref, &languages, &heading_tree); + self.write_page_tree(page_tree_ref); + self.write_catalog(page_tree_ref, &languages); } fn write_page( &mut self, page: Page, page_id: Ref, - page_refs: &[Ref], page_tree_ref: Ref, languages: &mut HashMap, - heading_tree: &mut Vec, page_heights: &mut Vec, ) { let content_id = self.alloc.bump(); @@ -371,7 +368,7 @@ impl<'a> PdfExporter<'a> { link.action() .action_type(ActionType::GoTo) .destination_direct() - .page(page_refs[index]) + .page(self.page_refs[index]) .xyz(loc.pos.x.to_f32(), height - loc.pos.y.to_f32(), None); } } @@ -387,24 +384,16 @@ impl<'a> PdfExporter<'a> { .or_insert_with(|| count); } - for heading in page.headings.into_iter() { - if let Some(last) = heading_tree.last_mut() { - if !last.insert(heading.clone(), page_id, 1) { - heading_tree.push(HeadingNode::leaf(heading, page_id)) - } - } else { - heading_tree.push(HeadingNode::leaf(heading, page_id)) - } - } - self.writer .stream(content_id, &deflate(&page.content.finish())) .filter(Filter::FlateDecode); } - fn write_page_tree(&mut self, page_refs: &[Ref], page_tree_ref: Ref) { + fn write_page_tree(&mut self, page_tree_ref: Ref) { let mut pages = self.writer.pages(page_tree_ref); - pages.count(page_refs.len() as i32).kids(page_refs.iter().copied()); + 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(); @@ -431,31 +420,26 @@ impl<'a> PdfExporter<'a> { pages.finish(); } - fn write_catalog( - &mut self, - page_tree_ref: Ref, - languages: &HashMap, - heading_tree: &Vec, - ) { + fn write_catalog(&mut self, page_tree_ref: Ref, languages: &HashMap) { // Build the outline tree. let outline_root_id = self.alloc.bump(); let outline_start_ref = self.alloc; - for (i, node) in heading_tree.iter().enumerate() { + for (i, node) in std::mem::take(&mut self.heading_tree).iter().enumerate() { self.write_outline_item( node, i == 0, - i + 1 == heading_tree.len(), + i + 1 == self.heading_tree.len(), outline_root_id, ); } - if !heading_tree.is_empty() { + if !self.heading_tree.is_empty() { 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(heading_tree.len() as i32); + outline_root.count(self.heading_tree.len() as i32); } let lang = languages @@ -475,7 +459,7 @@ impl<'a> PdfExporter<'a> { catalog.pages(page_tree_ref); catalog.viewer_preferences().direction(dir); - if !heading_tree.is_empty() { + if !self.heading_tree.is_empty() { catalog.outlines(outline_root_id); } @@ -516,7 +500,7 @@ impl<'a> PdfExporter<'a> { } outline.title(TextStr(&node.heading.content)); - outline.dest_direct().page(node.page).xyz( + 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, @@ -538,13 +522,14 @@ struct PageExporter<'a> { font_map: &'a mut Remapper, image_map: &'a mut Remapper, glyphs: &'a mut HashMap>, + heading_tree: &'a mut Vec, + page_ref: Ref, languages: HashMap, bottom: f32, content: Content, links: Vec<(Destination, Rect)>, state: State, saves: Vec, - headings: Vec, } /// Data for an exported page. @@ -553,7 +538,6 @@ struct Page { content: Content, links: Vec<(Destination, Rect)>, languages: HashMap, - headings: Vec, } /// A simulated graphics state used to deduplicate graphics state changes and @@ -574,57 +558,55 @@ struct Heading { content: EcoString, level: usize, position: Point, + page: Ref, } #[derive(Debug, Clone)] struct HeadingNode { heading: Heading, - page: Ref, children: Vec, } impl HeadingNode { - fn leaf(heading: Heading, page: Ref) -> Self { - HeadingNode { heading, page, children: Vec::new() } + fn leaf(heading: Heading) -> Self { + HeadingNode { heading, children: Vec::new() } } fn len(&self) -> usize { 1 + self.children.iter().map(|c| c.len()).sum::() } - fn insert(&mut self, other: Heading, page: Ref, level: usize) -> bool { + fn insert(&mut self, other: Heading, level: usize) -> bool { if level >= other.level { return false; } if !self.children.is_empty() && level + 1 > other.level { - return self.children.last_mut().unwrap().insert(other, page, level + 1); + return self.children.last_mut().unwrap().insert(other, level + 1); } - self.children.push(HeadingNode { - heading: other, - page, - children: Vec::new(), - }); + self.children + .push(HeadingNode { heading: other, children: Vec::new() }); true } } impl<'a> PageExporter<'a> { - fn new(exporter: &'a mut PdfExporter) -> Self { + fn new(exporter: &'a mut PdfExporter, 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, + heading_tree: &mut exporter.heading_tree, + page_ref, languages: HashMap::new(), bottom: 0.0, content: Content::new(), links: vec![], state: State::default(), saves: vec![], - headings: vec![], } } @@ -645,17 +627,25 @@ impl<'a> PageExporter<'a> { content: self.content, links: self.links, languages: self.languages, - headings: self.headings, } } fn write_frame(&mut self, frame: &Frame) { if let Some(Role::Heading(level)) = frame.role() { - self.headings.push(Heading { + let heading = Heading { position: Point::new(self.state.transform.tx, self.state.transform.ty), content: frame.inner_text(), + page: self.page_ref, level, - }) + }; + + if let Some(last) = self.heading_tree.last_mut() { + if !last.insert(heading.clone(), 1) { + self.heading_tree.push(HeadingNode::leaf(heading)) + } + } else { + self.heading_tree.push(HeadingNode::leaf(heading)) + } } for &(pos, ref element) in &frame.elements { From 72d3f3fffabe6872eb7839585bea925b89aac6a4 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Wed, 8 Jun 2022 18:43:00 +0200 Subject: [PATCH 6/6] CR: Whoever said orange is the new pink was seriously disturbed. --- src/export/pdf.rs | 228 ++++++++++++++----------------- src/frame.rs | 41 +++--- src/library/graphics/shape.rs | 4 +- src/library/layout/grid.rs | 63 ++------- src/library/layout/page.rs | 14 +- src/library/structure/heading.rs | 5 +- src/library/structure/list.rs | 4 +- src/library/text/par.rs | 1 - src/model/content.rs | 2 +- src/model/styles.rs | 7 - 10 files changed, 148 insertions(+), 221 deletions(-) diff --git a/src/export/pdf.rs b/src/export/pdf.rs index f8144ad93..b8fc1e39d 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -43,34 +43,42 @@ 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, - face_map: Remapper, + page_heights: Vec, + alloc: Ref, + page_tree_ref: Ref, face_refs: Vec, - glyph_sets: HashMap>, - image_map: Remapper, image_refs: Vec, page_refs: Vec, + face_map: Remapper, + image_map: Remapper, + glyph_sets: HashMap>, + languages: HashMap, heading_tree: Vec, } 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(), - face_refs: vec![], - glyph_sets: HashMap::new(), - image_map: Remapper::new(), - image_refs: 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![], } } @@ -79,7 +87,15 @@ 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() } @@ -88,6 +104,7 @@ impl<'a> PdfExporter<'a> { 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); } } @@ -307,45 +324,11 @@ impl<'a> PdfExporter<'a> { } } - fn write_structure(&mut self) { - // The root page tree. - let page_tree_ref = self.alloc.bump(); - - // The page objects (non-root nodes in the page tree). - let mut page_heights = - self.pages.iter().map(|page| page.size.y.to_f32()).collect(); - - let mut languages = HashMap::new(); - - for (page, page_id) in std::mem::take(&mut self.pages) - .into_iter() - .zip(self.page_refs.clone().iter()) - { - self.write_page( - page, - *page_id, - page_tree_ref, - &mut languages, - &mut page_heights, - ); - } - - self.write_page_tree(page_tree_ref); - self.write_catalog(page_tree_ref, &languages); - } - - fn write_page( - &mut self, - page: Page, - page_id: Ref, - page_tree_ref: Ref, - languages: &mut HashMap, - page_heights: &mut Vec, - ) { + 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(page_tree_ref); + 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(); @@ -364,7 +347,7 @@ impl<'a> PdfExporter<'a> { } Destination::Internal(loc) => { let index = loc.page - 1; - let height = page_heights[index]; + let height = self.page_heights[index]; link.action() .action_type(ActionType::GoTo) .destination_direct() @@ -377,20 +360,13 @@ impl<'a> PdfExporter<'a> { 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); } - fn write_page_tree(&mut self, page_tree_ref: Ref) { - let mut pages = self.writer.pages(page_tree_ref); + 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()); @@ -420,34 +396,36 @@ impl<'a> PdfExporter<'a> { pages.finish(); } - fn write_catalog(&mut self, page_tree_ref: Ref, languages: &HashMap) { + fn write_catalog(&mut self) { // Build the outline tree. - let outline_root_id = self.alloc.bump(); - + 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() { - self.write_outline_item( + prev_ref = Some(self.write_outline_item( node, - i == 0, - i + 1 == self.heading_tree.len(), - outline_root_id, - ); + outline_root_id.unwrap(), + prev_ref, + i + 1 == len, + )); } - if !self.heading_tree.is_empty() { + 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 = languages - .into_iter() - .max_by(|(_, v1), (_, v2)| v1.cmp(v2)) - .map(|(k, _)| k); + let lang = self + .languages + .iter() + .max_by_key(|(&lang, &count)| (count, lang)) + .map(|(&k, _)| k); - let dir = if lang.copied().map(Lang::dir) == Some(Dir::RTL) { + let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { Direction::R2L } else { Direction::L2R @@ -456,10 +434,10 @@ 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 !self.heading_tree.is_empty() { + if let Some(outline_root_id) = outline_root_id { catalog.outlines(outline_root_id); } @@ -473,29 +451,28 @@ impl<'a> PdfExporter<'a> { fn write_outline_item( &mut self, node: &HeadingNode, - is_first: bool, - is_last: bool, parent_ref: Ref, - ) { + prev_ref: Option, + is_last: bool, + ) -> Ref { let id = self.alloc.bump(); - let next = Ref::new(id.get() + node.len() as i32); + 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); + outline.next(next_ref); } - if !is_first { - outline.prev(Ref::new(id.get() - 1)); + 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.get() - 1)); - + outline.last(Ref::new(next_ref.get() - 1)); outline.count(-1 * node.children.len() as i32); } @@ -508,36 +485,37 @@ impl<'a> PdfExporter<'a> { outline.finish(); - if !node.children.is_empty() { - for (i, child) in node.children.iter().enumerate() { - self.write_outline_item(child, i == 0, i + 1 == node.children.len(), id); - } + 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, - image_map: &'a mut Remapper, - glyphs: &'a mut HashMap>, - heading_tree: &'a mut Vec, +struct PageExporter<'a, 'b> { + exporter: &'a mut PdfExporter<'b>, page_ref: Ref, - languages: HashMap, - bottom: f32, content: Content, - links: Vec<(Destination, Rect)>, state: State, saves: Vec, + 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, } /// A simulated graphics state used to deduplicate graphics state changes and @@ -573,7 +551,7 @@ impl HeadingNode { } fn len(&self) -> usize { - 1 + self.children.iter().map(|c| c.len()).sum::() + 1 + self.children.iter().map(Self::len).sum::() } fn insert(&mut self, other: Heading, level: usize) -> bool { @@ -581,32 +559,27 @@ impl HeadingNode { return false; } - if !self.children.is_empty() && level + 1 > other.level { - return self.children.last_mut().unwrap().insert(other, level + 1); + if let Some(child) = self.children.last_mut() { + if child.insert(other.clone(), level + 1) { + return true; + } } - self.children - .push(HeadingNode { heading: other, children: Vec::new() }); - + self.children.push(Self::leaf(other)); true } } -impl<'a> PageExporter<'a> { - fn new(exporter: &'a mut PdfExporter, page_ref: Ref) -> Self { +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, - heading_tree: &mut exporter.heading_tree, + exporter, page_ref, - languages: HashMap::new(), - bottom: 0.0, content: Content::new(), - links: vec![], state: State::default(), saves: vec![], + bottom: 0.0, + links: vec![], } } @@ -625,8 +598,8 @@ impl<'a> PageExporter<'a> { Page { size: frame.size, content: self.content, + id: self.page_ref, links: self.links, - languages: self.languages, } } @@ -634,17 +607,17 @@ impl<'a> PageExporter<'a> { if let Some(Role::Heading(level)) = frame.role() { let heading = Heading { position: Point::new(self.state.transform.tx, self.state.transform.ty), - content: frame.inner_text(), + content: frame.text(), page: self.page_ref, level, }; - if let Some(last) = self.heading_tree.last_mut() { + if let Some(last) = self.exporter.heading_tree.last_mut() { if !last.insert(heading.clone(), 1) { - self.heading_tree.push(HeadingNode::leaf(heading)) + self.exporter.heading_tree.push(HeadingNode::leaf(heading)) } } else { - self.heading_tree.push(HeadingNode::leaf(heading)) + self.exporter.heading_tree.push(HeadingNode::leaf(heading)) } } @@ -684,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); @@ -804,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(); @@ -868,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)); } diff --git a/src/frame.rs b/src/frame.rs index 0a8a20548..7972b0697 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -71,11 +71,8 @@ impl Frame { /// group based on the number of elements in the frame. pub fn push_frame(&mut self, pos: Point, frame: impl FrameRepr) { if (self.elements.is_empty() || frame.as_ref().is_light()) - && (frame.as_ref().role().is_none() || self.role.is_none()) + && frame.as_ref().role().is_none() { - if self.role.is_none() { - self.role = frame.as_ref().role() - } frame.inline(self, self.layer(), pos); } else { self.elements.push((pos, Element::Group(Group::new(frame.share())))); @@ -98,11 +95,8 @@ impl Frame { /// Add a frame at a position in the background. pub fn prepend_frame(&mut self, pos: Point, frame: impl FrameRepr) { if (self.elements.is_empty() || frame.as_ref().is_light()) - && (frame.as_ref().role().is_none() || self.role.is_none()) + && frame.as_ref().role().is_none() { - if self.role.is_none() { - self.role = frame.as_ref().role() - } frame.inline(self, 0, pos); } else { self.elements @@ -149,10 +143,8 @@ impl Frame { /// Apply the given role to the frame if it doesn't already have one. pub fn apply_role(&mut self, role: Role) { - match self.role { - None => self.role = Some(role), - Some(old) if old.is_weak() => self.role = Some(role), - Some(_) => {} + if self.role.map_or(true, Role::is_weak) { + self.role = Some(role); } } @@ -179,24 +171,29 @@ impl Frame { } /// Recover the text inside of the frame and its children. - pub fn inner_text(&self) -> EcoString { - let mut res = EcoString::new(); + pub fn text(&self) -> EcoString { + let mut text = EcoString::new(); for (_, element) in &self.elements { match element { - Element::Text(text) => res.push_str( - &text.glyphs.iter().map(|glyph| glyph.c).collect::(), - ), - Element::Group(group) => res.push_str(&group.frame.inner_text()), + Element::Text(content) => { + for glyph in &content.glyphs { + text.push(glyph.c); + } + } + Element::Group(group) => text.push_str(&group.frame.text()), _ => {} } } - res + text } } impl Debug for Frame { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.role.fmt(f)?; + if let Some(role) = self.role { + write!(f, "{role:?} ")?; + } + f.debug_list() .entries(self.elements.iter().map(|(_, element)| element)) .finish() @@ -422,7 +419,7 @@ pub enum Role { /// A generic inline subdivision. GenericInline, /// A list. The boolean indicates whether it is ordered. - List(bool), + List { ordered: bool }, /// A list item. Must have a list parent. ListItem, /// The label of a list item. @@ -445,6 +442,8 @@ pub enum Role { Footer, /// A page background. Background, + /// A page foreground. + Foreground, } impl Role { diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index e972a3d52..82eb2d9d9 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -94,9 +94,7 @@ impl Layout for ShapeNode { frames = child.layout(ctx, &pod, styles)?; for frame in frames.iter_mut() { - if frame.role().map_or(true, Role::is_weak) { - Arc::make_mut(frame).apply_role(Role::GenericBlock); - } + Arc::make_mut(frame).apply_role(Role::GenericBlock); } // Relayout with full expansion into square region to make sure diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs index 687091fff..2d6eb2596 100644 --- a/src/library/layout/grid.rs +++ b/src/library/layout/grid.rs @@ -65,41 +65,6 @@ pub enum TrackSizing { Fractional(Fraction), } -/// Defines what kind of semantics a grid should represent. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -enum GridSemantics { - /// The grid is transparent to the semantic tree. - None, - /// The grid is a list, its rows are list items. The bool indicates whether - /// the list is ordered. - List, - /// The grid is a table. - Table, -} - -impl GridSemantics { - /// The role of a row in a grid with these semantics. - fn row(self) -> Option { - match self { - Self::None => None, - Self::List => Some(Role::ListItem), - Self::Table => Some(Role::TableRow), - } - } - - /// Returns the semantic role of a grid row given the previous semantics and - /// the cell's role. - fn determine(other: Option, role: Option) -> Self { - match (other, role) { - (None, Some(Role::ListItem | Role::ListLabel)) => Self::List, - (Some(Self::List), Some(Role::ListItem | Role::ListLabel)) => Self::List, - (None, Some(Role::TableCell)) => Self::Table, - (Some(Self::Table), Some(Role::TableCell)) => Self::Table, - _ => Self::None, - } - } -} - castable! { Vec, Expected: "integer, auto, relative length, fraction, or array of the latter three)", @@ -487,7 +452,6 @@ impl<'a> GridLayouter<'a> { let mut output = Frame::new(Size::new(self.used.x, height)); let mut pos = Point::zero(); - let mut semantic = None; for (x, &rcol) in self.rcols.iter().enumerate() { if let Some(node) = self.cell(x, y) { @@ -501,7 +465,13 @@ impl<'a> GridLayouter<'a> { let pod = Regions::one(size, base, Spec::splat(true)); let frame = node.layout(self.ctx, &pod, self.styles)?.remove(0); - semantic = Some(GridSemantics::determine(semantic, frame.role())); + match frame.role() { + Some(Role::ListLabel | Role::ListItemBody) => { + output.apply_role(Role::ListItem) + } + Some(Role::TableCell) => output.apply_role(Role::TableRow), + _ => {} + } output.push_frame(pos, frame); } @@ -509,10 +479,6 @@ impl<'a> GridLayouter<'a> { pos.x += rcol; } - if let Some(role) = semantic.and_then(GridSemantics::row) { - output.apply_role(role); - } - Ok(output) } @@ -535,7 +501,6 @@ impl<'a> GridLayouter<'a> { // Layout the row. let mut pos = Point::zero(); - let mut semantic = None; for (x, &rcol) in self.rcols.iter().enumerate() { if let Some(node) = self.cell(x, y) { pod.first.x = rcol; @@ -549,7 +514,13 @@ impl<'a> GridLayouter<'a> { // Push the layouted frames into the individual output frames. let frames = node.layout(self.ctx, &pod, self.styles)?; for (output, frame) in outputs.iter_mut().zip(frames) { - semantic = Some(GridSemantics::determine(semantic, frame.role())); + match frame.role() { + Some(Role::ListLabel | Role::ListItemBody) => { + output.apply_role(Role::ListItem) + } + Some(Role::TableCell) => output.apply_role(Role::TableRow), + _ => {} + } output.push_frame(pos, frame); } } @@ -557,12 +528,6 @@ impl<'a> GridLayouter<'a> { pos.x += rcol; } - for output in outputs.iter_mut() { - if let Some(role) = semantic.and_then(GridSemantics::row) { - output.apply_role(role); - } - } - Ok(outputs) } diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs index 8bd507c43..115a1923b 100644 --- a/src/library/layout/page.rs +++ b/src/library/layout/page.rs @@ -110,30 +110,28 @@ impl PageNode { let pad = padding.resolve(styles).relative_to(size); let pw = size.x - pad.left - pad.right; let py = size.y - pad.bottom; - for (marginal, pos, area, role) in [ + for (role, marginal, pos, area) in [ ( + Role::Header, header, Point::with_x(pad.left), Size::new(pw, pad.top), - Role::Header, ), ( + Role::Footer, footer, Point::new(pad.left, py), Size::new(pw, pad.bottom), - Role::Footer, ), - (foreground, Point::zero(), size, Role::Background), - (background, Point::zero(), size, Role::Background), + (Role::Foreground, foreground, Point::zero(), size), + (Role::Background, background, Point::zero(), size), ] { if let Some(content) = marginal.resolve(ctx, page)? { let pod = Regions::one(area, area, Spec::splat(true)); - let role_map = StyleMap::with_role(role); - let styles = role_map.chain(&styles); let mut sub = content.layout(ctx, &pod, styles)?.remove(0); Arc::make_mut(&mut sub).apply_role(role); - if std::ptr::eq(marginal, background) { + if role == Role::Background { Arc::make_mut(frame).prepend_frame(pos, sub); } else { Arc::make_mut(frame).push_frame(pos, sub); diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs index 242912a3d..af2b36268 100644 --- a/src/library/structure/heading.rs +++ b/src/library/structure/heading.rs @@ -92,8 +92,7 @@ impl Show for HeadingNode { }; } - let mut map = StyleMap::with_role(Role::Heading(self.level.get())); - + let mut map = StyleMap::new(); map.set(TextNode::SIZE, resolve!(Self::SIZE)); if let Smart::Custom(family) = resolve!(Self::FAMILY) { @@ -116,7 +115,7 @@ impl Show for HeadingNode { realized = realized.underlined(); } - realized = realized.styled_with_map(map); + realized = realized.styled_with_map(map).role(Role::Heading(self.level.get())); realized = realized.spaced( resolve!(Self::ABOVE).resolve(styles), resolve!(Self::BELOW).resolve(styles), diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs index 077536d46..015ef5209 100644 --- a/src/library/structure/list.rs +++ b/src/library/structure/list.rs @@ -161,7 +161,9 @@ impl Show for ListNode { } } - Ok(realized.role(Role::List(L == ORDERED)).spaced(above, below)) + Ok(realized + .role(Role::List { ordered: L == ORDERED }) + .spaced(above, below)) } } diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 0fec11ad8..41246b001 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -557,7 +557,6 @@ fn prepare<'a>( if !shift.is_zero() || frame.role().map_or(true, Role::is_weak) { let frame = Arc::make_mut(&mut frame); - frame.translate(Point::with_y(shift)); frame.apply_role(Role::GenericInline); } diff --git a/src/model/content.rs b/src/model/content.rs index 2b9eb182a..3e27c02fd 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -206,7 +206,7 @@ impl Content { /// Assign a role to this content by adding a style map. pub fn role(self, role: Role) -> Self { - self.styled_with_map(StyleMap::with_role(role)) + self.styled_with_entry(StyleEntry::Role(role)) } /// Reenable the show rule identified by the selector. diff --git a/src/model/styles.rs b/src/model/styles.rs index 7db2df42f..7d16f4ba6 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -37,13 +37,6 @@ impl StyleMap { styles } - /// Create a style map from a single role. - pub fn with_role(role: Role) -> Self { - let mut styles = Self::new(); - styles.push(StyleEntry::Role(role)); - styles - } - /// Set an inner value for a style property. /// /// If the property needs folding and the value is already contained in the