diff options
Diffstat (limited to 'src/export')
| -rw-r--r-- | src/export/mod.rs | 7 | ||||
| -rw-r--r-- | src/export/pdf/font.rs | 204 | ||||
| -rw-r--r-- | src/export/pdf/image.rs | 143 | ||||
| -rw-r--r-- | src/export/pdf/mod.rs | 235 | ||||
| -rw-r--r-- | src/export/pdf/outline.rs | 127 | ||||
| -rw-r--r-- | src/export/pdf/page.rs | 565 | ||||
| -rw-r--r-- | src/export/render.rs | 673 |
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) -} |
