summaryrefslogtreecommitdiff
path: root/src/export
diff options
context:
space:
mode:
Diffstat (limited to 'src/export')
-rw-r--r--src/export/mod.rs7
-rw-r--r--src/export/pdf/font.rs204
-rw-r--r--src/export/pdf/image.rs143
-rw-r--r--src/export/pdf/mod.rs235
-rw-r--r--src/export/pdf/outline.rs127
-rw-r--r--src/export/pdf/page.rs565
-rw-r--r--src/export/render.rs673
7 files changed, 0 insertions, 1954 deletions
diff --git a/src/export/mod.rs b/src/export/mod.rs
deleted file mode 100644
index eb0731a9..00000000
--- a/src/export/mod.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-//! Exporting into external formats.
-
-mod pdf;
-mod render;
-
-pub use self::pdf::pdf;
-pub use self::render::render;
diff --git a/src/export/pdf/font.rs b/src/export/pdf/font.rs
deleted file mode 100644
index f0676d8f..00000000
--- a/src/export/pdf/font.rs
+++ /dev/null
@@ -1,204 +0,0 @@
-use std::collections::BTreeMap;
-
-use ecow::{eco_format, EcoString};
-use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap};
-use pdf_writer::{Filter, Finish, Name, Rect, Str};
-use ttf_parser::{name_id, GlyphId, Tag};
-use unicode_general_category::GeneralCategory;
-
-use super::{deflate, EmExt, PdfContext, RefExt};
-use crate::font::Font;
-use crate::util::{Bytes, SliceExt};
-
-const CMAP_NAME: Name = Name(b"Custom");
-const SYSTEM_INFO: SystemInfo = SystemInfo {
- registry: Str(b"Adobe"),
- ordering: Str(b"Identity"),
- supplement: 0,
-};
-
-/// Embed all used fonts into the PDF.
-#[tracing::instrument(skip_all)]
-pub fn write_fonts(ctx: &mut PdfContext) {
- for font in ctx.font_map.items() {
- let type0_ref = ctx.alloc.bump();
- let cid_ref = ctx.alloc.bump();
- let descriptor_ref = ctx.alloc.bump();
- let cmap_ref = ctx.alloc.bump();
- let data_ref = ctx.alloc.bump();
- ctx.font_refs.push(type0_ref);
-
- let glyph_set = ctx.glyph_sets.get_mut(font).unwrap();
- let metrics = font.metrics();
- let ttf = font.ttf();
-
- let postscript_name = font
- .find_name(name_id::POST_SCRIPT_NAME)
- .unwrap_or_else(|| "unknown".to_string());
-
- let base_font = eco_format!("ABCDEF+{}", postscript_name);
- let base_font = Name(base_font.as_bytes());
-
- // Write the base font object referencing the CID font.
- ctx.writer
- .type0_font(type0_ref)
- .base_font(base_font)
- .encoding_predefined(Name(b"Identity-H"))
- .descendant_font(cid_ref)
- .to_unicode(cmap_ref);
-
- // Check for the presence of CFF outlines to select the correct
- // CID-Font subtype.
- let subtype = match ttf
- .raw_face()
- .table(Tag::from_bytes(b"CFF "))
- .or(ttf.raw_face().table(Tag::from_bytes(b"CFF2")))
- {
- Some(_) => CidFontType::Type0,
- None => CidFontType::Type2,
- };
-
- // Write the CID font referencing the font descriptor.
- let mut cid = ctx.writer.cid_font(cid_ref);
- cid.subtype(subtype);
- cid.base_font(base_font);
- cid.system_info(SYSTEM_INFO);
- cid.font_descriptor(descriptor_ref);
- cid.default_width(0.0);
-
- if subtype == CidFontType::Type2 {
- cid.cid_to_gid_map_predefined(Name(b"Identity"));
- }
-
- // Extract the widths of all glyphs.
- let num_glyphs = ttf.number_of_glyphs();
- let mut widths = vec![0.0; num_glyphs as usize];
- for &g in glyph_set.keys() {
- let x = ttf.glyph_hor_advance(GlyphId(g)).unwrap_or(0);
- widths[g as usize] = font.to_em(x).to_font_units();
- }
-
- // Write all non-zero glyph widths.
- let mut first = 0;
- let mut width_writer = cid.widths();
- for (w, group) in widths.group_by_key(|&w| w) {
- let end = first + group.len();
- if w != 0.0 {
- let last = end - 1;
- width_writer.same(first as u16, last as u16, w);
- }
- first = end;
- }
-
- width_writer.finish();
- cid.finish();
-
- let mut flags = FontFlags::empty();
- flags.set(FontFlags::SERIF, postscript_name.contains("Serif"));
- flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced());
- flags.set(FontFlags::ITALIC, ttf.is_italic());
- flags.insert(FontFlags::SYMBOLIC);
- flags.insert(FontFlags::SMALL_CAP);
-
- let global_bbox = ttf.global_bounding_box();
- let bbox = Rect::new(
- font.to_em(global_bbox.x_min).to_font_units(),
- font.to_em(global_bbox.y_min).to_font_units(),
- font.to_em(global_bbox.x_max).to_font_units(),
- font.to_em(global_bbox.y_max).to_font_units(),
- );
-
- let italic_angle = ttf.italic_angle().unwrap_or(0.0);
- let ascender = metrics.ascender.to_font_units();
- let descender = metrics.descender.to_font_units();
- let cap_height = metrics.cap_height.to_font_units();
- let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0);
-
- // Write the font descriptor (contains metrics about the font).
- let mut font_descriptor = ctx.writer.font_descriptor(descriptor_ref);
- font_descriptor
- .name(base_font)
- .flags(flags)
- .bbox(bbox)
- .italic_angle(italic_angle)
- .ascent(ascender)
- .descent(descender)
- .cap_height(cap_height)
- .stem_v(stem_v);
-
- match subtype {
- CidFontType::Type0 => font_descriptor.font_file3(data_ref),
- CidFontType::Type2 => font_descriptor.font_file2(data_ref),
- };
-
- font_descriptor.finish();
-
- // Write the /ToUnicode character map, which maps glyph ids back to
- // unicode codepoints to enable copying out of the PDF.
- let cmap = create_cmap(ttf, glyph_set);
- ctx.writer.cmap(cmap_ref, &cmap.finish());
-
- // Subset and write the font's bytes.
- let glyphs: Vec<_> = glyph_set.keys().copied().collect();
- let data = subset_font(font, &glyphs);
- let mut stream = ctx.writer.stream(data_ref, &data);
- stream.filter(Filter::FlateDecode);
-
- if subtype == CidFontType::Type0 {
- stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C"));
- }
-
- stream.finish();
- }
-}
-
-/// Subset a font to the given glyphs.
-#[comemo::memoize]
-fn subset_font(font: &Font, glyphs: &[u16]) -> Bytes {
- let data = font.data();
- let profile = subsetter::Profile::pdf(glyphs);
- let subsetted = subsetter::subset(data, font.index(), profile);
- let data = subsetted.as_deref().unwrap_or(data);
- deflate(data).into()
-}
-
-/// Create a /ToUnicode CMap.
-fn create_cmap(
- ttf: &ttf_parser::Face,
- glyph_set: &mut BTreeMap<u16, EcoString>,
-) -> UnicodeCmap {
- // For glyphs that have codepoints mapping to in the font's cmap table, we
- // prefer them over pre-existing text mappings from the document. Only
- // things that don't have a corresponding codepoint (or only a private-use
- // one) like the "Th" in Linux Libertine get the text of their first
- // occurrences in the document instead.
- for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) {
- if !subtable.is_unicode() {
- continue;
- }
-
- subtable.codepoints(|n| {
- let Some(c) = std::char::from_u32(n) else { return };
- if unicode_general_category::get_general_category(c)
- == GeneralCategory::PrivateUse
- {
- return;
- }
-
- let Some(GlyphId(g)) = ttf.glyph_index(c) else { return };
- if glyph_set.contains_key(&g) {
- glyph_set.insert(g, c.into());
- }
- });
- }
-
- // Produce a reverse mapping from glyphs to unicode strings.
- let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO);
- for (&g, text) in glyph_set.iter() {
- if !text.is_empty() {
- cmap.pair_with_multiple(g, text.chars());
- }
- }
-
- cmap
-}
diff --git a/src/export/pdf/image.rs b/src/export/pdf/image.rs
deleted file mode 100644
index 48472d9f..00000000
--- a/src/export/pdf/image.rs
+++ /dev/null
@@ -1,143 +0,0 @@
-use std::io::Cursor;
-
-use image::{DynamicImage, GenericImageView, Rgba};
-use pdf_writer::{Filter, Finish};
-
-use super::{deflate, PdfContext, RefExt};
-use crate::image::{DecodedImage, Image, RasterFormat};
-use crate::util::Bytes;
-
-/// Embed all used images into the PDF.
-#[tracing::instrument(skip_all)]
-pub fn write_images(ctx: &mut PdfContext) {
- for image in ctx.image_map.items() {
- let image_ref = ctx.alloc.bump();
- let icc_ref = ctx.alloc.bump();
- ctx.image_refs.push(image_ref);
-
- let width = image.width();
- let height = image.height();
-
- // Add the primary image.
- // TODO: Error if image could not be encoded.
- match image.decoded().as_ref() {
- DecodedImage::Raster(dynamic, icc, _) => {
- // TODO: Error if image could not be encoded.
- let (data, filter, has_color) = encode_image(image);
- let mut image = ctx.writer.image_xobject(image_ref, &data);
- image.filter(filter);
- image.width(width as i32);
- image.height(height as i32);
- image.bits_per_component(8);
-
- let space = image.color_space();
- if icc.is_some() {
- space.icc_based(icc_ref);
- } else if has_color {
- space.device_rgb();
- } else {
- space.device_gray();
- }
-
- // Add a second gray-scale image containing the alpha values if
- // this image has an alpha channel.
- if dynamic.color().has_alpha() {
- let (alpha_data, alpha_filter) = encode_alpha(dynamic);
- let mask_ref = ctx.alloc.bump();
- image.s_mask(mask_ref);
- image.finish();
-
- let mut mask = ctx.writer.image_xobject(mask_ref, &alpha_data);
- mask.filter(alpha_filter);
- mask.width(width as i32);
- mask.height(height as i32);
- mask.color_space().device_gray();
- mask.bits_per_component(8);
- } else {
- image.finish();
- }
-
- if let Some(icc) = icc {
- let compressed = deflate(&icc.0);
- let mut stream = ctx.writer.icc_profile(icc_ref, &compressed);
- stream.filter(Filter::FlateDecode);
- if has_color {
- stream.n(3);
- stream.alternate().srgb();
- } else {
- stream.n(1);
- stream.alternate().d65_gray();
- }
- }
- }
- DecodedImage::Svg(svg) => {
- let next_ref = svg2pdf::convert_tree_into(
- svg,
- svg2pdf::Options::default(),
- &mut ctx.writer,
- image_ref,
- );
- ctx.alloc = next_ref;
- }
- }
- }
-}
-
-/// Encode an image with a suitable filter and return the data, filter and
-/// whether the image has color.
-///
-/// Skips the alpha channel as that's encoded separately.
-#[comemo::memoize]
-#[tracing::instrument(skip_all)]
-fn encode_image(image: &Image) -> (Bytes, Filter, bool) {
- let decoded = image.decoded();
- let (dynamic, format) = match decoded.as_ref() {
- DecodedImage::Raster(dynamic, _, format) => (dynamic, *format),
- _ => panic!("can only encode raster image"),
- };
-
- match (format, dynamic) {
- // 8-bit gray JPEG.
- (RasterFormat::Jpg, DynamicImage::ImageLuma8(_)) => {
- let mut data = Cursor::new(vec![]);
- dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
- (data.into_inner().into(), Filter::DctDecode, false)
- }
-
- // 8-bit RGB JPEG (CMYK JPEGs get converted to RGB earlier).
- (RasterFormat::Jpg, DynamicImage::ImageRgb8(_)) => {
- let mut data = Cursor::new(vec![]);
- dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
- (data.into_inner().into(), Filter::DctDecode, true)
- }
-
- // TODO: Encode flate streams with PNG-predictor?
-
- // 8-bit gray PNG.
- (RasterFormat::Png, DynamicImage::ImageLuma8(luma)) => {
- let data = deflate(luma.as_raw());
- (data.into(), Filter::FlateDecode, false)
- }
-
- // Anything else (including Rgb(a) PNGs).
- (_, buf) => {
- let (width, height) = buf.dimensions();
- let mut pixels = Vec::with_capacity(3 * width as usize * height as usize);
- for (_, _, Rgba([r, g, b, _])) in buf.pixels() {
- pixels.push(r);
- pixels.push(g);
- pixels.push(b);
- }
-
- let data = deflate(&pixels);
- (data.into(), Filter::FlateDecode, true)
- }
- }
-}
-
-/// Encode an image's alpha channel if present.
-#[tracing::instrument(skip_all)]
-fn encode_alpha(dynamic: &DynamicImage) -> (Vec<u8>, Filter) {
- let pixels: Vec<_> = dynamic.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
- (deflate(&pixels), Filter::FlateDecode)
-}
diff --git a/src/export/pdf/mod.rs b/src/export/pdf/mod.rs
deleted file mode 100644
index 48485862..00000000
--- a/src/export/pdf/mod.rs
+++ /dev/null
@@ -1,235 +0,0 @@
-//! Exporting into PDF documents.
-
-mod font;
-mod image;
-mod outline;
-mod page;
-
-use std::cmp::Eq;
-use std::collections::{BTreeMap, HashMap};
-use std::hash::Hash;
-
-use ecow::EcoString;
-use pdf_writer::types::Direction;
-use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr};
-use xmp_writer::{LangId, RenditionClass, XmpWriter};
-
-use self::page::Page;
-use crate::doc::{Document, Lang};
-use crate::font::Font;
-use crate::geom::{Abs, Dir, Em};
-use crate::image::Image;
-use crate::model::Introspector;
-
-/// Export a document into a PDF file.
-///
-/// Returns the raw bytes making up the PDF file.
-#[tracing::instrument(skip_all)]
-pub fn pdf(document: &Document) -> Vec<u8> {
- let mut ctx = PdfContext::new(document);
- page::construct_pages(&mut ctx, &document.pages);
- font::write_fonts(&mut ctx);
- image::write_images(&mut ctx);
- page::write_page_tree(&mut ctx);
- write_catalog(&mut ctx);
- ctx.writer.finish()
-}
-
-/// Identifies the color space definitions.
-const SRGB: Name<'static> = Name(b"srgb");
-const D65_GRAY: Name<'static> = Name(b"d65gray");
-
-/// Context for exporting a whole PDF document.
-pub struct PdfContext<'a> {
- document: &'a Document,
- introspector: Introspector,
- writer: PdfWriter,
- pages: Vec<Page>,
- page_heights: Vec<f32>,
- alloc: Ref,
- page_tree_ref: Ref,
- font_refs: Vec<Ref>,
- image_refs: Vec<Ref>,
- page_refs: Vec<Ref>,
- font_map: Remapper<Font>,
- image_map: Remapper<Image>,
- /// For each font a mapping from used glyphs to their text representation.
- /// May contain multiple chars in case of ligatures or similar things. The
- /// same glyph can have a different text representation within one document,
- /// then we just save the first one. The resulting strings are used for the
- /// PDF's /ToUnicode map for glyphs that don't have an entry in the font's
- /// cmap. This is important for copy-paste and searching.
- glyph_sets: HashMap<Font, BTreeMap<u16, EcoString>>,
- languages: HashMap<Lang, usize>,
-}
-
-impl<'a> PdfContext<'a> {
- fn new(document: &'a Document) -> Self {
- let mut alloc = Ref::new(1);
- let page_tree_ref = alloc.bump();
- Self {
- document,
- introspector: Introspector::new(&document.pages),
- writer: PdfWriter::new(),
- pages: vec![],
- page_heights: vec![],
- alloc,
- page_tree_ref,
- page_refs: vec![],
- font_refs: vec![],
- image_refs: vec![],
- font_map: Remapper::new(),
- image_map: Remapper::new(),
- glyph_sets: HashMap::new(),
- languages: HashMap::new(),
- }
- }
-}
-
-/// Write the document catalog.
-#[tracing::instrument(skip_all)]
-fn write_catalog(ctx: &mut PdfContext) {
- let lang = ctx
- .languages
- .iter()
- .max_by_key(|(&lang, &count)| (count, lang))
- .map(|(&k, _)| k);
-
- let dir = if lang.map(Lang::dir) == Some(Dir::RTL) {
- Direction::R2L
- } else {
- Direction::L2R
- };
-
- // Write the outline tree.
- let outline_root_id = outline::write_outline(ctx);
-
- // Write the document information.
- let mut info = ctx.writer.document_info(ctx.alloc.bump());
- let mut xmp = XmpWriter::new();
- if let Some(title) = &ctx.document.title {
- info.title(TextStr(title));
- xmp.title([(None, title.as_str())]);
- }
-
- let authors = &ctx.document.author;
- if !authors.is_empty() {
- info.author(TextStr(&authors.join(", ")));
- xmp.creator(authors.iter().map(|s| s.as_str()));
- }
- info.creator(TextStr("Typst"));
- info.finish();
- xmp.creator_tool("Typst");
- xmp.num_pages(ctx.document.pages.len() as u32);
- xmp.format("application/pdf");
- xmp.language(ctx.languages.keys().map(|lang| LangId(lang.as_str())));
- xmp.rendition_class(RenditionClass::Proof);
- xmp.pdf_version("1.7");
-
- let xmp_buf = xmp.finish(None);
- let meta_ref = ctx.alloc.bump();
- let mut meta_stream = ctx.writer.stream(meta_ref, xmp_buf.as_bytes());
- meta_stream.pair(Name(b"Type"), Name(b"Metadata"));
- meta_stream.pair(Name(b"Subtype"), Name(b"XML"));
- meta_stream.finish();
-
- // Write the document catalog.
- let mut catalog = ctx.writer.catalog(ctx.alloc.bump());
- catalog.pages(ctx.page_tree_ref);
- catalog.viewer_preferences().direction(dir);
- catalog.pair(Name(b"Metadata"), meta_ref);
-
- if let Some(outline_root_id) = outline_root_id {
- catalog.outlines(outline_root_id);
- }
-
- if let Some(lang) = lang {
- catalog.lang(TextStr(lang.as_str()));
- }
-}
-
-/// Compress data with the DEFLATE algorithm.
-#[tracing::instrument(skip_all)]
-fn deflate(data: &[u8]) -> Vec<u8> {
- const COMPRESSION_LEVEL: u8 = 6;
- miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL)
-}
-
-/// Assigns new, consecutive PDF-internal indices to items.
-struct Remapper<T> {
- /// Forwards from the items to the pdf indices.
- to_pdf: HashMap<T, usize>,
- /// Backwards from the pdf indices to the items.
- to_items: Vec<T>,
-}
-
-impl<T> Remapper<T>
-where
- T: Eq + Hash + Clone,
-{
- fn new() -> Self {
- Self { to_pdf: HashMap::new(), to_items: vec![] }
- }
-
- fn insert(&mut self, item: T) {
- let to_layout = &mut self.to_items;
- self.to_pdf.entry(item.clone()).or_insert_with(|| {
- let pdf_index = to_layout.len();
- to_layout.push(item);
- pdf_index
- });
- }
-
- fn map(&self, item: T) -> usize {
- self.to_pdf[&item]
- }
-
- fn pdf_indices<'a>(
- &'a self,
- refs: &'a [Ref],
- ) -> impl Iterator<Item = (Ref, usize)> + 'a {
- refs.iter().copied().zip(0..self.to_pdf.len())
- }
-
- fn items(&self) -> impl Iterator<Item = &T> + '_ {
- self.to_items.iter()
- }
-}
-
-/// Additional methods for [`Abs`].
-trait AbsExt {
- /// Convert an to a number of points.
- fn to_f32(self) -> f32;
-}
-
-impl AbsExt for Abs {
- fn to_f32(self) -> f32 {
- self.to_pt() as f32
- }
-}
-
-/// Additional methods for [`Em`].
-trait EmExt {
- /// Convert an em length to a number of PDF font units.
- fn to_font_units(self) -> f32;
-}
-
-impl EmExt for Em {
- fn to_font_units(self) -> f32 {
- 1000.0 * self.get() as f32
- }
-}
-
-/// Additional methods for [`Ref`].
-trait RefExt {
- /// Bump the reference up by one and return the previous one.
- fn bump(&mut self) -> Self;
-}
-
-impl RefExt for Ref {
- fn bump(&mut self) -> Self {
- let prev = *self;
- *self = Self::new(prev.get() + 1);
- prev
- }
-}
diff --git a/src/export/pdf/outline.rs b/src/export/pdf/outline.rs
deleted file mode 100644
index 539647eb..00000000
--- a/src/export/pdf/outline.rs
+++ /dev/null
@@ -1,127 +0,0 @@
-use std::num::NonZeroUsize;
-
-use pdf_writer::{Finish, Ref, TextStr};
-
-use super::{AbsExt, PdfContext, RefExt};
-use crate::geom::Abs;
-use crate::model::Content;
-
-/// Construct the outline for the document.
-#[tracing::instrument(skip_all)]
-pub fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
- let mut tree: Vec<HeadingNode> = vec![];
- for heading in ctx.introspector.query(&item!(heading_func).select()) {
- let leaf = HeadingNode::leaf((*heading).clone());
-
- let mut children = &mut tree;
- while children.last().map_or(false, |last| last.level < leaf.level) {
- children = &mut children.last_mut().unwrap().children;
- }
-
- children.push(leaf);
- }
-
- if tree.is_empty() {
- return None;
- }
-
- let root_id = ctx.alloc.bump();
- let start_ref = ctx.alloc;
- let len = tree.len();
-
- let mut prev_ref = None;
- for (i, node) in tree.iter().enumerate() {
- prev_ref = Some(write_outline_item(ctx, node, root_id, prev_ref, i + 1 == len));
- }
-
- ctx.writer
- .outline(root_id)
- .first(start_ref)
- .last(Ref::new(ctx.alloc.get() - 1))
- .count(tree.len() as i32);
-
- Some(root_id)
-}
-
-/// A heading in the outline panel.
-#[derive(Debug, Clone)]
-struct HeadingNode {
- element: Content,
- level: NonZeroUsize,
- children: Vec<HeadingNode>,
-}
-
-impl HeadingNode {
- fn leaf(element: Content) -> Self {
- HeadingNode {
- level: element.expect_field::<NonZeroUsize>("level"),
- element,
- children: Vec::new(),
- }
- }
-
- fn len(&self) -> usize {
- 1 + self.children.iter().map(Self::len).sum::<usize>()
- }
-}
-
-/// Write an outline item and all its children.
-#[tracing::instrument(skip_all)]
-fn write_outline_item(
- ctx: &mut PdfContext,
- node: &HeadingNode,
- parent_ref: Ref,
- prev_ref: Option<Ref>,
- is_last: bool,
-) -> Ref {
- let id = ctx.alloc.bump();
- let next_ref = Ref::new(id.get() + node.len() as i32);
-
- let mut outline = ctx.writer.outline_item(id);
- outline.parent(parent_ref);
-
- if !is_last {
- outline.next(next_ref);
- }
-
- if let Some(prev_rev) = prev_ref {
- outline.prev(prev_rev);
- }
-
- if !node.children.is_empty() {
- let current_child = Ref::new(id.get() + 1);
- outline.first(current_child);
- outline.last(Ref::new(next_ref.get() - 1));
- outline.count(-(node.children.len() as i32));
- }
-
- let body = node.element.expect_field::<Content>("body");
- outline.title(TextStr(body.plain_text().trim()));
-
- let loc = node.element.location().unwrap();
- let pos = ctx.introspector.position(loc);
- let index = pos.page.get() - 1;
- if let Some(&height) = ctx.page_heights.get(index) {
- let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
- outline.dest().page(ctx.page_refs[index]).xyz(
- pos.point.x.to_f32(),
- height - y.to_f32(),
- None,
- );
- }
-
- outline.finish();
-
- let mut prev_ref = None;
- for (i, child) in node.children.iter().enumerate() {
- prev_ref = Some(write_outline_item(
- ctx,
- child,
- id,
- prev_ref,
- i + 1 == node.children.len(),
- ));
- }
-
- id
-}
diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs
deleted file mode 100644
index 22e590d5..00000000
--- a/src/export/pdf/page.rs
+++ /dev/null
@@ -1,565 +0,0 @@
-use ecow::eco_format;
-use pdf_writer::types::{
- ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle,
-};
-use pdf_writer::writers::ColorSpace;
-use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str};
-
-use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB};
-use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem};
-use crate::font::Font;
-use crate::geom::{
- self, Abs, Color, Em, Geometry, LineCap, LineJoin, Numeric, Paint, Point, Ratio,
- Shape, Size, Stroke, Transform,
-};
-use crate::image::Image;
-
-/// Construct page objects.
-#[tracing::instrument(skip_all)]
-pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) {
- for frame in frames {
- construct_page(ctx, frame);
- }
-}
-
-/// Construct a page object.
-#[tracing::instrument(skip_all)]
-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: Abs::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.
-#[tracing::instrument(skip_all)]
-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.font_map.pdf_indices(&ctx.font_refs) {
- let name = eco_format!("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 = eco_format!("Im{}", im);
- images.pair(Name(name.as_bytes()), image_ref);
- }
-
- images.finish();
- resources.finish();
- pages.finish();
-}
-
-/// Write a page tree node.
-#[tracing::instrument(skip_all)]
-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 annotation = annotations.push();
- annotation.subtype(AnnotationType::Link).rect(rect);
- annotation.border(0.0, 0.0, 0.0, None);
-
- let pos = match dest {
- Destination::Url(uri) => {
- annotation
- .action()
- .action_type(ActionType::Uri)
- .uri(Str(uri.as_bytes()));
- continue;
- }
- Destination::Position(pos) => pos,
- Destination::Location(loc) => ctx.introspector.position(loc),
- };
-
- let index = pos.page.get() - 1;
- let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
- if let Some(&height) = ctx.page_heights.get(index) {
- annotation
- .action()
- .action_type(ActionType::GoTo)
- .destination()
- .page(ctx.page_refs[index])
- .xyz(pos.point.x.to_f32(), height - 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<(Font, Abs)>,
- fill: Option<Paint>,
- fill_space: Option<Name<'static>>,
- stroke: Option<Stroke>,
- stroke_space: Option<Name<'static>>,
-}
-
-impl PageContext<'_, '_> {
- 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, font: &Font, size: Abs) {
- if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) {
- self.parent.font_map.insert(font.clone());
- let name = eco_format!("F{}", self.parent.font_map.map(font.clone()));
- self.content.set_font(Name(name.as_bytes()), size.to_f32());
- self.state.font = Some((font.clone(), size));
- }
- }
-
- fn set_fill(&mut self, fill: &Paint) {
- if self.state.fill.as_ref() != 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.reset_fill_color_space();
- self.content.set_fill_cmyk(f(c.c), f(c.m), f(c.y), f(c.k));
- }
- }
- self.state.fill = Some(fill.clone());
- }
- }
-
- 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 reset_fill_color_space(&mut self) {
- self.state.fill_space = None;
- }
-
- fn set_stroke(&mut self, stroke: &Stroke) {
- if self.state.stroke.as_ref() != Some(stroke) {
- let Stroke {
- paint,
- thickness,
- line_cap,
- line_join,
- dash_pattern,
- miter_limit,
- } = stroke;
-
- let f = |c| c as f32 / 255.0;
- let Paint::Solid(color) = 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.reset_stroke_color_space();
- self.content.set_stroke_cmyk(f(c.c), f(c.m), f(c.y), f(c.k));
- }
- }
-
- self.content.set_line_width(thickness.to_f32());
- if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) {
- self.content.set_line_cap(line_cap.into());
- }
- if self.state.stroke.as_ref().map(|s| &s.line_join) != Some(line_join) {
- self.content.set_line_join(line_join.into());
- }
- if self.state.stroke.as_ref().map(|s| &s.dash_pattern) != Some(dash_pattern) {
- if let Some(pattern) = dash_pattern {
- self.content.set_dash_pattern(
- pattern.array.iter().map(|l| l.to_f32()),
- pattern.phase.to_f32(),
- );
- } else {
- self.content.set_dash_pattern([], 0.0);
- }
- }
- if self.state.stroke.as_ref().map(|s| &s.miter_limit) != Some(miter_limit) {
- self.content.set_miter_limit(miter_limit.0 as f32);
- }
- self.state.stroke = Some(stroke.clone());
- }
- }
-
- 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);
- }
- }
-
- fn reset_stroke_color_space(&mut self) {
- self.state.stroke_space = None;
- }
-}
-
-/// Encode a frame into the content stream.
-fn write_frame(ctx: &mut PageContext, frame: &Frame) {
- for &(pos, ref item) in frame.items() {
- let x = pos.x.to_f32();
- let y = pos.y.to_f32();
- match item {
- FrameItem::Group(group) => write_group(ctx, pos, group),
- FrameItem::Text(text) => write_text(ctx, x, y, text),
- FrameItem::Shape(shape, _) => write_shape(ctx, x, y, shape),
- FrameItem::Image(image, size, _) => write_image(ctx, x, y, image, *size),
- FrameItem::Meta(meta, size) => match meta {
- Meta::Link(dest) => write_link(ctx, pos, dest, *size),
- Meta::Elem(_) => {}
- Meta::Hide => {}
- Meta::PageNumbering(_) => {}
- },
- }
- }
-}
-
-/// Encode a group into the content stream.
-fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) {
- 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: &TextItem) {
- *ctx.parent.languages.entry(text.lang).or_insert(0) += text.glyphs.len();
-
- let glyph_set = ctx.parent.glyph_sets.entry(text.font.clone()).or_default();
- for g in &text.glyphs {
- let segment = &text.text[g.range()];
- glyph_set.entry(g.id).or_insert_with(|| segment.into());
- }
-
- ctx.set_fill(&text.fill);
- ctx.set_font(&text.font, text.size);
- ctx.content.begin_text();
-
- // Positiosn 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) = text.font.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) {
- let stroke = shape.stroke.as_ref().and_then(|stroke| {
- if stroke.thickness.to_f32() > 0.0 {
- Some(stroke)
- } else {
- None
- }
- });
-
- if shape.fill.is_none() && stroke.is_none() {
- return;
- }
-
- if let Some(fill) = &shape.fill {
- ctx.set_fill(fill);
- }
-
- if let Some(stroke) = stroke {
- ctx.set_stroke(stroke);
- }
-
- match shape.geometry {
- 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::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::Path(ref path) => {
- write_path(ctx, x, y, path);
- }
- }
-
- match (&shape.fill, 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::PathItem::MoveTo(p) => {
- ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32())
- }
- geom::PathItem::LineTo(p) => {
- ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32())
- }
- geom::PathItem::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::PathItem::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, image: &Image, size: Size) {
- ctx.parent.image_map.insert(image.clone());
- let name = eco_format!("Im{}", ctx.parent.image_map.map(image.clone()));
- 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]);
-
- if let Some(alt) = image.alt() {
- let mut image_span =
- ctx.content.begin_marked_content_with_properties(Name(b"Span"));
- let mut image_alt = image_span.properties();
- image_alt.pair(Name(b"Alt"), pdf_writer::Str(alt.as_bytes()));
- image_alt.finish();
- image_span.finish();
-
- ctx.content.x_object(Name(name.as_bytes()));
- ctx.content.end_marked_content();
- } else {
- 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 = Abs::inf();
- let mut min_y = Abs::inf();
- let mut max_x = -Abs::inf();
- let mut max_y = -Abs::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));
-}
-
-impl From<&LineCap> for LineCapStyle {
- fn from(line_cap: &LineCap) -> Self {
- match line_cap {
- LineCap::Butt => LineCapStyle::ButtCap,
- LineCap::Round => LineCapStyle::RoundCap,
- LineCap::Square => LineCapStyle::ProjectingSquareCap,
- }
- }
-}
-
-impl From<&LineJoin> for LineJoinStyle {
- fn from(line_join: &LineJoin) -> Self {
- match line_join {
- LineJoin::Miter => LineJoinStyle::MiterJoin,
- LineJoin::Round => LineJoinStyle::RoundJoin,
- LineJoin::Bevel => LineJoinStyle::BevelJoin,
- }
- }
-}
diff --git a/src/export/render.rs b/src/export/render.rs
deleted file mode 100644
index d8115b12..00000000
--- a/src/export/render.rs
+++ /dev/null
@@ -1,673 +0,0 @@
-//! Rendering into raster images.
-
-use std::io::Read;
-use std::sync::Arc;
-
-use image::imageops::FilterType;
-use image::{GenericImageView, Rgba};
-use pixglyph::Bitmap;
-use resvg::FitTo;
-use tiny_skia as sk;
-use ttf_parser::{GlyphId, OutlineBuilder};
-use usvg::{NodeExt, TreeParsing};
-
-use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem};
-use crate::font::Font;
-use crate::geom::{
- self, Abs, Color, Geometry, LineCap, LineJoin, Paint, PathItem, Shape, Size, Stroke,
- Transform,
-};
-use crate::image::{DecodedImage, Image};
-
-/// Export a frame into a raster image.
-///
-/// This renders the frame at the given number of pixels per point and returns
-/// the resulting `tiny-skia` pixel buffer.
-pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap {
- let size = frame.size();
- let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32;
- let pxh = (pixel_per_pt * size.y.to_f32()).round().max(1.0) as u32;
-
- let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap();
- canvas.fill(fill.into());
-
- let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt);
- render_frame(&mut canvas, ts, None, frame);
-
- canvas
-}
-
-/// Render a frame into the canvas.
-fn render_frame(
- canvas: &mut sk::Pixmap,
- ts: sk::Transform,
- mask: Option<&sk::Mask>,
- frame: &Frame,
-) {
- for (pos, item) in frame.items() {
- let x = pos.x.to_f32();
- let y = pos.y.to_f32();
- let ts = ts.pre_translate(x, y);
-
- match item {
- FrameItem::Group(group) => {
- render_group(canvas, ts, mask, group);
- }
- FrameItem::Text(text) => {
- render_text(canvas, ts, mask, text);
- }
- FrameItem::Shape(shape, _) => {
- render_shape(canvas, ts, mask, shape);
- }
- FrameItem::Image(image, size, _) => {
- render_image(canvas, ts, mask, image, *size);
- }
- FrameItem::Meta(meta, _) => match meta {
- Meta::Link(_) => {}
- Meta::Elem(_) => {}
- Meta::PageNumbering(_) => {}
- Meta::Hide => {}
- },
- }
- }
-}
-
-/// Render a group frame with optional transform and clipping into the canvas.
-fn render_group(
- canvas: &mut sk::Pixmap,
- ts: sk::Transform,
- mask: Option<&sk::Mask>,
- group: &GroupItem,
-) {
- let ts = ts.pre_concat(group.transform.into());
-
- let mut mask = mask;
- let storage;
- if group.clips {
- let size = group.frame.size();
- let w = size.x.to_f32();
- let h = size.y.to_f32();
- if let Some(path) = sk::Rect::from_xywh(0.0, 0.0, w, h)
- .map(sk::PathBuilder::from_rect)
- .and_then(|path| path.transform(ts))
- {
- if let Some(mask) = mask {
- let mut mask = mask.clone();
- mask.intersect_path(
- &path,
- sk::FillRule::default(),
- false,
- sk::Transform::default(),
- );
- storage = mask;
- } else {
- let pxw = canvas.width();
- let pxh = canvas.height();
- let Some(mut mask) = sk::Mask::new(pxw, pxh) else {
- // Fails if clipping rect is empty. In that case we just
- // clip everything by returning.
- return;
- };
-
- mask.fill_path(
- &path,
- sk::FillRule::default(),
- false,
- sk::Transform::default(),
- );
- storage = mask;
- };
-
- mask = Some(&storage);
- }
- }
-
- render_frame(canvas, ts, mask, &group.frame);
-}
-
-/// Render a text run into the canvas.
-fn render_text(
- canvas: &mut sk::Pixmap,
- ts: sk::Transform,
- mask: Option<&sk::Mask>,
- text: &TextItem,
-) {
- let mut x = 0.0;
- for glyph in &text.glyphs {
- let id = GlyphId(glyph.id);
- let offset = x + glyph.x_offset.at(text.size).to_f32();
- let ts = ts.pre_translate(offset, 0.0);
-
- render_svg_glyph(canvas, ts, mask, text, id)
- .or_else(|| render_bitmap_glyph(canvas, ts, mask, text, id))
- .or_else(|| render_outline_glyph(canvas, ts, mask, text, id));
-
- x += glyph.x_advance.at(text.size).to_f32();
- }
-}
-
-/// Render an SVG glyph into the canvas.
-fn render_svg_glyph(
- canvas: &mut sk::Pixmap,
- ts: sk::Transform,
- mask: Option<&sk::Mask>,
- text: &TextItem,
- id: GlyphId,
-) -> Option<()> {
- let mut data = text.font.ttf().glyph_svg_image(id)?;
-
- // Decompress SVGZ.
- let mut decoded = vec![];
- if data.starts_with(&[0x1f, 0x8b]) {
- let mut decoder = flate2::read::GzDecoder::new(data);
- decoder.read_to_end(&mut decoded).ok()?;
- data = &decoded;
- }
-
- // Parse XML.
- let xml = std::str::from_utf8(data).ok()?;
- let document = roxmltree::Document::parse(xml).ok()?;
- let root = document.root_element();
-
- // Parse SVG.
- let opts = usvg::Options::default();
- let tree = usvg::Tree::from_xmltree(&document, &opts).ok()?;
- let view_box = tree.view_box.rect;
-
- // If there's no viewbox defined, use the em square for our scale
- // transformation ...
- let upem = text.font.units_per_em() as f32;
- let (mut width, mut height) = (upem, upem);
-
- // ... but if there's a viewbox or width, use that.
- if root.has_attribute("viewBox") || root.has_attribute("width") {
- width = view_box.width() as f32;
- }
-
- // Same as for width.
- if root.has_attribute("viewBox") || root.has_attribute("height") {
- height = view_box.height() as f32;
- }
-
- let size = text.size.to_f32();
- let ts = ts.pre_scale(size / width, size / height);
-
- // Compute the space we need to draw our glyph.
- // See https://github.com/RazrFalcon/resvg/issues/602 for why
- // using the svg size is problematic here.
- let mut bbox = usvg::Rect::new_bbox();
- for node in tree.root.descendants() {
- if let Some(rect) = node.calculate_bbox().and_then(|b| b.to_rect()) {
- bbox = bbox.expand(rect);
- }
- }
-
- let canvas_rect = usvg::ScreenRect::new(0, 0, canvas.width(), canvas.height())?;
-
- // Compute the bbox after the transform is applied.
- // We add a nice 5px border along the bounding box to
- // be on the safe size. We also compute the intersection
- // with the canvas rectangle
- let svg_ts = usvg::Transform::new(
- ts.sx.into(),
- ts.kx.into(),
- ts.ky.into(),
- ts.sy.into(),
- ts.tx.into(),
- ts.ty.into(),
- );
- let bbox = bbox.transform(&svg_ts)?.to_screen_rect();
- let bbox = usvg::ScreenRect::new(
- bbox.left() - 5,
- bbox.y() - 5,
- bbox.width() + 10,
- bbox.height() + 10,
- )?
- .fit_to_rect(canvas_rect);
-
- let mut pixmap = sk::Pixmap::new(bbox.width(), bbox.height())?;
-
- // We offset our transform so that the pixmap starts at the edge of the bbox.
- let ts = ts.post_translate(-bbox.left() as f32, -bbox.top() as f32);
- resvg::render(&tree, FitTo::Original, ts, pixmap.as_mut())?;
-
- canvas.draw_pixmap(
- bbox.left(),
- bbox.top(),
- pixmap.as_ref(),
- &sk::PixmapPaint::default(),
- sk::Transform::identity(),
- mask,
- );
-
- Some(())
-}
-
-/// Render a bitmap glyph into the canvas.
-fn render_bitmap_glyph(
- canvas: &mut sk::Pixmap,
- ts: sk::Transform,
- mask: Option<&sk::Mask>,
- text: &TextItem,
- id: GlyphId,
-) -> Option<()> {
- let size = text.size.to_f32();
- let ppem = size * ts.sy;
- let raster = text.font.ttf().glyph_raster_image(id, ppem as u16)?;
- let image = Image::new(raster.data.into(), raster.format.into(), None).ok()?;
-
- // FIXME: Vertical alignment isn't quite right for Apple Color Emoji,
- // and maybe also for Noto Color Emoji. And: Is the size calculation
- // correct?
- let h = text.size;
- let w = (image.width() as f64 / image.height() as f64) * h;
- let dx = (raster.x as f32) / (image.width() as f32) * size;
- let dy = (raster.y as f32) / (image.height() as f32) * size;
- let ts = ts.pre_translate(dx, -size - dy);
- render_image(canvas, ts, mask, &image, Size::new(w, h))
-}
-
-/// Render an outline glyph into the canvas. This is the "normal" case.
-fn render_outline_glyph(
- canvas: &mut sk::Pixmap,
- ts: sk::Transform,
- mask: Option<&sk::Mask>,
- text: &TextItem,
- id: GlyphId,
-) -> Option<()> {
- let ppem = text.size.to_f32() * ts.sy;
-
- // Render a glyph directly as a path. This only happens when the fast glyph
- // rasterization can't be used due to very large text size or weird
- // scale/skewing transforms.
- if ppem > 100.0 || ts.kx != 0.0 || ts.ky != 0.0 || ts.sx != ts.sy {
- let path = {
- let mut builder = WrappedPathBuilder(sk::PathBuilder::new());
- text.font.ttf().outline_glyph(id, &mut builder)?;
- builder.0.finish()?
- };
-
- let paint = (&text.fill).into();
- let rule = sk::FillRule::default();
-
- // Flip vertically because font design coordinate
- // system is Y-up.
- let scale = text.size.to_f32() / text.font.units_per_em() as f32;
- let ts = ts.pre_scale(scale, -scale);
- canvas.fill_path(&path, &paint, rule, ts, mask);
- return Some(());
- }
-
- // Rasterize the glyph with `pixglyph`.
- #[comemo::memoize]
- fn rasterize(
- font: &Font,
- id: GlyphId,
- x: u32,
- y: u32,
- size: u32,
- ) -> Option<Arc<Bitmap>> {
- let glyph = pixglyph::Glyph::load(font.ttf(), id)?;
- Some(Arc::new(glyph.rasterize(
- f32::from_bits(x),
- f32::from_bits(y),
- f32::from_bits(size),
- )))
- }
-
- // Try to retrieve a prepared glyph or prepare it from scratch if it
- // doesn't exist, yet.
- let bitmap =
- rasterize(&text.font, id, ts.tx.to_bits(), ts.ty.to_bits(), ppem.to_bits())?;
-
- // If we have a clip mask we first render to a pixmap that we then blend
- // with our canvas
- if mask.is_some() {
- let mw = bitmap.width;
- let mh = bitmap.height;
-
- let Paint::Solid(color) = text.fill;
- let c = color.to_rgba();
-
- // Pad the pixmap with 1 pixel in each dimension so that we do
- // not get any problem with floating point errors along their border
- let mut pixmap = sk::Pixmap::new(mw + 2, mh + 2)?;
- for x in 0..mw {
- for y in 0..mh {
- let alpha = bitmap.coverage[(y * mw + x) as usize];
- let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, alpha).premultiply();
- pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] = color;
- }
- }
-
- let left = bitmap.left;
- let top = bitmap.top;
-
- canvas.draw_pixmap(
- left - 1,
- top - 1,
- pixmap.as_ref(),
- &sk::PixmapPaint::default(),
- sk::Transform::identity(),
- mask,
- );
- } else {
- let cw = canvas.width() as i32;
- let ch = canvas.height() as i32;
- let mw = bitmap.width as i32;
- let mh = bitmap.height as i32;
-
- // Determine the pixel bounding box that we actually need to draw.
- let left = bitmap.left;
- let right = left + mw;
- let top = bitmap.top;
- let bottom = top + mh;
-
- // Premultiply the text color.
- let Paint::Solid(color) = text.fill;
- let c = color.to_rgba();
- let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, 255).premultiply().get();
-
- // Blend the glyph bitmap with the existing pixels on the canvas.
- let pixels = bytemuck::cast_slice_mut::<u8, u32>(canvas.data_mut());
- for x in left.clamp(0, cw)..right.clamp(0, cw) {
- for y in top.clamp(0, ch)..bottom.clamp(0, ch) {
- let ai = ((y - top) * mw + (x - left)) as usize;
- let cov = bitmap.coverage[ai];
- if cov == 0 {
- continue;
- }
-
- let pi = (y * cw + x) as usize;
- if cov == 255 {
- pixels[pi] = color;
- continue;
- }
-
- let applied = alpha_mul(color, cov as u32);
- pixels[pi] = blend_src_over(applied, pixels[pi]);
- }
- }
- }
-
- Some(())
-}
-
-/// Render a geometrical shape into the canvas.
-fn render_shape(
- canvas: &mut sk::Pixmap,
- ts: sk::Transform,
- mask: Option<&sk::Mask>,
- shape: &Shape,
-) -> Option<()> {
- let path = match shape.geometry {
- Geometry::Line(target) => {
- let mut builder = sk::PathBuilder::new();
- builder.line_to(target.x.to_f32(), target.y.to_f32());
- builder.finish()?
- }
- Geometry::Rect(size) => {
- let w = size.x.to_f32();
- let h = size.y.to_f32();
- let rect = sk::Rect::from_xywh(0.0, 0.0, w, h)?;
- sk::PathBuilder::from_rect(rect)
- }
- Geometry::Path(ref path) => convert_path(path)?,
- };
-
- if let Some(fill) = &shape.fill {
- let mut paint: sk::Paint = fill.into();
- if matches!(shape.geometry, Geometry::Rect(_)) {
- paint.anti_alias = false;
- }
-
- let rule = sk::FillRule::default();
- canvas.fill_path(&path, &paint, rule, ts, mask);
- }
-
- if let Some(Stroke {
- paint,
- thickness,
- line_cap,
- line_join,
- dash_pattern,
- miter_limit,
- }) = &shape.stroke
- {
- let width = thickness.to_f32();
-
- // Don't draw zero-pt stroke.
- if width > 0.0 {
- let dash = dash_pattern.as_ref().and_then(|pattern| {
- // tiny-skia only allows dash patterns with an even number of elements,
- // while pdf allows any number.
- let pattern_len = pattern.array.len();
- let len =
- if pattern_len % 2 == 1 { 2 * pattern_len } else { pattern_len };
- let dash_array =
- pattern.array.iter().map(|l| l.to_f32()).cycle().take(len).collect();
-
- sk::StrokeDash::new(dash_array, pattern.phase.to_f32())
- });
- let paint = paint.into();
- let stroke = sk::Stroke {
- width,
- line_cap: line_cap.into(),
- line_join: line_join.into(),
- dash,
- miter_limit: miter_limit.0 as f32,
- };
- canvas.stroke_path(&path, &paint, &stroke, ts, mask);
- }
- }
-
- Some(())
-}
-
-/// Convert a Typst path into a tiny-skia path.
-fn convert_path(path: &geom::Path) -> Option<sk::Path> {
- let mut builder = sk::PathBuilder::new();
- for elem in &path.0 {
- match elem {
- PathItem::MoveTo(p) => {
- builder.move_to(p.x.to_f32(), p.y.to_f32());
- }
- PathItem::LineTo(p) => {
- builder.line_to(p.x.to_f32(), p.y.to_f32());
- }
- PathItem::CubicTo(p1, p2, p3) => {
- builder.cubic_to(
- p1.x.to_f32(),
- p1.y.to_f32(),
- p2.x.to_f32(),
- p2.y.to_f32(),
- p3.x.to_f32(),
- p3.y.to_f32(),
- );
- }
- PathItem::ClosePath => {
- builder.close();
- }
- };
- }
- builder.finish()
-}
-
-/// Render a raster or SVG image into the canvas.
-fn render_image(
- canvas: &mut sk::Pixmap,
- ts: sk::Transform,
- mask: Option<&sk::Mask>,
- image: &Image,
- size: Size,
-) -> Option<()> {
- let view_width = size.x.to_f32();
- let view_height = size.y.to_f32();
-
- // For better-looking output, resize `image` to its final size before
- // painting it to `canvas`. For the math, see:
- // https://github.com/typst/typst/issues/1404#issuecomment-1598374652
- let theta = f32::atan2(-ts.kx, ts.sx);
-
- // To avoid division by 0, choose the one of { sin, cos } that is
- // further from 0.
- let prefer_sin = theta.sin().abs() > std::f32::consts::FRAC_1_SQRT_2;
- let scale_x =
- f32::abs(if prefer_sin { ts.kx / theta.sin() } else { ts.sx / theta.cos() });
-
- let aspect = (image.width() as f32) / (image.height() as f32);
- let w = (scale_x * view_width.max(aspect * view_height)).ceil() as u32;
- let h = ((w as f32) / aspect).ceil() as u32;
-
- let pixmap = scaled_texture(image, w, h)?;
- let paint_scale_x = view_width / pixmap.width() as f32;
- let paint_scale_y = view_height / pixmap.height() as f32;
-
- let paint = sk::Paint {
- shader: sk::Pattern::new(
- (*pixmap).as_ref(),
- sk::SpreadMode::Pad,
- sk::FilterQuality::Nearest,
- 1.0,
- sk::Transform::from_scale(paint_scale_x, paint_scale_y),
- ),
- ..Default::default()
- };
-
- let rect = sk::Rect::from_xywh(0.0, 0.0, view_width, view_height)?;
- canvas.fill_rect(rect, &paint, ts, mask);
-
- Some(())
-}
-
-/// Prepare a texture for an image at a scaled size.
-#[comemo::memoize]
-fn scaled_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
- let mut pixmap = sk::Pixmap::new(w, h)?;
- match image.decoded().as_ref() {
- DecodedImage::Raster(dynamic, _, _) => {
- let downscale = w < image.width();
- let filter =
- if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom };
- let buf = dynamic.resize(w, h, filter);
- for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) {
- let Rgba([r, g, b, a]) = src;
- *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
- }
- }
- DecodedImage::Svg(tree) => {
- resvg::render(
- tree,
- FitTo::Size(w, h),
- sk::Transform::identity(),
- pixmap.as_mut(),
- )?;
- }
- }
- Some(Arc::new(pixmap))
-}
-
-impl From<Transform> for sk::Transform {
- fn from(transform: Transform) -> Self {
- let Transform { sx, ky, kx, sy, tx, ty } = transform;
- sk::Transform::from_row(
- sx.get() as _,
- ky.get() as _,
- kx.get() as _,
- sy.get() as _,
- tx.to_f32(),
- ty.to_f32(),
- )
- }
-}
-
-impl From<&Paint> for sk::Paint<'static> {
- fn from(paint: &Paint) -> Self {
- let mut sk_paint = sk::Paint::default();
- let Paint::Solid(color) = *paint;
- sk_paint.set_color(color.into());
- sk_paint.anti_alias = true;
- sk_paint
- }
-}
-
-impl From<Color> for sk::Color {
- fn from(color: Color) -> Self {
- let c = color.to_rgba();
- sk::Color::from_rgba8(c.r, c.g, c.b, c.a)
- }
-}
-
-impl From<&LineCap> for sk::LineCap {
- fn from(line_cap: &LineCap) -> Self {
- match line_cap {
- LineCap::Butt => sk::LineCap::Butt,
- LineCap::Round => sk::LineCap::Round,
- LineCap::Square => sk::LineCap::Square,
- }
- }
-}
-
-impl From<&LineJoin> for sk::LineJoin {
- fn from(line_join: &LineJoin) -> Self {
- match line_join {
- LineJoin::Miter => sk::LineJoin::Miter,
- LineJoin::Round => sk::LineJoin::Round,
- LineJoin::Bevel => sk::LineJoin::Bevel,
- }
- }
-}
-
-/// Allows to build tiny-skia paths from glyph outlines.
-struct WrappedPathBuilder(sk::PathBuilder);
-
-impl OutlineBuilder for WrappedPathBuilder {
- fn move_to(&mut self, x: f32, y: f32) {
- self.0.move_to(x, y);
- }
-
- fn line_to(&mut self, x: f32, y: f32) {
- self.0.line_to(x, y);
- }
-
- fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
- self.0.quad_to(x1, y1, x, y);
- }
-
- fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
- self.0.cubic_to(x1, y1, x2, y2, x, y);
- }
-
- fn close(&mut self) {
- self.0.close();
- }
-}
-
-/// Additional methods for [`Length`].
-trait AbsExt {
- /// Convert to a number of points as f32.
- fn to_f32(self) -> f32;
-}
-
-impl AbsExt for Abs {
- fn to_f32(self) -> f32 {
- self.to_pt() as f32
- }
-}
-
-// Alpha multiplication and blending are ported from:
-// https://skia.googlesource.com/skia/+/refs/heads/main/include/core/SkColorPriv.h
-
-/// Blends two premulitplied, packed 32-bit RGBA colors. Alpha channel must be
-/// in the 8 high bits.
-fn blend_src_over(src: u32, dst: u32) -> u32 {
- src + alpha_mul(dst, 256 - (src >> 24))
-}
-
-/// Alpha multiply a color.
-fn alpha_mul(color: u32, scale: u32) -> u32 {
- let mask = 0xff00ff;
- let rb = ((color & mask) * scale) >> 8;
- let ag = ((color >> 8) & mask) * scale;
- (rb & mask) | (ag & !mask)
-}