diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 24944246d..05f73e52e 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -14,7 +14,7 @@ use ttf_parser::{name_id, GlyphId, Tag}; use super::subset; use crate::font::{find_name, FaceId, FontStore}; -use crate::frame::{Element, Frame, Geometry}; +use crate::frame::{Element, Frame, Geometry, Text}; use crate::geom::{self, Color, Em, Length, Paint, Size}; use crate::image::{Image, ImageId, ImageStore}; use crate::Context; @@ -36,16 +36,16 @@ struct PdfExporter<'a> { frames: &'a [Rc], fonts: &'a FontStore, images: &'a ImageStore, - glyphs: HashMap>, font_map: Remapper, image_map: Remapper, + glyphs: HashMap>, } impl<'a> PdfExporter<'a> { fn new(ctx: &'a Context, frames: &'a [Rc]) -> Self { - let mut glyphs = HashMap::>::new(); 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 { @@ -56,7 +56,6 @@ impl<'a> PdfExporter<'a> { let set = glyphs.entry(text.face_id).or_default(); set.extend(text.glyphs.iter().map(|g| g.id)); } - Element::Geometry(_, _) => {} Element::Image(id, _) => { let img = ctx.images.get(id); if img.buf.color().has_alpha() { @@ -64,7 +63,7 @@ impl<'a> PdfExporter<'a> { } image_map.insert(id); } - Element::Link(_, _) => {} + _ => {} } } } @@ -120,8 +119,8 @@ impl<'a> PdfExporter<'a> { for ((page_id, content_id), page) in self.refs.pages().zip(self.refs.contents()).zip(self.frames) { - let w = page.size.w.to_pt() as f32; - let h = page.size.h.to_pt() as f32; + 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 @@ -131,10 +130,10 @@ impl<'a> PdfExporter<'a> { let mut annotations = page_writer.annotations(); for (pos, element) in page.elements() { if let Element::Link(href, size) = element { - let x = pos.x.to_pt() as f32; - let y = (page.size.h - pos.y).to_pt() as f32; - let w = size.w.to_pt() as f32; - let h = size.h.to_pt() as f32; + 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() @@ -158,148 +157,9 @@ impl<'a> PdfExporter<'a> { } fn write_page(&mut self, id: Ref, page: &'a Frame) { - let mut content = Content::new(); - - // We only write font switching actions when the used face changes. To - // do that, we need to remember the active face. - let mut face_id = None; - let mut size = Length::zero(); - let mut fill: Option = None; - let mut in_text_state = false; - - for (pos, element) in page.elements() { - // Make sure the content stream is in the correct state. - match element { - Element::Text(_) if !in_text_state => { - content.begin_text(); - in_text_state = true; - } - - Element::Geometry(..) | Element::Image(..) if in_text_state => { - content.end_text(); - in_text_state = false; - } - - _ => {} - } - - let x = pos.x.to_pt() as f32; - let y = (page.size.h - pos.y).to_pt() as f32; - - match *element { - Element::Text(ref text) => { - if fill != Some(text.fill) { - write_fill(&mut content, text.fill); - fill = Some(text.fill); - } - - // Then, also check if we need to issue a font switching - // action. - if face_id != Some(text.face_id) || text.size != size { - face_id = Some(text.face_id); - size = text.size; - - let name = format_eco!("F{}", self.font_map.map(text.face_id)); - content.set_font(Name(name.as_bytes()), size.to_pt() as f32); - } - - let face = self.fonts.get(text.face_id); - - // Position the text. - content.set_text_matrix([1.0, 0.0, 0.0, 1.0, x, y]); - - let mut positioned = 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_pdf()); - adjustment = Em::zero(); - } - - encoded.push((glyph.id >> 8) as u8); - encoded.push((glyph.id & 0xff) as u8); - - if let Some(advance) = face.advance(glyph.id) { - adjustment += glyph.x_advance - advance; - } - - adjustment -= glyph.x_offset; - } - - if !encoded.is_empty() { - items.show(Str(&encoded)); - } - } - - Element::Geometry(ref geometry, paint) => { - content.save_state(); - - match *geometry { - Geometry::Rect(Size { w, h }) => { - let w = w.to_pt() as f32; - let h = h.to_pt() as f32; - if w > 0.0 && h > 0.0 { - write_fill(&mut content, paint); - content.rect(x, y - h, w, h); - content.fill_nonzero(); - } - } - Geometry::Ellipse(size) => { - let path = geom::Path::ellipse(size); - write_fill(&mut content, paint); - write_path(&mut content, x, y, &path); - } - Geometry::Line(target, thickness) => { - write_stroke(&mut content, paint, thickness.to_pt() as f32); - content.move_to(x, y); - content.line_to( - x + target.x.to_pt() as f32, - y - target.y.to_pt() as f32, - ); - content.stroke(); - } - Geometry::Path(ref path) => { - write_fill(&mut content, paint); - write_path(&mut content, x, y, path) - } - } - - content.restore_state(); - } - - Element::Image(id, Size { w, h }) => { - let name = format!("Im{}", self.image_map.map(id)); - let w = w.to_pt() as f32; - let h = h.to_pt() as f32; - - content.save_state(); - content.concat_matrix([w, 0.0, 0.0, h, x, y - h]); - content.x_object(Name(name.as_bytes())); - content.restore_state(); - } - - Element::Link(_, _) => {} - } - } - - if in_text_state { - content.end_text(); - } - - self.writer - .stream(id, &deflate(&content.finish())) - .filter(Filter::FlateDecode); + let writer = PageExporter::new(self); + let content = writer.write(page); + self.writer.stream(id, &deflate(&content)).filter(Filter::FlateDecode); } fn write_fonts(&mut self) { @@ -350,7 +210,7 @@ impl<'a> PdfExporter<'a> { let num_glyphs = ttf.number_of_glyphs(); (0 .. num_glyphs).map(|g| { let x = ttf.glyph_hor_advance(GlyphId(g)).unwrap_or(0); - face.to_em(x).to_pdf() + face.to_em(x).to_font_units() }) }); @@ -363,16 +223,16 @@ impl<'a> PdfExporter<'a> { let global_bbox = ttf.global_bounding_box(); let bbox = Rect::new( - face.to_em(global_bbox.x_min).to_pdf(), - face.to_em(global_bbox.y_min).to_pdf(), - face.to_em(global_bbox.x_max).to_pdf(), - face.to_em(global_bbox.y_max).to_pdf(), + face.to_em(global_bbox.x_min).to_font_units(), + face.to_em(global_bbox.y_min).to_font_units(), + face.to_em(global_bbox.x_max).to_font_units(), + face.to_em(global_bbox.y_max).to_font_units(), ); let italic_angle = ttf.italic_angle().unwrap_or(0.0); - let ascender = face.ascender.to_pdf(); - let descender = face.descender.to_pdf(); - let cap_height = face.cap_height.to_pdf(); + let ascender = face.ascender.to_font_units(); + let descender = face.descender.to_font_units(); + let cap_height = face.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). @@ -474,45 +334,244 @@ impl<'a> PdfExporter<'a> { } } -/// Write a fill change into a content stream. -fn write_fill(content: &mut Content, fill: Paint) { - let Paint::Color(Color::Rgba(c)) = fill; - content.set_fill_rgb(c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0); +/// A writer for the contents of a single page. +struct PageExporter<'a> { + fonts: &'a FontStore, + font_map: &'a Remapper, + image_map: &'a Remapper, + content: Content, + in_text_state: bool, + face_id: Option, + font_size: Length, + font_fill: Option, } -/// Write a stroke change into a content stream. -fn write_stroke(content: &mut Content, stroke: Paint, thickness: f32) { - match stroke { - Paint::Color(Color::Rgba(c)) => { - content.set_stroke_rgb( - c.r as f32 / 255.0, - c.g as f32 / 255.0, - c.b as f32 / 255.0, - ); +impl<'a> PageExporter<'a> { + /// Create a new page exporter. + fn new(exporter: &'a PdfExporter) -> Self { + Self { + fonts: exporter.fonts, + font_map: &exporter.font_map, + image_map: &exporter.image_map, + content: Content::new(), + in_text_state: false, + face_id: None, + font_size: Length::zero(), + font_fill: None, } } - content.set_line_width(thickness); -} -/// Write a path into a content stream. -fn write_path(content: &mut Content, x: f32, y: f32, path: &geom::Path) { - let f = |length: Length| length.to_pt() as f32; - for elem in &path.0 { - match elem { - geom::PathElement::MoveTo(p) => content.move_to(x + f(p.x), y + f(p.y)), - geom::PathElement::LineTo(p) => content.line_to(x + f(p.x), y + f(p.y)), - geom::PathElement::CubicTo(p1, p2, p3) => content.cubic_to( - x + f(p1.x), - y + f(p1.y), - x + f(p2.x), - y + f(p2.y), - x + f(p3.x), - y + f(p3.y), - ), - geom::PathElement::ClosePath => content.close_path(), - }; + /// 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() + } + + /// Write a frame into the content stream. + fn write_frame(&mut self, x: f32, y: f32, frame: &Frame) { + if frame.clips { + let w = frame.size.w.to_f32(); + let h = 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.clip_nonzero(); + self.content.end_path(); + } + + 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::Geometry(..) | 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(); + + match *element { + Element::Text(ref text) => { + self.write_text(x, y, text); + } + Element::Geometry(ref geometry, paint) => { + self.write_geometry(x, y, geometry, paint); + } + Element::Image(id, size) => { + self.write_image(x, y, id, size); + } + Element::Link(_, _) => {} + Element::Frame(ref frame) => { + self.write_frame(x, y, frame); + } + } + } + + if self.in_text_state { + self.content.end_text(); + } + + if frame.clips { + self.content.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); + } + + // 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()); + } + + 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]); + + let mut positioned = self.content.show_positioned(); + let mut items = positioned.items(); + let mut adjustment = Em::zero(); + let mut encoded = vec![]; + + // Write the glyphs with kerning adjustments. + for glyph in &text.glyphs { + adjustment += glyph.x_offset; + + if !adjustment.is_zero() { + if !encoded.is_empty() { + items.show(Str(&encoded)); + encoded.clear(); + } + + items.adjust(-adjustment.to_font_units()); + adjustment = Em::zero(); + } + + encoded.push((glyph.id >> 8) as u8); + encoded.push((glyph.id & 0xff) as u8); + + if let Some(advance) = face.advance(glyph.id) { + adjustment += glyph.x_advance - advance; + } + + adjustment -= glyph.x_offset; + } + + if !encoded.is_empty() { + items.show(Str(&encoded)); + } + } + + /// 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 geometrical shape into the content stream. + fn write_geometry(&mut self, x: f32, y: f32, geometry: &Geometry, paint: Paint) { + self.content.save_state(); + + match *geometry { + Geometry::Rect(Size { w, h }) => { + let w = w.to_f32(); + let h = h.to_f32(); + if w > 0.0 && h > 0.0 { + self.write_fill(paint); + self.content.rect(x, y - h, w, h); + self.content.fill_nonzero(); + } + } + Geometry::Ellipse(size) => { + let path = geom::Path::ellipse(size); + self.write_fill(paint); + self.write_filled_path(x, y, &path); + } + Geometry::Line(target, thickness) => { + self.write_stroke(paint, thickness.to_f32()); + self.content.move_to(x, y); + self.content.line_to(x + target.x.to_f32(), y - target.y.to_f32()); + self.content.stroke(); + } + Geometry::Path(ref path) => { + self.write_fill(paint); + self.write_filled_path(x, y, path) + } + } + + self.content.restore_state(); + } + + /// Write and fill path into a content stream. + fn write_filled_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()) + } + geom::PathElement::LineTo(p) => { + 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(), + x + p2.x.to_f32(), + y + p2.y.to_f32(), + x + p3.x.to_f32(), + y + p3.y.to_f32(), + ), + geom::PathElement::ClosePath => self.content.close_path(), + }; + } + self.content.fill_nonzero(); + } + + /// Write a fill change into a content stream. + fn write_fill(&mut self, fill: Paint) { + let Paint::Color(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, + ); + } + + /// Write a stroke change into a content stream. + fn write_stroke(&mut self, stroke: Paint, thickness: f32) { + let Paint::Color(Color::Rgba(c)) = stroke; + 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(thickness); } - content.fill_nonzero(); } /// The compression level for the deflating. @@ -693,14 +752,26 @@ where } } +/// Additional methods for [`Length`]. +trait LengthExt { + /// Convert an em length to a number of points. + fn to_f32(self) -> f32; +} + +impl LengthExt for Length { + 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_pdf(self) -> f32; + fn to_font_units(self) -> f32; } impl EmExt for Em { - fn to_pdf(self) -> f32 { + fn to_font_units(self) -> f32 { 1000.0 * self.get() as f32 } } diff --git a/src/frame.rs b/src/frame.rs index 68aa2e9cf..94234ae9c 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -16,8 +16,10 @@ pub struct Frame { pub size: Size, /// The baseline of the frame measured from the top. pub baseline: Length, + /// Whether this frame should be a clipping boundary. + pub clips: bool, /// The elements composing this layout. - pub children: Vec<(Point, FrameChild)>, + pub elements: Vec<(Point, Element)>, } impl Frame { @@ -25,37 +27,44 @@ impl Frame { #[track_caller] pub fn new(size: Size, baseline: Length) -> Self { assert!(size.is_finite()); - Self { size, baseline, children: vec![] } - } - - /// Add an element at a position in the foreground. - pub fn push(&mut self, pos: Point, element: Element) { - self.children.push((pos, FrameChild::Element(element))); + Self { + size, + baseline, + elements: vec![], + clips: false, + } } /// Add an element at a position in the background. pub fn prepend(&mut self, pos: Point, element: Element) { - self.children.insert(0, (pos, FrameChild::Element(element))); + self.elements.insert(0, (pos, element)); + } + + /// Add an element at a position in the foreground. + pub fn push(&mut self, pos: Point, element: Element) { + self.elements.push((pos, element)); } /// Add a frame element. pub fn push_frame(&mut self, pos: Point, subframe: Rc) { - self.children.push((pos, FrameChild::Group(subframe))) + self.elements.push((pos, Element::Frame(subframe))) } /// Add all elements of another frame, placing them relative to the given /// position. pub fn merge_frame(&mut self, pos: Point, subframe: Self) { - if pos == Point::zero() && self.children.is_empty() { - self.children = subframe.children; + if subframe.clips { + self.push_frame(pos, Rc::new(subframe)); + } else if pos == Point::zero() && self.elements.is_empty() { + self.elements = subframe.elements; } else { - for (subpos, child) in subframe.children { - self.children.push((pos + subpos, child)); + for (subpos, child) in subframe.elements { + self.elements.push((pos + subpos, child)); } } } - /// An iterator over all elements in the frame and its children. + /// An iterator over all non-frame elements in this and nested frames. pub fn elements(&self) -> Elements { Elements { stack: vec![(0, Point::zero(), self)] } } @@ -63,7 +72,7 @@ impl Frame { impl Debug for Frame { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - struct Children<'a>(&'a [(Point, FrameChild)]); + struct Children<'a>(&'a [(Point, Element)]); impl Debug for Children<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { @@ -74,30 +83,12 @@ impl Debug for Frame { f.debug_struct("Frame") .field("size", &self.size) .field("baseline", &self.baseline) - .field("children", &Children(&self.children)) + .field("clips", &self.clips) + .field("children", &Children(&self.elements)) .finish() } } -/// A frame can contain two different kinds of children: a leaf element or a -/// nested frame. -#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] -pub enum FrameChild { - /// A leaf node in the frame tree. - Element(Element), - /// An interior group. - Group(Rc), -} - -impl Debug for FrameChild { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Element(element) => element.fmt(f), - Self::Group(frame) => frame.fmt(f), - } - } -} - /// An iterator over all elements in a frame, alongside with their positions. pub struct Elements<'a> { stack: Vec<(usize, Point, &'a Frame)>, @@ -108,23 +99,21 @@ impl<'a> Iterator for Elements<'a> { fn next(&mut self) -> Option { let (cursor, offset, frame) = self.stack.last_mut()?; - match frame.children.get(*cursor) { - Some((pos, FrameChild::Group(f))) => { + if let Some((pos, e)) = frame.elements.get(*cursor) { + if let Element::Frame(f) = e { let new_offset = *offset + *pos; self.stack.push((0, new_offset, f.as_ref())); self.next() - } - Some((pos, FrameChild::Element(e))) => { + } else { *cursor += 1; Some((*offset + *pos, e)) } - None => { - self.stack.pop(); - if let Some((cursor, _, _)) = self.stack.last_mut() { - *cursor += 1; - } - self.next() + } else { + self.stack.pop(); + if let Some((cursor, _, _)) = self.stack.last_mut() { + *cursor += 1; } + self.next() } } } @@ -141,6 +130,8 @@ pub enum Element { Image(ImageId, Size), /// A link to an external resource. Link(String, Size), + /// A subframe, which can be a clipping boundary. + Frame(Rc), } /// A run of shaped text. diff --git a/src/geom/length.rs b/src/geom/length.rs index de184e6ce..68261d5f3 100644 --- a/src/geom/length.rs +++ b/src/geom/length.rs @@ -127,11 +127,15 @@ impl Length { self == other || (self - other).to_raw().abs() < 1e-6 } - /// Perform a checked division by a number, returning `None` if the result + /// Perform a checked division by a number, returning zero if the result /// is not finite. - pub fn div_finite(self, number: f64) -> Option { + pub fn safe_div(self, number: f64) -> Self { let result = self.to_raw() / number; - result.is_finite().then(|| Self::raw(result)) + if result.is_finite() { + Self::raw(result) + } else { + Self::zero() + } } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 7d2837de2..949fff642 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -16,8 +16,9 @@ use std::rc::Rc; use crate::font::FontStore; use crate::frame::Frame; +use crate::geom::{Linear, Spec}; use crate::image::ImageStore; -use crate::library::DocumentNode; +use crate::library::{DocumentNode, SizedNode}; use crate::Context; /// Layout a document node into a collection of frames. @@ -93,6 +94,23 @@ pub struct PackedNode { hash: u64, } +impl PackedNode { + /// Force a size for this node. + /// + /// If at least one of `width` and `height` is `Some`, this wraps the node + /// in a [`SizedNode`]. Otherwise, it returns the node unchanged. + pub fn sized(self, width: Option, height: Option) -> PackedNode { + if width.is_some() || height.is_some() { + Layout::pack(SizedNode { + sizing: Spec::new(width, height), + child: self, + }) + } else { + self + } + } +} + impl Layout for PackedNode { fn layout( &self, diff --git a/src/library/deco.rs b/src/library/deco.rs index 6ef5a97bc..2722fd687 100644 --- a/src/library/deco.rs +++ b/src/library/deco.rs @@ -102,9 +102,9 @@ pub enum LineKind { impl LineDecoration { /// Apply a line decoration to a all text elements in a frame. pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) { - for i in 0 .. frame.children.len() { - let (pos, child) = &frame.children[i]; - if let FrameChild::Element(Element::Text(text)) = child { + for i in 0 .. frame.elements.len() { + let (pos, child) = &frame.elements[i]; + if let Element::Text(text) = child { let face = ctx.fonts.get(text.face_id); let metrics = match self.kind { LineKind::Underline => face.underline, diff --git a/src/library/grid.rs b/src/library/grid.rs index c9accffb2..f247c7f45 100644 --- a/src/library/grid.rs +++ b/src/library/grid.rs @@ -167,7 +167,8 @@ impl<'a> GridLayouter<'a> { cols.pop(); rows.pop(); - // We use the regions only for auto row measurement and constraints. + // We use the regions for auto row measurement. Since at that moment, + // columns are already sized, we can enable horizontal expansion. let expand = regions.expand; regions.expand = Spec::new(true, false); diff --git a/src/library/image.rs b/src/library/image.rs index f93d4b548..ea9a050c3 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -9,7 +9,9 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult { let path = args.expect::>("path to image file")?; let width = args.named("width")?; let height = args.named("height")?; + let fit = args.named("fit")?.unwrap_or_default(); + // Load the image. let full = ctx.make_path(&path.v); let id = ctx.images.load(&full).map_err(|err| { Error::boxed(path.span, match err.kind() { @@ -18,10 +20,8 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult { }) })?; - Ok(Value::Template(Template::from_inline(move |_| ImageNode { - id, - width, - height, + Ok(Value::Template(Template::from_inline(move |_| { + ImageNode { id, fit }.pack().sized(width, height) }))) } @@ -30,10 +30,8 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult { pub struct ImageNode { /// The id of the image file. pub id: ImageId, - /// The fixed width, if any. - pub width: Option, - /// The fixed height, if any. - pub height: Option, + /// How the image should adjust itself to a given area. + pub fit: ImageFit, } impl Layout for ImageNode { @@ -42,36 +40,77 @@ impl Layout for ImageNode { ctx: &mut LayoutContext, regions: &Regions, ) -> Vec>> { + let &Regions { current, expand, .. } = regions; + let img = ctx.images.get(self.id); - let pixel_size = Spec::new(img.width() as f64, img.height() as f64); - let pixel_ratio = pixel_size.x / pixel_size.y; + let pixel_w = img.width() as f64; + let pixel_h = img.height() as f64; - let width = self.width.map(|w| w.resolve(regions.base.w)); - let height = self.height.map(|w| w.resolve(regions.base.h)); + let region_ratio = current.w / current.h; + let pixel_ratio = pixel_w / pixel_h; + let wide = region_ratio < pixel_ratio; - let mut cts = Constraints::new(regions.expand); - cts.set_base_if_linear(regions.base, Spec::new(self.width, self.height)); - - let size = match (width, height) { - (Some(width), Some(height)) => Size::new(width, height), - (Some(width), None) => Size::new(width, width / pixel_ratio), - (None, Some(height)) => Size::new(height * pixel_ratio, height), - (None, None) => { - cts.exact.x = Some(regions.current.w); - if regions.current.w.is_finite() { - // Fit to width. - Size::new(regions.current.w, regions.current.w / pixel_ratio) - } else { - // Unbounded width, we have to make up something, - // so it is 1pt per pixel. - pixel_size.map(Length::pt).to_size() - } - } + // The space into which the image will be placed according to its fit. + let canvas = if expand.x && expand.y { + current + } else if expand.x || (wide && current.w.is_finite()) { + Size::new(current.w, current.w.safe_div(pixel_ratio)) + } else if current.h.is_finite() { + Size::new(current.h * pixel_ratio, current.h) + } else { + Size::new(Length::pt(pixel_w), Length::pt(pixel_h)) }; - let mut frame = Frame::new(size, size.h); - frame.push(Point::zero(), Element::Image(self.id, size)); + // The actual size of the fitted image. + let size = match self.fit { + ImageFit::Contain | ImageFit::Cover => { + if wide == (self.fit == ImageFit::Contain) { + Size::new(canvas.w, canvas.w / pixel_ratio) + } else { + Size::new(canvas.h * pixel_ratio, canvas.h) + } + } + ImageFit::Stretch => canvas, + }; + // The position of the image so that it is centered in the canvas. + let mut frame = Frame::new(canvas, canvas.h); + frame.clips = self.fit == ImageFit::Cover; + frame.push( + Point::new((canvas.w - size.w) / 2.0, (canvas.h - size.h) / 2.0), + Element::Image(self.id, size), + ); + + let mut cts = Constraints::new(regions.expand); + cts.exact = regions.current.to_spec().map(Some); vec![frame.constrain(cts)] } } + +/// How an image should adjust itself to a given area. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ImageFit { + /// The image should be fully contained in the area. + Contain, + /// The image should completely cover the area. + Cover, + /// The image should be stretched so that it exactly fills the area. + Stretch, +} + +castable! { + ImageFit, + Expected: "string", + Value::Str(string) => match string.as_str() { + "contain" => Self::Contain, + "cover" => Self::Cover, + "stretch" => Self::Stretch, + _ => Err(r#"expected "contain", "cover" or "stretch""#)?, + }, +} + +impl Default for ImageFit { + fn default() -> Self { + Self::Contain + } +} diff --git a/src/library/pad.rs b/src/library/pad.rs index 1ec1b4a23..a1c8c6f90 100644 --- a/src/library/pad.rs +++ b/src/library/pad.rs @@ -46,9 +46,7 @@ impl Layout for PadNode { frames.iter_mut().zip(regions.iter()) { fn solve_axis(length: Length, padding: Linear) -> Length { - (length + padding.abs) - .div_finite(1.0 - padding.rel.get()) - .unwrap_or_default() + (length + padding.abs).safe_div(1.0 - padding.rel.get()) } // Solve for the size `padded` that satisfies (approximately): diff --git a/src/library/shape.rs b/src/library/shape.rs index 407d5974d..dbd6eea71 100644 --- a/src/library/shape.rs +++ b/src/library/shape.rs @@ -1,7 +1,7 @@ use std::f64::consts::SQRT_2; use super::prelude::*; -use super::{PadNode, SizedNode}; +use super::PadNode; use crate::util::RcExt; /// `rect`: A rectangle with optional content. @@ -65,20 +65,13 @@ fn shape_impl( let fill = fill.unwrap_or(Color::Rgba(RgbaColor::gray(175))); Value::Template(Template::from_inline(move |style| { - let shape = Layout::pack(ShapeNode { + ShapeNode { kind, fill: Some(Paint::Color(fill)), child: body.as_ref().map(|body| body.pack(style)), - }); - - if width.is_some() || height.is_some() { - Layout::pack(SizedNode { - sizing: Spec::new(width, height), - child: shape, - }) - } else { - shape } + .pack() + .sized(width, height) })) } @@ -112,7 +105,7 @@ impl Layout for ShapeNode { ctx: &mut LayoutContext, regions: &Regions, ) -> Vec>> { - // Layout. + // Layout, either with or without child. let mut frame = if let Some(child) = &self.child { let mut node: &dyn Layout = child; @@ -141,15 +134,18 @@ impl Layout for ShapeNode { frames = node.layout(ctx, &pod); } - // Validate and set constraints. - assert_eq!(frames.len(), 1); + // Extract the frame. Rc::take(frames.into_iter().next().unwrap().item) } else { + // When there's no child, fill the area if expansion is on, + // otherwise fall back to a default size. let default = Length::pt(30.0); let size = Size::new( if regions.expand.x { regions.current.w } else { + // For rectangle and ellipse, the default shape is a bit + // wider than high. match self.kind { ShapeKind::Square | ShapeKind::Circle => default, ShapeKind::Rect | ShapeKind::Ellipse => 1.5 * default, @@ -161,7 +157,7 @@ impl Layout for ShapeNode { Frame::new(size, size.h) }; - // Add background shape if desired. + // Add background fill if desired. if let Some(fill) = self.fill { let (pos, geometry) = match self.kind { ShapeKind::Square | ShapeKind::Rect => { @@ -175,11 +171,10 @@ impl Layout for ShapeNode { frame.prepend(pos, Element::Geometry(geometry, fill)); } - // Generate tight constraints for now. + // Return tight constraints for now. let mut cts = Constraints::new(regions.expand); cts.exact = regions.current.to_spec().map(Some); cts.base = regions.base.to_spec().map(Some); - vec![frame.constrain(cts)] } } diff --git a/src/library/sized.rs b/src/library/sized.rs index 686d79b90..6394b0f4e 100644 --- a/src/library/sized.rs +++ b/src/library/sized.rs @@ -6,12 +6,7 @@ pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult { let height = args.named("height")?; let body: Template = args.find().unwrap_or_default(); Ok(Value::Template(Template::from_inline(move |style| { - let child = body.pack(style); - if width.is_some() || height.is_some() { - Layout::pack(SizedNode { sizing: Spec::new(width, height), child }) - } else { - child - } + body.pack(style).sized(width, height) }))) } @@ -21,12 +16,7 @@ pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult { let height = args.named("height")?; let body: Template = args.find().unwrap_or_default(); Ok(Value::Template(Template::from_block(move |style| { - let child = body.pack(style); - if width.is_some() || height.is_some() { - Layout::pack(SizedNode { sizing: Spec::new(width, height), child }) - } else { - child - } + body.pack(style).sized(width, height) }))) } diff --git a/src/library/transform.rs b/src/library/transform.rs index 85d65703d..d0a276229 100644 --- a/src/library/transform.rs +++ b/src/library/transform.rs @@ -36,7 +36,7 @@ impl Layout for MoveNode { self.offset.y.map(|y| y.resolve(base.h)).unwrap_or_default(), ); - for (point, _) in &mut Rc::make_mut(frame).children { + for (point, _) in &mut Rc::make_mut(frame).elements { *point += offset; } } diff --git a/tests/ref/coma.png b/tests/ref/coma.png index d4c6c3def..04356991b 100644 Binary files a/tests/ref/coma.png and b/tests/ref/coma.png differ diff --git a/tests/ref/elements/circle.png b/tests/ref/elements/circle.png index 2b938c89b..efc0b8ece 100644 Binary files a/tests/ref/elements/circle.png and b/tests/ref/elements/circle.png differ diff --git a/tests/ref/elements/image.png b/tests/ref/elements/image.png index 943d43b1e..2385d8200 100644 Binary files a/tests/ref/elements/image.png and b/tests/ref/elements/image.png differ diff --git a/tests/ref/layout/grid-3.png b/tests/ref/layout/grid-3.png index f4d450f8e..7ef5df73a 100644 Binary files a/tests/ref/layout/grid-3.png and b/tests/ref/layout/grid-3.png differ diff --git a/tests/ref/layout/grid-5.png b/tests/ref/layout/grid-5.png index 1b29e0bd0..51703b11a 100644 Binary files a/tests/ref/layout/grid-5.png and b/tests/ref/layout/grid-5.png differ diff --git a/tests/ref/layout/page.png b/tests/ref/layout/page.png index 71c3f18af..75ad00d12 100644 Binary files a/tests/ref/layout/page.png and b/tests/ref/layout/page.png differ diff --git a/tests/ref/layout/stack-1.png b/tests/ref/layout/stack-1.png index 78f7ed773..535632c4c 100644 Binary files a/tests/ref/layout/stack-1.png and b/tests/ref/layout/stack-1.png differ diff --git a/tests/ref/layout/stack-2.png b/tests/ref/layout/stack-2.png index 3e503e653..470b57ece 100644 Binary files a/tests/ref/layout/stack-2.png and b/tests/ref/layout/stack-2.png differ diff --git a/tests/ref/markup/enums.png b/tests/ref/markup/enums.png index f1d3855bb..a201131f2 100644 Binary files a/tests/ref/markup/enums.png and b/tests/ref/markup/enums.png differ diff --git a/tests/ref/markup/escape.png b/tests/ref/markup/escape.png index 3f41a5164..41b8c4d6a 100644 Binary files a/tests/ref/markup/escape.png and b/tests/ref/markup/escape.png differ diff --git a/tests/ref/markup/lists.png b/tests/ref/markup/lists.png index fef578ae1..1405b95e6 100644 Binary files a/tests/ref/markup/lists.png and b/tests/ref/markup/lists.png differ diff --git a/tests/ref/text/basic.png b/tests/ref/text/basic.png index 1a5a13091..88d3059c5 100644 Binary files a/tests/ref/text/basic.png and b/tests/ref/text/basic.png differ diff --git a/tests/ref/text/links.png b/tests/ref/text/links.png index 38abf0c59..43b77c87b 100644 Binary files a/tests/ref/text/links.png and b/tests/ref/text/links.png differ diff --git a/tests/ref/text/whitespace.png b/tests/ref/text/whitespace.png index f1421bfe3..7e79c1772 100644 Binary files a/tests/ref/text/whitespace.png and b/tests/ref/text/whitespace.png differ diff --git a/tests/typ/elements/image.typ b/tests/typ/elements/image.typ index c0e6a3efc..43f93baf8 100644 --- a/tests/typ/elements/image.typ +++ b/tests/typ/elements/image.typ @@ -5,30 +5,42 @@ // Load an RGBA PNG image. #image("../../res/rhino.png") -#pagebreak() // Load an RGB JPEG image. +#page(height: 60pt) #image("../../res/tiger.jpg") --- // Test configuring the size and fitting behaviour of images. -// Set width explicitly. -#image("../../res/rhino.png", width: 50pt) - -// Set height explicitly. -#image("../../res/rhino.png", height: 50pt) +// Set width and height explicitly. +#image("../../res/rhino.png", width: 30pt) +#image("../../res/rhino.png", height: 30pt) // Set width and height explicitly and force stretching. -#image("../../res/rhino.png", width: 25pt, height: 50pt) +#image("../../res/tiger.jpg", width: 100%, height: 20pt, fit: "stretch") // Make sure the bounding-box of the image is correct. #align(bottom, right) -#image("../../res/tiger.jpg", width: 60pt) +#image("../../res/tiger.jpg", width: 40pt) --- -// Does not fit to height of page. +// Test all three fit modes. +#page(height: 50pt, margins: 0pt) +#grid( + columns: 3, + rows: 100%, + gutter: 3pt, + image("../../res/tiger.jpg", fit: "contain"), + image("../../res/tiger.jpg", fit: "cover"), + image("../../res/tiger.jpg", fit: "stretch"), +) + +--- +// Does not fit to remaining height of page. #page(height: 60pt) +Stuff \ +Stuff #image("../../res/rhino.png") --- diff --git a/tests/typ/layout/grid-3.typ b/tests/typ/layout/grid-3.typ index af9718750..82472e554 100644 --- a/tests/typ/layout/grid-3.typ +++ b/tests/typ/layout/grid-3.typ @@ -23,7 +23,7 @@ columns: 4 * (1fr,), row-gutter: 10pt, column-gutter: (0pt, 10%), - image("../../res/rhino.png"), + align(top, image("../../res/rhino.png")), align(right, rect(width: 100%, fill: eastern)[LoL]), [rofl], [\ A] * 3, diff --git a/tests/typ/layout/stack-1.typ b/tests/typ/layout/stack-1.typ index 42a0137dc..2fbe22e3d 100644 --- a/tests/typ/layout/stack-1.typ +++ b/tests/typ/layout/stack-1.typ @@ -17,7 +17,6 @@ #page(width: 50pt, margins: 0pt) #stack(dir: btt, ..items) -#pagebreak() --- // Test spacing. diff --git a/tests/typeset.rs b/tests/typeset.rs index aa4d250bb..a287638a6 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -13,9 +13,7 @@ use typst::diag::Error; use typst::eval::Value; use typst::font::Face; use typst::frame::{Element, Frame, Geometry, Text}; -use typst::geom::{ - self, Color, Length, Paint, PathElement, Point, RgbaColor, Sides, Size, -}; +use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size}; use typst::image::Image; use typst::layout::layout; #[cfg(feature = "layout-cache")] @@ -390,66 +388,96 @@ fn draw(ctx: &Context, frames: &[Rc], dpp: f32) -> sk::Pixmap { let width = 2.0 * pad + frames.iter().map(|l| l.size.w).max().unwrap_or_default(); let height = pad + frames.iter().map(|l| l.size.h + pad).sum::(); - let pixel_width = (dpp * width.to_pt() as f32) as u32; - let pixel_height = (dpp * height.to_pt() as f32) as u32; - if pixel_width > 4000 || pixel_height > 4000 { + let pxw = (dpp * width.to_pt() as f32) as u32; + let pxh = (dpp * height.to_pt() as f32) as u32; + if pxw > 4000 || pxh > 4000 { panic!( "overlarge image: {} by {} ({:?} x {:?})", - pixel_width, pixel_height, width, height, + pxw, pxh, width, height, ); } - let mut canvas = sk::Pixmap::new(pixel_width, pixel_height).unwrap(); - let ts = sk::Transform::from_scale(dpp, dpp); + let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); canvas.fill(sk::Color::BLACK); - let mut origin = Point::splat(pad); + let mut mask = sk::ClipMask::new(); + let rect = sk::Rect::from_xywh(0.0, 0.0, pxw as f32, pxh as f32).unwrap(); + let path = sk::PathBuilder::from_rect(rect); + mask.set_path(pxw, pxh, &path, sk::FillRule::default(), false); + + let mut ts = sk::Transform::from_scale(dpp, dpp) + .pre_translate(pad.to_pt() as f32, pad.to_pt() as f32); + for frame in frames { - let mut paint = sk::Paint::default(); - paint.set_color(sk::Color::WHITE); - canvas.fill_rect( - sk::Rect::from_xywh( - origin.x.to_pt() as f32, - origin.y.to_pt() as f32, - frame.size.w.to_pt() as f32, - frame.size.h.to_pt() as f32, - ) - .unwrap(), - &paint, - ts, - None, - ); + let mut background = sk::Paint::default(); + background.set_color(sk::Color::WHITE); - for (pos, element) in frame.elements() { - let global = origin + pos; - let x = global.x.to_pt() as f32; - let y = global.y.to_pt() as f32; - let ts = ts.pre_translate(x, y); - match *element { - Element::Text(ref text) => { - draw_text(&mut canvas, ts, ctx.fonts.get(text.face_id), text); - } - Element::Geometry(ref geometry, paint) => { - draw_geometry(&mut canvas, ts, geometry, paint); - } - Element::Image(id, size) => { - draw_image(&mut canvas, ts, ctx.images.get(id), size); - } - Element::Link(_, s) => { - let outline = Geometry::Rect(s); - let paint = Paint::Color(Color::Rgba(RgbaColor::new(40, 54, 99, 40))); - draw_geometry(&mut canvas, ts, &outline, paint); - } - } - } + let w = frame.size.w.to_pt() as f32; + let h = frame.size.h.to_pt() as f32; + let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); + canvas.fill_rect(rect, &background, ts, None); - origin.y += frame.size.h + pad; + draw_frame(&mut canvas, ts, &mask, ctx, frame); + ts = ts.pre_translate(0.0, (frame.size.h + pad).to_pt() as f32); } canvas } -fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Text) { +fn draw_frame( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: &sk::ClipMask, + ctx: &Context, + frame: &Frame, +) { + let mut storage; + let mask = if frame.clips { + let w = frame.size.w.to_pt() as f32; + let h = frame.size.h.to_pt() as f32; + let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); + let path = sk::PathBuilder::from_rect(rect).transform(ts).unwrap(); + storage = mask.clone(); + storage.intersect_path(&path, sk::FillRule::default(), false); + &storage + } else { + mask + }; + + for (pos, element) in &frame.elements { + let x = pos.x.to_pt() as f32; + let y = pos.y.to_pt() as f32; + let ts = ts.pre_translate(x, y); + + match *element { + Element::Text(ref text) => { + draw_text(canvas, ts, mask, ctx.fonts.get(text.face_id), text); + } + Element::Geometry(ref geometry, paint) => { + draw_geometry(canvas, ts, mask, geometry, paint); + } + Element::Image(id, size) => { + draw_image(canvas, ts, mask, ctx.images.get(id), size); + } + Element::Link(_, s) => { + let outline = Geometry::Rect(s); + let paint = Paint::Color(Color::Rgba(RgbaColor::new(40, 54, 99, 40))); + draw_geometry(canvas, ts, mask, &outline, paint); + } + Element::Frame(ref frame) => { + draw_frame(canvas, ts, mask, ctx, frame); + } + } + } +} + +fn draw_text( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: &sk::ClipMask, + face: &Face, + text: &Text, +) { let ttf = face.ttf(); let size = text.size.to_pt() as f32; let units_per_em = ttf.units_per_em() as f32; @@ -481,7 +509,7 @@ fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Tex if let Some(fill) = &node.fill { let path = convert_usvg_path(&node.data); let (paint, fill_rule) = convert_usvg_fill(fill); - canvas.fill_path(&path, &paint, fill_rule, ts, None); + canvas.fill_path(&path, &paint, fill_rule, ts, Some(mask)); } } } @@ -497,7 +525,7 @@ fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Tex let dx = (raster.x as f32) / (img.width() as f32) * size; let dy = (raster.y as f32) / (img.height() as f32) * size; let ts = ts.pre_translate(dx, -size - dy); - draw_image(canvas, ts, &img, Size::new(w, h)); + draw_image(canvas, ts, mask, &img, Size::new(w, h)); } else { // Otherwise, draw normal outline. let mut builder = WrappedPathBuilder(sk::PathBuilder::new()); @@ -507,7 +535,7 @@ fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Tex let path = builder.0.finish().unwrap(); let mut paint = convert_typst_paint(text.fill); paint.anti_alias = true; - canvas.fill_path(&path, &paint, sk::FillRule::default(), ts, None); + canvas.fill_path(&path, &paint, sk::FillRule::default(), ts, Some(mask)); } } @@ -518,6 +546,7 @@ fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Tex fn draw_geometry( canvas: &mut sk::Pixmap, ts: sk::Transform, + mask: &sk::ClipMask, geometry: &Geometry, paint: Paint, ) { @@ -529,11 +558,11 @@ fn draw_geometry( let w = width.to_pt() as f32; let h = height.to_pt() as f32; let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); - canvas.fill_rect(rect, &paint, ts, None); + canvas.fill_rect(rect, &paint, ts, Some(mask)); } Geometry::Ellipse(size) => { let path = convert_typst_path(&geom::Path::ellipse(size)); - canvas.fill_path(&path, &paint, rule, ts, None); + canvas.fill_path(&path, &paint, rule, ts, Some(mask)); } Geometry::Line(target, thickness) => { let path = { @@ -544,16 +573,22 @@ fn draw_geometry( let mut stroke = sk::Stroke::default(); stroke.width = thickness.to_pt() as f32; - canvas.stroke_path(&path, &paint, &stroke, ts, None); + canvas.stroke_path(&path, &paint, &stroke, ts, Some(mask)); } Geometry::Path(ref path) => { let path = convert_typst_path(path); - canvas.fill_path(&path, &paint, rule, ts, None); + canvas.fill_path(&path, &paint, rule, ts, Some(mask)); } }; } -fn draw_image(canvas: &mut sk::Pixmap, ts: sk::Transform, img: &Image, size: Size) { +fn draw_image( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: &sk::ClipMask, + img: &Image, + size: Size, +) { let mut pixmap = sk::Pixmap::new(img.buf.width(), img.buf.height()).unwrap(); for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) { let Rgba([r, g, b, a]) = src; @@ -575,7 +610,7 @@ fn draw_image(canvas: &mut sk::Pixmap, ts: sk::Transform, img: &Image, size: Siz ); let rect = sk::Rect::from_xywh(0.0, 0.0, view_width, view_height).unwrap(); - canvas.fill_rect(rect, &paint, ts, None); + canvas.fill_rect(rect, &paint, ts, Some(mask)); } fn convert_typst_paint(paint: Paint) -> sk::Paint<'static> {