Image fit modes

This commit is contained in:
Laurenz 2021-11-16 21:32:29 +01:00
parent 0cdf17216f
commit 9a800daa82
29 changed files with 535 additions and 382 deletions

View File

@ -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<Frame>],
fonts: &'a FontStore,
images: &'a ImageStore,
glyphs: HashMap<FaceId, HashSet<u16>>,
font_map: Remapper<FaceId>,
image_map: Remapper<ImageId>,
glyphs: HashMap<FaceId, HashSet<u16>>,
}
impl<'a> PdfExporter<'a> {
fn new(ctx: &'a Context, frames: &'a [Rc<Frame>]) -> Self {
let mut glyphs = HashMap::<FaceId, HashSet<u16>>::new();
let mut font_map = Remapper::new();
let mut image_map = Remapper::new();
let mut glyphs = HashMap::<FaceId, HashSet<u16>>::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<Paint> = 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<FaceId>,
image_map: &'a Remapper<ImageId>,
content: Content,
in_text_state: bool,
face_id: Option<FaceId>,
font_size: Length,
font_fill: Option<Paint>,
}
/// 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<u8> {
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
}
}

View File

@ -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>) {
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<Frame>),
}
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<Self::Item> {
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<Frame>),
}
/// A run of shaped text.

View File

@ -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<Self> {
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()
}
}
}

View File

@ -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<Linear>, height: Option<Linear>) -> 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,

View File

@ -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,

View File

@ -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);

View File

@ -9,7 +9,9 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let path = args.expect::<Spanned<EcoString>>("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<Value> {
})
})?;
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<Value> {
pub struct ImageNode {
/// The id of the image file.
pub id: ImageId,
/// The fixed width, if any.
pub width: Option<Linear>,
/// The fixed height, if any.
pub height: Option<Linear>,
/// 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<Constrained<Rc<Frame>>> {
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
}
}

View File

@ -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):

View File

@ -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<Constrained<Rc<Frame>>> {
// 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)]
}
}

View File

@ -6,12 +6,7 @@ pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
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<Value> {
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)
})))
}

View File

@ -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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 B

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -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")
---

View File

@ -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,

View File

@ -17,7 +17,6 @@
#page(width: 50pt, margins: 0pt)
#stack(dir: btt, ..items)
#pagebreak()
---
// Test spacing.

View File

@ -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<Frame>], 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::<Length>();
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> {