summaryrefslogtreecommitdiff
path: root/src/export/pdf/page.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-06-14 21:36:33 +0200
committerLaurenz <laurmaedje@gmail.com>2022-06-14 21:36:33 +0200
commit4817c62dfbaf8cd200ed3582665995fd01fac263 (patch)
treeaa5dd0143743c5b7013e5d47eb7fdbe7621318ca /src/export/pdf/page.rs
parent7fb19d5ef8dc3b183d7de811e373768de56f7ee8 (diff)
Split up PDF exporter
Diffstat (limited to 'src/export/pdf/page.rs')
-rw-r--r--src/export/pdf/page.rs491
1 files changed, 491 insertions, 0 deletions
diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs
new file mode 100644
index 00000000..7cd0f58f
--- /dev/null
+++ b/src/export/pdf/page.rs
@@ -0,0 +1,491 @@
+use pdf_writer::types::{ActionType, AnnotationType, ColorSpaceOperand};
+use pdf_writer::writers::ColorSpace;
+use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str};
+
+use super::{
+ deflate, EmExt, Heading, HeadingNode, LengthExt, PdfContext, RefExt, D65_GRAY, SRGB,
+};
+use crate::font::FaceId;
+use crate::frame::{Destination, Element, Frame, Group, Role, Text};
+use crate::geom::{
+ self, Color, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size, Stroke,
+ Transform,
+};
+use crate::image::ImageId;
+
+/// Construct page objects.
+pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) {
+ for frame in frames {
+ construct_page(ctx, frame);
+ }
+}
+
+/// Construct a page object.
+pub fn construct_page(ctx: &mut PdfContext, frame: &Frame) {
+ let page_ref = ctx.alloc.bump();
+ ctx.page_refs.push(page_ref);
+ ctx.page_heights.push(frame.height().to_f32());
+
+ let mut ctx = PageContext {
+ parent: ctx,
+ page_ref,
+ content: Content::new(),
+ state: State::default(),
+ saves: vec![],
+ bottom: 0.0,
+ links: vec![],
+ };
+
+ let size = frame.size();
+
+ // Make the coordinate system start at the top-left.
+ ctx.bottom = size.y.to_f32();
+ ctx.transform(Transform {
+ sx: Ratio::one(),
+ ky: Ratio::zero(),
+ kx: Ratio::zero(),
+ sy: Ratio::new(-1.0),
+ tx: Length::zero(),
+ ty: size.y,
+ });
+
+ // Encode the page into the content stream.
+ write_frame(&mut ctx, frame);
+
+ let page = Page {
+ size,
+ content: ctx.content,
+ id: ctx.page_ref,
+ links: ctx.links,
+ };
+
+ ctx.parent.pages.push(page);
+}
+
+/// Write the page tree.
+pub fn write_page_tree(ctx: &mut PdfContext) {
+ for page in std::mem::take(&mut ctx.pages).into_iter() {
+ write_page(ctx, page);
+ }
+
+ let mut pages = ctx.writer.pages(ctx.page_tree_ref);
+ pages
+ .count(ctx.page_refs.len() as i32)
+ .kids(ctx.page_refs.iter().copied());
+
+ let mut resources = pages.resources();
+ let mut spaces = resources.color_spaces();
+ spaces.insert(SRGB).start::<ColorSpace>().srgb();
+ spaces.insert(D65_GRAY).start::<ColorSpace>().d65_gray();
+ spaces.finish();
+
+ let mut fonts = resources.fonts();
+ for (font_ref, f) in ctx.face_map.pdf_indices(&ctx.face_refs) {
+ let name = format_eco!("F{}", f);
+ fonts.pair(Name(name.as_bytes()), font_ref);
+ }
+
+ fonts.finish();
+
+ let mut images = resources.x_objects();
+ for (image_ref, im) in ctx.image_map.pdf_indices(&ctx.image_refs) {
+ let name = format_eco!("Im{}", im);
+ images.pair(Name(name.as_bytes()), image_ref);
+ }
+
+ images.finish();
+ resources.finish();
+ pages.finish();
+}
+
+/// Write a page tree node.
+fn write_page(ctx: &mut PdfContext, page: Page) {
+ let content_id = ctx.alloc.bump();
+
+ let mut page_writer = ctx.writer.page(page.id);
+ page_writer.parent(ctx.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.get() - 1;
+ if let Some(&height) = ctx.page_heights.get(index) {
+ link.action()
+ .action_type(ActionType::GoTo)
+ .destination_direct()
+ .page(ctx.page_refs[index])
+ .xyz(loc.pos.x.to_f32(), height - loc.pos.y.to_f32(), None);
+ }
+ }
+ }
+ }
+
+ annotations.finish();
+ page_writer.finish();
+
+ let data = page.content.finish();
+ let data = deflate(&data);
+ ctx.writer.stream(content_id, &data).filter(Filter::FlateDecode);
+}
+
+/// Data for an exported page.
+pub struct Page {
+ /// The indirect object id of the page.
+ pub id: Ref,
+ /// The page's dimensions.
+ pub size: Size,
+ /// The page's content stream.
+ pub content: Content,
+ /// Links in the PDF coordinate system.
+ pub links: Vec<(Destination, Rect)>,
+}
+
+/// An exporter for the contents of a single PDF page.
+struct PageContext<'a, 'b> {
+ parent: &'a mut PdfContext<'b>,
+ page_ref: Ref,
+ content: Content,
+ state: State,
+ saves: Vec<State>,
+ bottom: f32,
+ links: Vec<(Destination, Rect)>,
+}
+
+/// A simulated graphics state used to deduplicate graphics state changes and
+/// keep track of the current transformation matrix for link annotations.
+#[derive(Debug, Default, Clone)]
+struct State {
+ transform: Transform,
+ font: Option<(FaceId, Length)>,
+ fill: Option<Paint>,
+ fill_space: Option<Name<'static>>,
+ stroke: Option<Stroke>,
+ stroke_space: Option<Name<'static>>,
+}
+
+impl<'a, 'b> PageContext<'a, 'b> {
+ fn save_state(&mut self) {
+ self.saves.push(self.state.clone());
+ self.content.save_state();
+ }
+
+ fn restore_state(&mut self) {
+ self.content.restore_state();
+ self.state = self.saves.pop().expect("missing state save");
+ }
+
+ fn transform(&mut self, transform: Transform) {
+ let Transform { sx, ky, kx, sy, tx, ty } = transform;
+ self.state.transform = self.state.transform.pre_concat(transform);
+ self.content.transform([
+ sx.get() as _,
+ ky.get() as _,
+ kx.get() as _,
+ sy.get() as _,
+ tx.to_f32(),
+ ty.to_f32(),
+ ]);
+ }
+
+ fn set_font(&mut self, face_id: FaceId, size: Length) {
+ if self.state.font != Some((face_id, size)) {
+ self.parent.face_map.insert(face_id);
+ let name = format_eco!("F{}", self.parent.face_map.map(face_id));
+ self.content.set_font(Name(name.as_bytes()), size.to_f32());
+ self.state.font = Some((face_id, size));
+ }
+ }
+
+ fn set_fill(&mut self, fill: Paint) {
+ if self.state.fill != Some(fill) {
+ let f = |c| c as f32 / 255.0;
+ let Paint::Solid(color) = fill;
+ match color {
+ Color::Luma(c) => {
+ self.set_fill_color_space(D65_GRAY);
+ self.content.set_fill_gray(f(c.0));
+ }
+ Color::Rgba(c) => {
+ self.set_fill_color_space(SRGB);
+ self.content.set_fill_color([f(c.r), f(c.g), f(c.b)]);
+ }
+ Color::Cmyk(c) => {
+ self.content.set_fill_cmyk(f(c.c), f(c.m), f(c.y), f(c.k));
+ }
+ }
+ self.state.fill = Some(fill);
+ }
+ }
+
+ fn set_fill_color_space(&mut self, space: Name<'static>) {
+ if self.state.fill_space != Some(space) {
+ self.content.set_fill_color_space(ColorSpaceOperand::Named(space));
+ self.state.fill_space = Some(space);
+ }
+ }
+
+ fn set_stroke(&mut self, stroke: Stroke) {
+ if self.state.stroke != Some(stroke) {
+ let f = |c| c as f32 / 255.0;
+ let Paint::Solid(color) = stroke.paint;
+ match color {
+ Color::Luma(c) => {
+ self.set_stroke_color_space(D65_GRAY);
+ self.content.set_stroke_gray(f(c.0));
+ }
+ Color::Rgba(c) => {
+ self.set_stroke_color_space(SRGB);
+ self.content.set_stroke_color([f(c.r), f(c.g), f(c.b)]);
+ }
+ Color::Cmyk(c) => {
+ self.content.set_stroke_cmyk(f(c.c), f(c.m), f(c.y), f(c.k));
+ }
+ }
+
+ self.content.set_line_width(stroke.thickness.to_f32());
+ self.state.stroke = Some(stroke);
+ }
+ }
+
+ fn set_stroke_color_space(&mut self, space: Name<'static>) {
+ if self.state.stroke_space != Some(space) {
+ self.content.set_stroke_color_space(ColorSpaceOperand::Named(space));
+ self.state.stroke_space = Some(space);
+ }
+ }
+}
+
+/// Encode a frame into the content stream.
+fn write_frame(ctx: &mut PageContext, frame: &Frame) {
+ if let Some(Role::Heading { level, outlined: true }) = frame.role() {
+ let heading = Heading {
+ position: Point::new(ctx.state.transform.tx, ctx.state.transform.ty),
+ content: frame.text(),
+ page: ctx.page_ref,
+ level: level.get(),
+ };
+
+ if let Some(last) = ctx.parent.heading_tree.last_mut() {
+ if !last.insert(heading.clone(), 1) {
+ ctx.parent.heading_tree.push(HeadingNode::leaf(heading))
+ }
+ } else {
+ ctx.parent.heading_tree.push(HeadingNode::leaf(heading))
+ }
+ }
+
+ for &(pos, ref element) in frame.elements() {
+ let x = pos.x.to_f32();
+ let y = pos.y.to_f32();
+ match *element {
+ Element::Group(ref group) => write_group(ctx, pos, group),
+ Element::Text(ref text) => write_text(ctx, x, y, text),
+ Element::Shape(ref shape) => write_shape(ctx, x, y, shape),
+ Element::Image(id, size) => write_image(ctx, x, y, id, size),
+ Element::Link(ref dest, size) => write_link(ctx, pos, dest, size),
+ Element::Pin(_) => {}
+ }
+ }
+}
+
+/// Encode a group into the content stream.
+fn write_group(ctx: &mut PageContext, pos: Point, group: &Group) {
+ let translation = Transform::translate(pos.x, pos.y);
+
+ ctx.save_state();
+ ctx.transform(translation.pre_concat(group.transform));
+
+ if group.clips {
+ let size = group.frame.size();
+ let w = size.x.to_f32();
+ let h = size.y.to_f32();
+ ctx.content.move_to(0.0, 0.0);
+ ctx.content.line_to(w, 0.0);
+ ctx.content.line_to(w, h);
+ ctx.content.line_to(0.0, h);
+ ctx.content.clip_nonzero();
+ ctx.content.end_path();
+ }
+
+ write_frame(ctx, &group.frame);
+ ctx.restore_state();
+}
+
+/// Encode a text run into the content stream.
+fn write_text(ctx: &mut PageContext, x: f32, y: f32, text: &Text) {
+ *ctx.parent.languages.entry(text.lang).or_insert(0) += text.glyphs.len();
+ ctx.parent
+ .glyph_sets
+ .entry(text.face_id)
+ .or_default()
+ .extend(text.glyphs.iter().map(|g| g.id));
+
+ let face = ctx.parent.fonts.get(text.face_id);
+
+ ctx.set_fill(text.fill);
+ ctx.set_font(text.face_id, text.size);
+ ctx.content.begin_text();
+
+ // Position the text.
+ ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]);
+
+ let mut positioned = ctx.content.show_positioned();
+ let mut items = positioned.items();
+ let mut adjustment = Em::zero();
+ let mut encoded = vec![];
+
+ // Write the glyphs with kerning adjustments.
+ for glyph in &text.glyphs {
+ adjustment += glyph.x_offset;
+
+ if !adjustment.is_zero() {
+ if !encoded.is_empty() {
+ items.show(Str(&encoded));
+ encoded.clear();
+ }
+
+ items.adjust(-adjustment.to_font_units());
+ adjustment = Em::zero();
+ }
+
+ encoded.push((glyph.id >> 8) as u8);
+ encoded.push((glyph.id & 0xff) as u8);
+
+ if let Some(advance) = face.advance(glyph.id) {
+ adjustment += glyph.x_advance - advance;
+ }
+
+ adjustment -= glyph.x_offset;
+ }
+
+ if !encoded.is_empty() {
+ items.show(Str(&encoded));
+ }
+
+ items.finish();
+ positioned.finish();
+ ctx.content.end_text();
+}
+
+/// Encode a geometrical shape into the content stream.
+fn write_shape(ctx: &mut PageContext, x: f32, y: f32, shape: &Shape) {
+ if shape.fill.is_none() && shape.stroke.is_none() {
+ return;
+ }
+
+ if let Some(fill) = shape.fill {
+ ctx.set_fill(fill);
+ }
+
+ if let Some(stroke) = shape.stroke {
+ ctx.set_stroke(stroke);
+ }
+
+ match shape.geometry {
+ Geometry::Rect(size) => {
+ let w = size.x.to_f32();
+ let h = size.y.to_f32();
+ if w > 0.0 && h > 0.0 {
+ ctx.content.rect(x, y, w, h);
+ }
+ }
+ Geometry::Ellipse(size) => {
+ let approx = geom::Path::ellipse(size);
+ write_path(ctx, x, y, &approx);
+ }
+ Geometry::Line(target) => {
+ let dx = target.x.to_f32();
+ let dy = target.y.to_f32();
+ ctx.content.move_to(x, y);
+ ctx.content.line_to(x + dx, y + dy);
+ }
+ Geometry::Path(ref path) => {
+ write_path(ctx, x, y, path);
+ }
+ }
+
+ match (shape.fill, shape.stroke) {
+ (None, None) => unreachable!(),
+ (Some(_), None) => ctx.content.fill_nonzero(),
+ (None, Some(_)) => ctx.content.stroke(),
+ (Some(_), Some(_)) => ctx.content.fill_nonzero_and_stroke(),
+ };
+}
+
+/// Encode a bezier path into the content stream.
+fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) {
+ for elem in &path.0 {
+ match elem {
+ geom::PathElement::MoveTo(p) => {
+ ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32())
+ }
+ geom::PathElement::LineTo(p) => {
+ ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32())
+ }
+ geom::PathElement::CubicTo(p1, p2, p3) => ctx.content.cubic_to(
+ x + p1.x.to_f32(),
+ y + p1.y.to_f32(),
+ x + p2.x.to_f32(),
+ y + p2.y.to_f32(),
+ x + p3.x.to_f32(),
+ y + p3.y.to_f32(),
+ ),
+ geom::PathElement::ClosePath => ctx.content.close_path(),
+ };
+ }
+}
+
+/// Encode a vector or raster image into the content stream.
+fn write_image(ctx: &mut PageContext, x: f32, y: f32, id: ImageId, size: Size) {
+ ctx.parent.image_map.insert(id);
+ let name = format_eco!("Im{}", ctx.parent.image_map.map(id));
+ let w = size.x.to_f32();
+ let h = size.y.to_f32();
+ ctx.content.save_state();
+ ctx.content.transform([w, 0.0, 0.0, -h, x, y + h]);
+ ctx.content.x_object(Name(name.as_bytes()));
+ ctx.content.restore_state();
+}
+
+/// Save a link for later writing in the annotations dictionary.
+fn write_link(ctx: &mut PageContext, pos: Point, dest: &Destination, size: Size) {
+ let mut min_x = Length::inf();
+ let mut min_y = Length::inf();
+ let mut max_x = -Length::inf();
+ let mut max_y = -Length::inf();
+
+ // Compute the bounding box of the transformed link.
+ for point in [
+ pos,
+ pos + Point::with_x(size.x),
+ pos + Point::with_y(size.y),
+ pos + size.to_point(),
+ ] {
+ let t = point.transform(ctx.state.transform);
+ min_x.set_min(t.x);
+ min_y.set_min(t.y);
+ max_x.set_max(t.x);
+ max_y.set_max(t.y);
+ }
+
+ let x1 = min_x.to_f32();
+ let x2 = max_x.to_f32();
+ let y1 = max_y.to_f32();
+ let y2 = min_y.to_f32();
+ let rect = Rect::new(x1, y1, x2, y2);
+
+ ctx.links.push((dest.clone(), rect));
+}