diff --git a/Cargo.toml b/Cargo.toml index bec1306fa..88a15742b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ fxhash = "0.2.1" image = { version = "0.23", default-features = false, features = ["png", "jpeg"] } itertools = "0.10" miniz_oxide = "0.4" -pdf-writer = { git = "https://github.com/typst/pdf-writer", rev = "f446079" } +pdf-writer = { git = "https://github.com/typst/pdf-writer", rev = "27b207a" } rustybuzz = "0.4" serde = { version = "1", features = ["derive", "rc"] } ttf-parser = "0.12" diff --git a/src/export/pdf.rs b/src/export/pdf.rs index b12ee7716..7de34905c 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -15,7 +15,7 @@ use ttf_parser::{name_id, GlyphId, Tag}; use super::subset; use crate::font::{find_name, FaceId, FontStore}; use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; -use crate::geom::{self, Color, Em, Length, Paint, Size}; +use crate::geom::{self, Color, Em, Length, Paint, Point, Size, Transform}; use crate::image::{Image, ImageId, ImageStore}; use crate::Context; @@ -27,144 +27,63 @@ use crate::Context; /// /// Returns the raw bytes making up the PDF file. pub fn pdf(ctx: &Context, frames: &[Rc]) -> Vec { - PdfExporter::new(ctx, frames).write() + PdfExporter::new(ctx).export(frames) } +/// An exporter for a whole PDF document. struct PdfExporter<'a> { - writer: PdfWriter, - refs: Refs, - frames: &'a [Rc], fonts: &'a FontStore, images: &'a ImageStore, - font_map: Remapper, + writer: PdfWriter, + alloc: Ref, + pages: Vec, + face_map: Remapper, + face_refs: Vec, + glyph_sets: HashMap>, image_map: Remapper, - glyphs: HashMap>, + image_refs: Vec, } impl<'a> PdfExporter<'a> { - fn new(ctx: &'a Context, frames: &'a [Rc]) -> Self { - let mut font_map = Remapper::new(); - let mut image_map = Remapper::new(); - let mut glyphs = HashMap::>::new(); - let mut alpha_masks = 0; - - for frame in frames { - for (_, element) in frame.elements() { - match *element { - Element::Text(ref text) => { - font_map.insert(text.face_id); - let set = glyphs.entry(text.face_id).or_default(); - set.extend(text.glyphs.iter().map(|g| g.id)); - } - Element::Image(id, _) => { - let img = ctx.images.get(id); - if img.buf.color().has_alpha() { - alpha_masks += 1; - } - image_map.insert(id); - } - _ => {} - } - } - } - + fn new(ctx: &'a Context) -> Self { Self { - writer: PdfWriter::new(), - refs: Refs::new(frames.len(), font_map.len(), image_map.len(), alpha_masks), - frames, fonts: &ctx.fonts, images: &ctx.images, - glyphs, - font_map, - image_map, + writer: PdfWriter::new(), + alloc: Ref::new(1), + pages: vec![], + face_map: Remapper::new(), + face_refs: vec![], + glyph_sets: HashMap::new(), + image_map: Remapper::new(), + image_refs: vec![], } } - fn write(mut self) -> Vec { - self.write_structure(); - self.write_pages(); + fn export(mut self, frames: &[Rc]) -> Vec { + self.build_pages(frames); self.write_fonts(); self.write_images(); - self.writer.finish(self.refs.catalog) + self.write_structure() } - fn write_structure(&mut self) { - // The document catalog. - self.writer.catalog(self.refs.catalog).pages(self.refs.page_tree); - - // The root page tree. - let mut pages = self.writer.pages(self.refs.page_tree); - pages.kids(self.refs.pages()); - - let mut resources = pages.resources(); - let mut fonts = resources.fonts(); - for (refs, f) in self.refs.fonts().zip(self.font_map.pdf_indices()) { - let name = format_eco!("F{}", f); - fonts.pair(Name(name.as_bytes()), refs.type0_font); + fn build_pages(&mut self, frames: &[Rc]) { + for frame in frames { + let page = PageExporter::new(self).export(frame); + self.pages.push(page); } - - fonts.finish(); - - let mut images = resources.x_objects(); - for (id, im) in self.refs.images().zip(self.image_map.pdf_indices()) { - let name = format_eco!("Im{}", im); - images.pair(Name(name.as_bytes()), id); - } - - images.finish(); - resources.finish(); - pages.finish(); - - // The page objects (non-root nodes in the page tree). - for ((page_id, content_id), page) in - self.refs.pages().zip(self.refs.contents()).zip(self.frames) - { - let w = page.size.w.to_f32(); - let h = page.size.h.to_f32(); - - let mut page_writer = self.writer.page(page_id); - page_writer - .parent(self.refs.page_tree) - .media_box(Rect::new(0.0, 0.0, w, h)); - - let mut annotations = page_writer.annotations(); - for (pos, element) in page.elements() { - if let Element::Link(href, size) = element { - let x = pos.x.to_f32(); - let y = (page.size.h - pos.y).to_f32(); - let w = size.w.to_f32(); - let h = size.h.to_f32(); - - annotations - .push() - .subtype(AnnotationType::Link) - .rect(Rect::new(x, y - h, x + w, y)) - .action() - .action_type(ActionType::Uri) - .uri(Str(href.as_bytes())); - } - } - - annotations.finish(); - page_writer.contents(content_id); - } - } - - fn write_pages(&mut self) { - for (id, page) in self.refs.contents().zip(self.frames) { - self.write_page(id, page); - } - } - - fn write_page(&mut self, id: Ref, page: &'a Frame) { - let writer = PageExporter::new(self); - let content = writer.write(page); - self.writer.stream(id, &deflate(&content)).filter(Filter::FlateDecode); } fn write_fonts(&mut self) { - for (refs, face_id) in self.refs.fonts().zip(self.font_map.layout_indices()) { - let glyphs = &self.glyphs[&face_id]; + for face_id in self.face_map.layout_indices() { + let type0_ref = self.alloc.bump(); + let cid_ref = self.alloc.bump(); + let descriptor_ref = self.alloc.bump(); + let cmap_ref = self.alloc.bump(); + let data_ref = self.alloc.bump(); + self.face_refs.push(type0_ref); + + let glyphs = &self.glyph_sets[&face_id]; let face = self.fonts.get(face_id); let ttf = face.ttf(); @@ -182,11 +101,11 @@ impl<'a> PdfExporter<'a> { // Write the base font object referencing the CID font. self.writer - .type0_font(refs.type0_font) + .type0_font(type0_ref) .base_font(base_font) .encoding_predefined(Name(b"Identity-H")) - .descendant_font(refs.cid_font) - .to_unicode(refs.cmap); + .descendant_font(cid_ref) + .to_unicode(cmap_ref); // Check for the presence of CFF outlines to select the correct // CID-Font subtype. @@ -200,10 +119,10 @@ impl<'a> PdfExporter<'a> { // Write the CID font referencing the font descriptor. self.writer - .cid_font(refs.cid_font, subtype) + .cid_font(cid_ref, subtype) .base_font(base_font) .system_info(system_info) - .font_descriptor(refs.font_descriptor) + .font_descriptor(descriptor_ref) .cid_to_gid_map_predefined(Name(b"Identity")) .widths() .individual(0, { @@ -237,7 +156,7 @@ impl<'a> PdfExporter<'a> { // Write the font descriptor (contains metrics about the font). self.writer - .font_descriptor(refs.font_descriptor) + .font_descriptor(descriptor_ref) .font_name(base_font) .font_flags(flags) .font_bbox(bbox) @@ -246,7 +165,7 @@ impl<'a> PdfExporter<'a> { .descent(descender) .cap_height(cap_height) .stem_v(stem_v) - .font_file2(refs.data); + .font_file2(data_ref); // Compute a reverse mapping from glyphs to unicode. let cmap = { @@ -275,7 +194,7 @@ impl<'a> PdfExporter<'a> { // Write the /ToUnicode character map, which maps glyph ids back to // unicode codepoints to enable copying out of the PDF. self.writer - .cmap(refs.cmap, &deflate(&cmap.finish())) + .cmap(cmap_ref, &deflate(&cmap.finish())) .filter(Filter::FlateDecode); // Subset and write the face's bytes. @@ -283,21 +202,23 @@ impl<'a> PdfExporter<'a> { let subsetted = subset(buffer, face.index(), glyphs); let data = subsetted.as_deref().unwrap_or(buffer); self.writer - .stream(refs.data, &deflate(data)) + .stream(data_ref, &deflate(data)) .filter(Filter::FlateDecode); } } fn write_images(&mut self) { - let mut masks_seen = 0; + for image_id in self.image_map.layout_indices() { + let image_ref = self.alloc.bump(); + self.image_refs.push(image_ref); - for (id, image_id) in self.refs.images().zip(self.image_map.layout_indices()) { let img = self.images.get(image_id); - let (width, height) = img.buf.dimensions(); + let width = img.width(); + let height = img.height(); // Add the primary image. if let Ok((data, filter, color_space)) = encode_image(img) { - let mut image = self.writer.image(id, &data); + let mut image = self.writer.image(image_ref, &data); image.filter(filter); image.width(width as i32); image.height(height as i32); @@ -308,23 +229,21 @@ impl<'a> PdfExporter<'a> { // this image has an alpha channel. if img.buf.color().has_alpha() { let (alpha_data, alpha_filter) = encode_alpha(img); - let mask_id = self.refs.alpha_mask(masks_seen); - image.s_mask(mask_id); + let mask_ref = self.alloc.bump(); + image.s_mask(mask_ref); image.finish(); - let mut mask = self.writer.image(mask_id, &alpha_data); + let mut mask = self.writer.image(mask_ref, &alpha_data); mask.filter(alpha_filter); mask.width(width as i32); mask.height(height as i32); mask.color_space(ColorSpace::DeviceGray); mask.bits_per_component(8); - - masks_seen += 1; } } else { // TODO: Warn that image could not be encoded. self.writer - .image(id, &[]) + .image(image_ref, &[]) .width(0) .height(0) .color_space(ColorSpace::DeviceGray) @@ -332,117 +251,180 @@ impl<'a> PdfExporter<'a> { } } } + + fn write_structure(mut self) -> Vec { + // The root page tree. + let page_tree_ref = self.alloc.bump(); + + // The page objects (non-root nodes in the page tree). + let mut page_refs = vec![]; + for page in self.pages { + let page_id = self.alloc.bump(); + let content_id = self.alloc.bump(); + page_refs.push(page_id); + + let mut page_writer = self.writer.page(page_id); + page_writer.parent(page_tree_ref); + + let w = page.size.w.to_f32(); + let h = page.size.h.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 (url, rect) in page.links { + annotations + .push() + .subtype(AnnotationType::Link) + .rect(rect) + .action() + .action_type(ActionType::Uri) + .uri(Str(url.as_bytes())); + } + + annotations.finish(); + page_writer.finish(); + + self.writer + .stream(content_id, &deflate(&page.content.finish())) + .filter(Filter::FlateDecode); + } + + let mut pages = self.writer.pages(page_tree_ref); + pages.kids(page_refs); + + let mut resources = pages.resources(); + let mut fonts = resources.fonts(); + for (font_ref, f) in self.face_map.pdf_indices(&self.face_refs) { + let name = format_eco!("F{}", f); + fonts.pair(Name(name.as_bytes()), font_ref); + } + + fonts.finish(); + + let mut images = resources.x_objects(); + for (image_ref, im) in self.image_map.pdf_indices(&self.image_refs) { + let name = format_eco!("Im{}", im); + images.pair(Name(name.as_bytes()), image_ref); + } + + images.finish(); + resources.finish(); + pages.finish(); + + // The document catalog. + let catalog_ref = self.alloc.bump(); + self.writer.catalog(catalog_ref).pages(page_tree_ref); + self.writer.finish(catalog_ref) + } } -/// A writer for the contents of a single page. +/// An exporter for the contents of a single PDF page. struct PageExporter<'a> { fonts: &'a FontStore, - font_map: &'a Remapper, - image_map: &'a Remapper, + font_map: &'a mut Remapper, + image_map: &'a mut Remapper, + glyphs: &'a mut HashMap>, + bottom: f32, content: Content, - in_text_state: bool, - face_id: Option, - font_size: Length, - font_fill: Option, + links: Vec<(String, Rect)>, + state: State, + saves: Vec, +} + +/// Data for an exported page. +struct Page { + size: Size, + content: Content, + links: Vec<(String, 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, + fill: Option, + stroke: Option, + font: Option<(FaceId, Length)>, } impl<'a> PageExporter<'a> { - /// Create a new page exporter. - fn new(exporter: &'a PdfExporter) -> Self { + fn new(exporter: &'a mut PdfExporter) -> Self { Self { fonts: exporter.fonts, - font_map: &exporter.font_map, - image_map: &exporter.image_map, + font_map: &mut exporter.face_map, + image_map: &mut exporter.image_map, + glyphs: &mut exporter.glyph_sets, + bottom: 0.0, content: Content::new(), - in_text_state: false, - face_id: None, - font_size: Length::zero(), - font_fill: None, + links: vec![], + state: State::default(), + saves: vec![], } } - /// Write the page frame into the content stream. - fn write(mut self, frame: &Frame) -> Vec { - self.write_frame(0.0, frame.size.h.to_f32(), frame); - self.content.finish() + fn export(mut self, frame: &Frame) -> Page { + // Make the coordinate system start at the top-left. + self.bottom = frame.size.h.to_f32(); + self.content.transform([1.0, 0.0, 0.0, -1.0, 0.0, self.bottom]); + self.write_frame(&frame); + Page { + size: frame.size, + content: self.content, + links: self.links, + } } - /// Write a frame into the content stream. - fn write_frame(&mut self, x: f32, y: f32, frame: &Frame) { - for (offset, element) in &frame.elements { - // Make sure the content stream is in the correct state. - match element { - Element::Text(_) if !self.in_text_state => { - self.content.begin_text(); - self.in_text_state = true; - } - - Element::Shape(_) | Element::Image(..) if self.in_text_state => { - self.content.end_text(); - self.in_text_state = false; - } - - _ => {} - } - - let x = x + offset.x.to_f32(); - let y = y - offset.y.to_f32(); - + fn write_frame(&mut self, frame: &Frame) { + for &(pos, ref element) in &frame.elements { + let x = pos.x.to_f32(); + let y = pos.y.to_f32(); match *element { - Element::Group(ref group) => self.write_group(x, y, group), + Element::Group(ref group) => self.write_group(pos, group), Element::Text(ref text) => self.write_text(x, y, text), Element::Shape(ref shape) => self.write_shape(x, y, shape), Element::Image(id, size) => self.write_image(x, y, id, size), - Element::Link(_, _) => {} + Element::Link(ref url, size) => self.write_link(pos, url, size), } } - - if self.in_text_state { - self.content.end_text(); - } } - fn write_group(&mut self, x: f32, y: f32, group: &Group) { + fn write_group(&mut self, pos: Point, group: &Group) { + let translation = Transform::translation(pos.x, pos.y); + + self.save_state(); + self.transform(translation.pre_concat(group.transform)); + if group.clips { let w = group.frame.size.w.to_f32(); let h = group.frame.size.h.to_f32(); - self.content.save_state(); - self.content.move_to(x, y); - self.content.line_to(x + w, y); - self.content.line_to(x + w, y - h); - self.content.line_to(x, y - h); + self.content.move_to(0.0, 0.0); + self.content.line_to(w, 0.0); + self.content.line_to(w, h); + self.content.line_to(0.0, h); self.content.clip_nonzero(); self.content.end_path(); } - self.write_frame(x, y, &group.frame); - - if group.clips { - self.content.restore_state(); - } + self.write_frame(&group.frame); + self.restore_state(); } - /// Write a glyph run into the content stream. fn write_text(&mut self, x: f32, y: f32, text: &Text) { - if self.font_fill != Some(text.fill) { - self.write_fill(text.fill); - self.font_fill = Some(text.fill); - } + self.glyphs + .entry(text.face_id) + .or_default() + .extend(text.glyphs.iter().map(|g| g.id)); - // Then, also check if we need to issue a font switching - // action. - if self.face_id != Some(text.face_id) || self.font_size != text.size { - self.face_id = Some(text.face_id); - self.font_size = text.size; - - let name = format_eco!("F{}", self.font_map.map(text.face_id)); - self.content.set_font(Name(name.as_bytes()), text.size.to_f32()); - } + self.content.begin_text(); + self.set_font(text.face_id, text.size); + self.set_fill(text.fill); let face = self.fonts.get(text.face_id); // Position the text. - self.content.set_text_matrix([1.0, 0.0, 0.0, 1.0, x, y]); + self.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); let mut positioned = self.content.show_positioned(); let mut items = positioned.items(); @@ -476,9 +458,12 @@ impl<'a> PageExporter<'a> { if !encoded.is_empty() { items.show(Str(&encoded)); } + + items.finish(); + positioned.finish(); + self.content.end_text(); } - /// Write a geometrical shape into the content stream. fn write_shape(&mut self, x: f32, y: f32, shape: &Shape) { if shape.fill.is_none() && shape.stroke.is_none() { return; @@ -489,7 +474,7 @@ impl<'a> PageExporter<'a> { let w = size.w.to_f32(); let h = size.h.to_f32(); if w > 0.0 && h > 0.0 { - self.content.rect(x, y - h, w, h); + self.content.rect(x, y, w, h); } } Geometry::Ellipse(size) => { @@ -500,21 +485,19 @@ impl<'a> PageExporter<'a> { let dx = target.x.to_f32(); let dy = target.y.to_f32(); self.content.move_to(x, y); - self.content.line_to(x + dx, y - dy); + self.content.line_to(x + dx, y + dy); } Geometry::Path(ref path) => { self.write_path(x, y, path); } } - self.content.save_state(); - if let Some(fill) = shape.fill { - self.write_fill(fill); + self.set_fill(fill); } if let Some(stroke) = shape.stroke { - self.write_stroke(stroke); + self.set_stroke(stroke); } match (shape.fill, shape.stroke) { @@ -523,69 +506,124 @@ impl<'a> PageExporter<'a> { (None, Some(_)) => self.content.stroke(), (Some(_), Some(_)) => self.content.fill_nonzero_and_stroke(), }; - - self.content.restore_state(); } - /// Write an image into the content stream. - fn write_image(&mut self, x: f32, y: f32, id: ImageId, size: Size) { - let name = format!("Im{}", self.image_map.map(id)); - let w = size.w.to_f32(); - let h = size.h.to_f32(); - self.content.save_state(); - self.content.concat_matrix([w, 0.0, 0.0, h, x, y - h]); - self.content.x_object(Name(name.as_bytes())); - self.content.restore_state(); - } - - /// Write a path into a content stream. fn write_path(&mut self, x: f32, y: f32, path: &geom::Path) { for elem in &path.0 { match elem { geom::PathElement::MoveTo(p) => { - self.content.move_to(x + p.x.to_f32(), y - p.y.to_f32()) + self.content.move_to(x + p.x.to_f32(), y + p.y.to_f32()) } geom::PathElement::LineTo(p) => { - self.content.line_to(x + p.x.to_f32(), y - p.y.to_f32()) + self.content.line_to(x + p.x.to_f32(), y + p.y.to_f32()) } geom::PathElement::CubicTo(p1, p2, p3) => self.content.cubic_to( x + p1.x.to_f32(), - y - p1.y.to_f32(), + y + p1.y.to_f32(), x + p2.x.to_f32(), - y - p2.y.to_f32(), + y + p2.y.to_f32(), x + p3.x.to_f32(), - y - p3.y.to_f32(), + y + p3.y.to_f32(), ), geom::PathElement::ClosePath => self.content.close_path(), }; } } - /// Write a fill change into a content stream. - fn write_fill(&mut self, fill: Paint) { - let Paint::Solid(Color::Rgba(c)) = fill; - self.content.set_fill_rgb( - c.r as f32 / 255.0, - c.g as f32 / 255.0, - c.b as f32 / 255.0, - ); + fn write_image(&mut self, x: f32, y: f32, id: ImageId, size: Size) { + self.image_map.insert(id); + let name = format_eco!("Im{}", self.image_map.map(id)); + let w = size.w.to_f32(); + let h = size.h.to_f32(); + self.content.save_state(); + self.content.transform([w, 0.0, 0.0, -h, x, y + h]); + self.content.x_object(Name(name.as_bytes())); + self.content.restore_state(); } - /// Write a stroke change into a content stream. - fn write_stroke(&mut self, stroke: Stroke) { - let Paint::Solid(Color::Rgba(c)) = stroke.paint; - self.content.set_stroke_rgb( - c.r as f32 / 255.0, - c.g as f32 / 255.0, - c.b as f32 / 255.0, - ); - self.content.set_line_width(stroke.thickness.to_f32()); + fn write_link(&mut self, pos: Point, url: &str, size: Size) { + let mut min_x = Length::inf(); + let mut min_y = Length::inf(); + let mut max_x = -Length::inf(); + let mut max_y = -Length::inf(); + + // Compute the bounding box of the transformed link. + for point in [ + pos, + pos + Point::with_x(size.w), + pos + Point::with_y(size.h), + pos + size.to_point(), + ] { + let t = point.transform(self.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 = self.bottom - max_y.to_f32(); + let y2 = self.bottom - min_y.to_f32(); + let rect = Rect::new(x1, y1, x2, y2); + self.links.push((url.to_string(), rect)); + } + + 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 f32, + ky.get() as f32, + kx.get() as f32, + sy.get() as f32, + tx.to_f32(), + ty.to_f32(), + ]); + } + + fn set_font(&mut self, face_id: FaceId, size: Length) { + if self.state.font != Some((face_id, size)) { + self.font_map.insert(face_id); + let name = format_eco!("F{}", self.font_map.map(face_id)); + self.content.set_font(Name(name.as_bytes()), size.to_f32()); + } + } + + fn set_fill(&mut self, fill: Paint) { + if self.state.fill != Some(fill) { + let Paint::Solid(Color::Rgba(c)) = fill; + self.content.set_fill_rgb( + c.r as f32 / 255.0, + c.g as f32 / 255.0, + c.b as f32 / 255.0, + ); + } + } + + fn set_stroke(&mut self, stroke: Stroke) { + if self.state.stroke != Some(stroke) { + let Paint::Solid(Color::Rgba(c)) = stroke.paint; + self.content.set_stroke_rgb( + c.r as f32 / 255.0, + c.g as f32 / 255.0, + c.b as f32 / 255.0, + ); + self.content.set_line_width(stroke.thickness.to_f32()); + } } } -/// The compression level for the deflating. -const DEFLATE_LEVEL: u8 = 6; - /// Encode an image with a suitable filter. /// /// Skips the alpha channel as that's encoded separately. @@ -637,86 +675,11 @@ fn encode_alpha(img: &Image) -> (Vec, Filter) { /// Compress data with the DEFLATE algorithm. fn deflate(data: &[u8]) -> Vec { - miniz_oxide::deflate::compress_to_vec_zlib(data, DEFLATE_LEVEL) + const COMPRESSION_LEVEL: u8 = 6; + miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL) } -/// We need to know exactly which indirect reference id will be used for which -/// objects up-front to correctly declare the document catalogue, page tree and -/// so on. These offsets are computed in the beginning and stored here. -struct Refs { - catalog: Ref, - page_tree: Ref, - pages_start: i32, - contents_start: i32, - fonts_start: i32, - images_start: i32, - alpha_masks_start: i32, - end: i32, -} - -struct FontRefs { - type0_font: Ref, - cid_font: Ref, - font_descriptor: Ref, - cmap: Ref, - data: Ref, -} - -impl Refs { - const OBJECTS_PER_FONT: usize = 5; - - fn new(pages: usize, fonts: usize, images: usize, alpha_masks: usize) -> Self { - let catalog = 1; - let page_tree = catalog + 1; - let pages_start = page_tree + 1; - let contents_start = pages_start + pages as i32; - let fonts_start = contents_start + pages as i32; - let images_start = fonts_start + (Self::OBJECTS_PER_FONT * fonts) as i32; - let alpha_masks_start = images_start + images as i32; - let end = alpha_masks_start + alpha_masks as i32; - - Self { - catalog: Ref::new(catalog), - page_tree: Ref::new(page_tree), - pages_start, - contents_start, - fonts_start, - images_start, - alpha_masks_start, - end, - } - } - - fn pages(&self) -> impl Iterator { - (self.pages_start .. self.contents_start).map(Ref::new) - } - - fn contents(&self) -> impl Iterator { - (self.contents_start .. self.images_start).map(Ref::new) - } - - fn fonts(&self) -> impl Iterator { - (self.fonts_start .. self.images_start) - .step_by(Self::OBJECTS_PER_FONT) - .map(|id| FontRefs { - type0_font: Ref::new(id), - cid_font: Ref::new(id + 1), - font_descriptor: Ref::new(id + 2), - cmap: Ref::new(id + 3), - data: Ref::new(id + 4), - }) - } - - fn images(&self) -> impl Iterator { - (self.images_start .. self.end).map(Ref::new) - } - - fn alpha_mask(&self, i: usize) -> Ref { - Ref::new(self.alpha_masks_start + i as i32) - } -} - -/// Used to assign new, consecutive PDF-internal indices to things. +/// Assigns new, consecutive PDF-internal indices to things. struct Remapper { /// Forwards from the old indices to the new pdf indices. to_pdf: HashMap, @@ -735,10 +698,6 @@ where } } - fn len(&self) -> usize { - self.to_layout.len() - } - fn insert(&mut self, index: Index) { let to_layout = &mut self.to_layout; self.to_pdf.entry(index).or_insert_with(|| { @@ -752,8 +711,11 @@ where self.to_pdf[&index] } - fn pdf_indices(&self) -> impl Iterator { - 0 .. self.to_pdf.len() + fn pdf_indices<'a>( + &'a self, + refs: &'a [Ref], + ) -> impl Iterator + 'a { + refs.iter().copied().zip(0 .. self.to_pdf.len()) } fn layout_indices(&self) -> impl Iterator + '_ { @@ -784,3 +746,17 @@ impl EmExt for Em { 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/frame.rs b/src/frame.rs index 7862e0054..5b1c36ce1 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -6,7 +6,7 @@ use std::rc::Rc; use serde::{Deserialize, Serialize}; use crate::font::FaceId; -use crate::geom::{Em, Length, Paint, Path, Point, Size}; +use crate::geom::{Em, Length, Paint, Path, Point, Size, Transform}; use crate::image::ImageId; /// A finished layout with elements at fixed positions. @@ -40,8 +40,7 @@ impl Frame { /// Add a group element. pub fn push_frame(&mut self, pos: Point, frame: Rc) { - self.elements - .push((pos, Element::Group(Group { frame, clips: false }))) + self.elements.push((pos, Element::Group(Group::new(frame)))); } /// Add all elements of another frame, placing them relative to the given @@ -62,11 +61,6 @@ impl Frame { *point += offset; } } - - /// An iterator over all non-frame elements in this and nested frames. - pub fn elements(&self) -> Elements { - Elements { stack: vec![(0, Point::zero(), self)] } - } } impl Debug for Frame { @@ -87,35 +81,6 @@ impl Debug for Frame { } } -/// An iterator over all elements in a frame, alongside with their positions. -pub struct Elements<'a> { - stack: Vec<(usize, Point, &'a Frame)>, -} - -impl<'a> Iterator for Elements<'a> { - type Item = (Point, &'a Element); - - fn next(&mut self) -> Option { - let (cursor, offset, frame) = self.stack.last_mut()?; - if let Some((pos, e)) = frame.elements.get(*cursor) { - if let Element::Group(g) = e { - let new_offset = *offset + *pos; - self.stack.push((0, new_offset, g.frame.as_ref())); - self.next() - } else { - *cursor += 1; - Some((*offset + *pos, e)) - } - } else { - self.stack.pop(); - if let Some((cursor, _, _)) = self.stack.last_mut() { - *cursor += 1; - } - self.next() - } - } -} - /// The building block frames are composed of. #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum Element { @@ -136,10 +101,35 @@ pub enum Element { pub struct Group { /// The group's frame. pub frame: Rc, + /// A transformation to apply to the group. + pub transform: Transform, /// Whether the frame should be a clipping boundary. pub clips: bool, } +impl Group { + /// Create a new group with default settings. + pub fn new(frame: Rc) -> Self { + Self { + frame, + transform: Transform::identity(), + clips: false, + } + } + + /// Set the group's transform. + pub fn transform(mut self, transform: Transform) -> Self { + self.transform = transform; + self + } + + /// Set whether the group should be a clipping boundary. + pub fn clips(mut self, clips: bool) -> Self { + self.clips = clips; + self + } +} + /// A run of shaped text. #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct Text { diff --git a/src/geom/mod.rs b/src/geom/mod.rs index c763e8fa8..b0c75d25c 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -18,6 +18,7 @@ mod scalar; mod sides; mod size; mod spec; +mod transform; pub use align::*; pub use angle::*; @@ -35,6 +36,7 @@ pub use scalar::*; pub use sides::*; pub use size::*; pub use spec::*; +pub use transform::*; use std::cmp::Ordering; use std::f64::consts::PI; diff --git a/src/geom/point.rs b/src/geom/point.rs index 7c11e81bd..49e3018a6 100644 --- a/src/geom/point.rs +++ b/src/geom/point.rs @@ -25,6 +25,16 @@ impl Point { Self { x: value, y: value } } + /// Create a new point with y set to zero. + pub const fn with_x(x: Length) -> Self { + Self { x, y: Length::zero() } + } + + /// Create a new point with x set to zero. + pub const fn with_y(y: Length) -> Self { + Self { x: Length::zero(), y } + } + /// Convert to the generic representation. pub const fn to_gen(self, block: SpecAxis) -> Gen { match block { @@ -32,6 +42,14 @@ impl Point { SpecAxis::Vertical => Gen::new(self.x, self.y), } } + + /// Transform the point with the given transformation. + pub fn transform(self, transform: Transform) -> Self { + Self::new( + transform.sx.resolve(self.x) + transform.kx.resolve(self.y) + transform.tx, + transform.ky.resolve(self.x) + transform.sy.resolve(self.y) + transform.ty, + ) + } } impl Get for Point { diff --git a/src/geom/relative.rs b/src/geom/relative.rs index 463c5d46e..c894f4a5e 100644 --- a/src/geom/relative.rs +++ b/src/geom/relative.rs @@ -5,6 +5,7 @@ use super::*; /// _Note_: `50%` is represented as `0.5` here, but stored as `50.0` in the /// corresponding [literal](crate::syntax::ast::LitKind::Percent). #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize)] pub struct Relative(Scalar); impl Relative { @@ -73,6 +74,14 @@ impl Add for Relative { sub_impl!(Relative - Relative -> Relative); +impl Mul for Relative { + type Output = Self; + + fn mul(self, other: Self) -> Self { + Self(self.0 * other.0) + } +} + impl Mul for Relative { type Output = Self; @@ -107,5 +116,6 @@ impl Div for Relative { assign_impl!(Relative += Relative); assign_impl!(Relative -= Relative); +assign_impl!(Relative *= Relative); assign_impl!(Relative *= f64); assign_impl!(Relative /= f64); diff --git a/src/geom/transform.rs b/src/geom/transform.rs new file mode 100644 index 000000000..ca44667b7 --- /dev/null +++ b/src/geom/transform.rs @@ -0,0 +1,73 @@ +use super::*; + +/// A scale-skew-translate transformation. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct Transform { + pub sx: Relative, + pub ky: Relative, + pub kx: Relative, + pub sy: Relative, + pub tx: Length, + pub ty: Length, +} + +impl Transform { + /// The identity transformation. + pub const fn identity() -> Self { + Self { + sx: Relative::one(), + ky: Relative::zero(), + kx: Relative::zero(), + sy: Relative::one(), + tx: Length::zero(), + ty: Length::zero(), + } + } + + /// A translation transform. + pub const fn translation(tx: Length, ty: Length) -> Self { + Self { tx, ty, ..Self::identity() } + } + + /// A scaling transform. + pub const fn scaling(sx: Relative, sy: Relative) -> Self { + Self { sx, sy, ..Self::identity() } + } + + /// A rotation transform. + pub fn rotation(angle: Angle) -> Self { + let v = angle.to_rad(); + let cos = Relative::new(v.cos()); + let sin = Relative::new(v.sin()); + Self { + sx: cos, + ky: sin, + kx: -sin, + sy: cos, + ..Self::default() + } + } + + /// Whether this is the identity transformation. + pub fn is_identity(&self) -> bool { + *self == Self::identity() + } + + /// Pre-concatenate another transformation. + pub fn pre_concat(&self, prev: Self) -> Self { + Transform { + sx: self.sx * prev.sx + self.kx * prev.ky, + ky: self.ky * prev.sx + self.sy * prev.ky, + kx: self.sx * prev.kx + self.kx * prev.sy, + sy: self.ky * prev.kx + self.sy * prev.sy, + tx: self.sx.resolve(prev.tx) + self.kx.resolve(prev.ty) + self.tx, + ty: self.ky.resolve(prev.tx) + self.sy.resolve(prev.ty) + self.ty, + } + } +} + +impl Default for Transform { + fn default() -> Self { + Self::identity() + } +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index be4e994cd..a491789a9 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -17,9 +17,9 @@ use std::rc::Rc; use crate::font::FontStore; use crate::frame::Frame; -use crate::geom::{Align, Linear, Sides, Spec}; +use crate::geom::{Align, Linear, Point, Sides, Spec, Transform}; use crate::image::ImageStore; -use crate::library::{AlignNode, DocumentNode, MoveNode, PadNode, SizedNode}; +use crate::library::{AlignNode, DocumentNode, PadNode, SizedNode, TransformNode}; use crate::Context; /// Layout a document node into a collection of frames. @@ -121,15 +121,6 @@ impl PackedNode { } } - /// Move this node's contents without affecting layout. - pub fn moved(self, offset: Spec>) -> Self { - if offset.any(Option::is_some) { - MoveNode { child: self, offset }.pack() - } else { - self - } - } - /// Pad this node at the sides. pub fn padded(self, padding: Sides) -> Self { if !padding.left.is_zero() @@ -142,6 +133,23 @@ impl PackedNode { self } } + + /// Transform this node's contents without affecting layout. + pub fn moved(self, offset: Point) -> Self { + self.transformed( + Transform::translation(offset.x, offset.y), + Spec::new(Align::Left, Align::Top), + ) + } + + /// Transform this node's contents without affecting layout. + pub fn transformed(self, transform: Transform, origin: Spec) -> Self { + if !transform.is_identity() { + TransformNode { child: self, transform, origin }.pack() + } else { + self + } + } } impl Layout for PackedNode { diff --git a/src/library/image.rs b/src/library/image.rs index a53eacc57..178226199 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -7,7 +7,8 @@ use crate::image::ImageId; /// `image`: An image. pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult { let path = args.expect::>("path to image file")?; - let sizing = Spec::new(args.named("width")?, args.named("height")?); + let width = args.named("width")?; + let height = args.named("height")?; let fit = args.named("fit")?.unwrap_or_default(); // Load the image. @@ -20,7 +21,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult { })?; Ok(Value::Template(Template::from_inline(move |_| { - ImageNode { id, fit }.pack().sized(sizing) + ImageNode { id, fit }.pack().sized(Spec::new(width, height)) }))) } @@ -81,13 +82,12 @@ impl Layout for ImageNode { // Create a clipping group if the image mode is `cover`. if self.fit == ImageFit::Cover { - let group = Group { - frame: Rc::new(frame), - clips: self.fit == ImageFit::Cover, - }; - - frame = Frame::new(canvas, canvas.h); - frame.push(Point::zero(), Element::Group(group)); + let mut wrapper = Frame::new(canvas, canvas.h); + wrapper.push( + Point::zero(), + Element::Group(Group::new(Rc::new(frame)).clips(true)), + ); + frame = wrapper; } let mut cts = Constraints::new(regions.expand); diff --git a/src/library/mod.rs b/src/library/mod.rs index 9e7f6f28d..4d730a7ec 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -76,12 +76,14 @@ pub fn new() -> Scope { std.def_func("box", box_); std.def_func("block", block); std.def_func("flow", flow); + std.def_func("stack", stack); + std.def_func("grid", grid); + std.def_func("pad", pad); std.def_func("align", align); std.def_func("place", place); std.def_func("move", move_); - std.def_func("stack", stack); - std.def_func("pad", pad); - std.def_func("grid", grid); + std.def_func("scale", scale); + std.def_func("rotate", rotate); // Elements. std.def_func("image", image); diff --git a/src/library/par.rs b/src/library/par.rs index 46dc304a4..7f3f1088b 100644 --- a/src/library/par.rs +++ b/src/library/par.rs @@ -628,7 +628,7 @@ impl<'a> LineStack<'a> { for line in self.lines.drain(..) { let frame = line.build(ctx, self.size.w); - let pos = Point::new(Length::zero(), offset); + let pos = Point::with_y(offset); if first { output.baseline = pos.y + frame.baseline; first = false; diff --git a/src/library/placed.rs b/src/library/placed.rs index e2c2e9e8d..4f1c5b85e 100644 --- a/src/library/placed.rs +++ b/src/library/placed.rs @@ -3,11 +3,12 @@ use super::prelude::*; /// `place`: Place content at an absolute position. pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult { let aligns = args.find().unwrap_or(Spec::new(Some(Align::Left), None)); - let offset = Spec::new(args.named("dx")?, args.named("dy")?); + let tx = args.named("dx")?.unwrap_or_default(); + let ty = args.named("dy")?.unwrap_or_default(); let body: Template = args.expect("body")?; Ok(Value::Template(Template::from_block(move |style| { PlacedNode { - child: body.pack(style).moved(offset).aligned(aligns), + child: body.pack(style).moved(Point::new(tx, ty)).aligned(aligns), } }))) } diff --git a/src/library/shape.rs b/src/library/shape.rs index 061b4d252..5d3504d04 100644 --- a/src/library/shape.rs +++ b/src/library/shape.rs @@ -5,8 +5,9 @@ use crate::util::RcExt; /// `rect`: A rectangle with optional content. pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult { - let sizing = Spec::new(args.named("width")?, args.named("height")?); - shape_impl(args, ShapeKind::Rect, sizing) + let width = args.named("width")?; + let height = args.named("height")?; + shape_impl(args, ShapeKind::Rect, width, height) } /// `square`: A square with optional content. @@ -20,14 +21,14 @@ pub fn square(_: &mut EvalContext, args: &mut Args) -> TypResult { None => args.named("height")?, size => size, }; - let sizing = Spec::new(width, height); - shape_impl(args, ShapeKind::Square, sizing) + shape_impl(args, ShapeKind::Square, width, height) } /// `ellipse`: An ellipse with optional content. pub fn ellipse(_: &mut EvalContext, args: &mut Args) -> TypResult { - let sizing = Spec::new(args.named("width")?, args.named("height")?); - shape_impl(args, ShapeKind::Ellipse, sizing) + let width = args.named("width")?; + let height = args.named("height")?; + shape_impl(args, ShapeKind::Ellipse, width, height) } /// `circle`: A circle with optional content. @@ -41,14 +42,14 @@ pub fn circle(_: &mut EvalContext, args: &mut Args) -> TypResult { None => args.named("height")?, diameter => diameter, }; - let sizing = Spec::new(width, height); - shape_impl(args, ShapeKind::Circle, sizing) + shape_impl(args, ShapeKind::Circle, width, height) } fn shape_impl( args: &mut Args, kind: ShapeKind, - sizing: Spec>, + width: Option, + height: Option, ) -> TypResult { // The default appearance of a shape. let default = Stroke { @@ -80,7 +81,7 @@ fn shape_impl( child: body.as_ref().map(|body| body.pack(style).padded(padding)), } .pack() - .sized(sizing) + .sized(Spec::new(width, height)) }))) } diff --git a/src/library/sized.rs b/src/library/sized.rs index 8d69afacb..d137c51ee 100644 --- a/src/library/sized.rs +++ b/src/library/sized.rs @@ -2,19 +2,21 @@ use super::prelude::*; /// `box`: Size content and place it into a paragraph. pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult { - let sizing = Spec::new(args.named("width")?, args.named("height")?); + let width = args.named("width")?; + let height = args.named("height")?; let body: Template = args.find().unwrap_or_default(); Ok(Value::Template(Template::from_inline(move |style| { - body.pack(style).sized(sizing) + body.pack(style).sized(Spec::new(width, height)) }))) } /// `block`: Size content and place it into the flow. pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult { - let sizing = Spec::new(args.named("width")?, args.named("height")?); + let width = args.named("width")?; + let height = args.named("height")?; let body: Template = args.find().unwrap_or_default(); Ok(Value::Template(Template::from_block(move |style| { - body.pack(style).sized(sizing) + body.pack(style).sized(Spec::new(width, height)) }))) } diff --git a/src/library/transform.rs b/src/library/transform.rs index 7553bef2e..8d1c6132c 100644 --- a/src/library/transform.rs +++ b/src/library/transform.rs @@ -1,24 +1,54 @@ use super::prelude::*; +use crate::geom::Transform; /// `move`: Move content without affecting layout. pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult { - let offset = Spec::new(args.named("x")?, args.named("y")?); + let tx = args.named("x")?.unwrap_or_default(); + let ty = args.named("y")?.unwrap_or_default(); + let transform = Transform::translation(tx, ty); + transform_impl(args, transform) +} + +/// `scale`: Scale content without affecting layout. +pub fn scale(_: &mut EvalContext, args: &mut Args) -> TypResult { + let all = args.find(); + let sx = args.named("x")?.or(all).unwrap_or(Relative::one()); + let sy = args.named("y")?.or(all).unwrap_or(Relative::one()); + let transform = Transform::scaling(sx, sy); + transform_impl(args, transform) +} + +/// `rotate`: Rotate content without affecting layout. +pub fn rotate(_: &mut EvalContext, args: &mut Args) -> TypResult { + let angle = args.expect("angle")?; + let transform = Transform::rotation(angle); + transform_impl(args, transform) +} + +fn transform_impl(args: &mut Args, transform: Transform) -> TypResult { let body: Template = args.expect("body")?; + let origin = args + .named("origin")? + .unwrap_or(Spec::splat(None)) + .unwrap_or(Spec::new(Align::Center, Align::Horizon)); + Ok(Value::Template(Template::from_inline(move |style| { - body.pack(style).moved(offset) + body.pack(style).transformed(transform, origin) }))) } -/// A node that moves its child without affecting layout. +/// A node that transforms its child without affecting layout. #[derive(Debug, Hash)] -pub struct MoveNode { - /// The node whose contents should be moved. +pub struct TransformNode { + /// The node whose contents should be transformed. pub child: PackedNode, - /// How much to move the contents. - pub offset: Spec>, + /// Transformation to apply to the contents. + pub transform: Transform, + /// The origin of the transformation. + pub origin: Spec, } -impl Layout for MoveNode { +impl Layout for TransformNode { fn layout( &self, ctx: &mut LayoutContext, @@ -26,13 +56,19 @@ impl Layout for MoveNode { ) -> Vec>> { let mut frames = self.child.layout(ctx, regions); - for (Constrained { item: frame, .. }, (_, base)) in - frames.iter_mut().zip(regions.iter()) - { - Rc::make_mut(frame).translate(Point::new( - self.offset.x.map(|x| x.resolve(base.w)).unwrap_or_default(), - self.offset.y.map(|y| y.resolve(base.h)).unwrap_or_default(), - )); + for Constrained { item: frame, .. } in frames.iter_mut() { + let x = self.origin.x.resolve(frame.size.w); + let y = self.origin.y.resolve(frame.size.h); + let transform = Transform::translation(x, y) + .pre_concat(self.transform) + .pre_concat(Transform::translation(-x, -y)); + + let mut wrapper = Frame::new(frame.size, frame.baseline); + wrapper.push( + Point::zero(), + Element::Group(Group::new(std::mem::take(frame)).transform(transform)), + ); + *frame = Rc::new(wrapper); } frames diff --git a/tests/ref/layout/move.png b/tests/ref/layout/move.png deleted file mode 100644 index dc2e7ab3e..000000000 Binary files a/tests/ref/layout/move.png and /dev/null differ diff --git a/tests/ref/layout/transform.png b/tests/ref/layout/transform.png new file mode 100644 index 000000000..5abdef97c Binary files /dev/null and b/tests/ref/layout/transform.png differ diff --git a/tests/ref/text/links.png b/tests/ref/text/links.png index c4aeec06e..18154a068 100644 Binary files a/tests/ref/text/links.png and b/tests/ref/text/links.png differ diff --git a/tests/typ/layout/move.typ b/tests/typ/layout/move.typ deleted file mode 100644 index c1f97e159..000000000 --- a/tests/typ/layout/move.typ +++ /dev/null @@ -1,11 +0,0 @@ -#let size = 11pt -#let tex = [{ - [T] - h(-0.14 * size) - move(y: 0.22 * size)[E] - h(-0.12 * size) - [X] -}] - -#font("Latin Modern Math", size) -Not #tex! diff --git a/tests/typ/layout/transform.typ b/tests/typ/layout/transform.typ new file mode 100644 index 000000000..5b1fa2a1b --- /dev/null +++ b/tests/typ/layout/transform.typ @@ -0,0 +1,49 @@ +// Test transformations. + +--- +// Test creating the TeX and XeTeX logos. +#let size = 11pt +#let tex = [{ + [T] + h(-0.14 * size) + move(y: 0.22 * size)[E] + h(-0.12 * size) + [X] +}] + +#let xetex = { + [X] + h(-0.14 * size) + scale(x: -100%, move(y: 0.26 * size)[E]) + h(-0.14 * size) + [T] + h(-0.14 * size) + move(y: 0.26 * size)[E] + h(-0.12 * size) + [X] +} + +#font("Latin Modern Math", size) +Neither #tex, \ +nor #xetex! + +--- +// Test combination of scaling and rotation. +#page(height: 80pt) +#align(center + horizon, + rotate(20deg, scale(70%, image("../../res/tiger.jpg"))) +) + +--- +// Test setting rotation origin. +#rotate(10deg, origin: top + left, + image("../../res/tiger.jpg", width: 50%) +) + +--- +// Test setting scaling origin. +#let r = rect(width: 100pt, height: 10pt, fill: forest) +#page(height: 65pt) +#scale(r, x: 50%, y: 200%, origin: left + top) +#scale(r, x: 50%, origin: center) +#scale(r, x: 50%, y: 200%, origin: right + bottom) diff --git a/tests/typ/text/links.typ b/tests/typ/text/links.typ index bea5f93ed..e5f7affc4 100644 --- a/tests/typ/text/links.typ +++ b/tests/typ/text/links.typ @@ -14,3 +14,9 @@ Contact #link("mailto:hi@typst.app") or call #link("tel:123") for more informati // Styled with underline and color. #let link(url, body) = link(url, font(fill: rgb("283663"), underline(body))) You could also make the #link("https://html5zombo.com/")[link look way more typical.] + +--- +// Transformed link. +#page(height: 60pt) +#let link = link("https://typst.app/")[LINK] +My cool #move(x: 0.7cm, y: 0.7cm, rotate(10deg, scale(200%, link))) diff --git a/tests/typeset.rs b/tests/typeset.rs index 6f85125ef..289cf7660 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -13,7 +13,9 @@ use typst::diag::Error; use typst::eval::{Smart, Value}; use typst::font::Face; use typst::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; -use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size}; +use typst::geom::{ + self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size, Transform, +}; use typst::image::Image; use typst::layout::layout; #[cfg(feature = "layout-cache")] @@ -465,6 +467,7 @@ fn draw_group( ctx: &Context, group: &Group, ) { + let ts = ts.pre_concat(convert_typst_transform(group.transform)); if group.clips { let w = group.frame.size.w.to_f32(); let h = group.frame.size.h.to_f32(); @@ -628,6 +631,18 @@ fn draw_image( canvas.fill_rect(rect, &paint, ts, Some(mask)); } +fn convert_typst_transform(transform: Transform) -> sk::Transform { + 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(), + ) +} + fn convert_typst_paint(paint: Paint) -> sk::Paint<'static> { let Paint::Solid(Color::Rgba(c)) = paint; let mut paint = sk::Paint::default();