mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Welcome to Tigerland
This commit is contained in:
parent
ed50661378
commit
0a974d86ba
@ -14,7 +14,7 @@ use ttf_parser::{name_id, GlyphId, Tag};
|
|||||||
|
|
||||||
use super::subset;
|
use super::subset;
|
||||||
use crate::font::{find_name, FaceId, FontStore};
|
use crate::font::{find_name, FaceId, FontStore};
|
||||||
use crate::frame::{Element, Frame, Geometry, Shape, Stroke, Text};
|
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, Size};
|
||||||
use crate::image::{Image, ImageId, ImageStore};
|
use crate::image::{Image, ImageId, ImageStore};
|
||||||
use crate::Context;
|
use crate::Context;
|
||||||
@ -369,18 +369,6 @@ impl<'a> PageExporter<'a> {
|
|||||||
|
|
||||||
/// Write a frame into the content stream.
|
/// Write a frame into the content stream.
|
||||||
fn write_frame(&mut self, x: f32, y: f32, frame: &Frame) {
|
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 {
|
for (offset, element) in &frame.elements {
|
||||||
// Make sure the content stream is in the correct state.
|
// Make sure the content stream is in the correct state.
|
||||||
match element {
|
match element {
|
||||||
@ -401,10 +389,10 @@ impl<'a> PageExporter<'a> {
|
|||||||
let y = y - offset.y.to_f32();
|
let y = y - offset.y.to_f32();
|
||||||
|
|
||||||
match *element {
|
match *element {
|
||||||
|
Element::Group(ref group) => self.write_group(x, y, group),
|
||||||
Element::Text(ref text) => self.write_text(x, y, text),
|
Element::Text(ref text) => self.write_text(x, y, text),
|
||||||
Element::Shape(ref shape) => self.write_shape(x, y, shape),
|
Element::Shape(ref shape) => self.write_shape(x, y, shape),
|
||||||
Element::Image(id, size) => self.write_image(x, y, id, size),
|
Element::Image(id, size) => self.write_image(x, y, id, size),
|
||||||
Element::Frame(ref frame) => self.write_frame(x, y, frame),
|
|
||||||
Element::Link(_, _) => {}
|
Element::Link(_, _) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -412,8 +400,24 @@ impl<'a> PageExporter<'a> {
|
|||||||
if self.in_text_state {
|
if self.in_text_state {
|
||||||
self.content.end_text();
|
self.content.end_text();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if frame.clips {
|
fn write_group(&mut self, x: f32, y: f32, group: &Group) {
|
||||||
|
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.clip_nonzero();
|
||||||
|
self.content.end_path();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.write_frame(x, y, &group.frame);
|
||||||
|
|
||||||
|
if group.clips {
|
||||||
self.content.restore_state();
|
self.content.restore_state();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
src/frame.rs
38
src/frame.rs
@ -16,8 +16,6 @@ pub struct Frame {
|
|||||||
pub size: Size,
|
pub size: Size,
|
||||||
/// The baseline of the frame measured from the top.
|
/// The baseline of the frame measured from the top.
|
||||||
pub baseline: Length,
|
pub baseline: Length,
|
||||||
/// Whether this frame should be a clipping boundary.
|
|
||||||
pub clips: bool,
|
|
||||||
/// The elements composing this layout.
|
/// The elements composing this layout.
|
||||||
pub elements: Vec<(Point, Element)>,
|
pub elements: Vec<(Point, Element)>,
|
||||||
}
|
}
|
||||||
@ -27,12 +25,7 @@ impl Frame {
|
|||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn new(size: Size, baseline: Length) -> Self {
|
pub fn new(size: Size, baseline: Length) -> Self {
|
||||||
assert!(size.is_finite());
|
assert!(size.is_finite());
|
||||||
Self {
|
Self { size, baseline, elements: vec![] }
|
||||||
size,
|
|
||||||
baseline,
|
|
||||||
elements: vec![],
|
|
||||||
clips: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an element at a position in the background.
|
/// Add an element at a position in the background.
|
||||||
@ -45,17 +38,16 @@ impl Frame {
|
|||||||
self.elements.push((pos, element));
|
self.elements.push((pos, element));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a frame element.
|
/// Add a group element.
|
||||||
pub fn push_frame(&mut self, pos: Point, subframe: Rc<Self>) {
|
pub fn push_frame(&mut self, pos: Point, frame: Rc<Self>) {
|
||||||
self.elements.push((pos, Element::Frame(subframe)))
|
self.elements
|
||||||
|
.push((pos, Element::Group(Group { frame, clips: false })))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add all elements of another frame, placing them relative to the given
|
/// Add all elements of another frame, placing them relative to the given
|
||||||
/// position.
|
/// position.
|
||||||
pub fn merge_frame(&mut self, pos: Point, subframe: Self) {
|
pub fn merge_frame(&mut self, pos: Point, subframe: Self) {
|
||||||
if subframe.clips {
|
if pos == Point::zero() && self.elements.is_empty() {
|
||||||
self.push_frame(pos, Rc::new(subframe));
|
|
||||||
} else if pos == Point::zero() && self.elements.is_empty() {
|
|
||||||
self.elements = subframe.elements;
|
self.elements = subframe.elements;
|
||||||
} else {
|
} else {
|
||||||
for (subpos, child) in subframe.elements {
|
for (subpos, child) in subframe.elements {
|
||||||
@ -90,7 +82,6 @@ impl Debug for Frame {
|
|||||||
f.debug_struct("Frame")
|
f.debug_struct("Frame")
|
||||||
.field("size", &self.size)
|
.field("size", &self.size)
|
||||||
.field("baseline", &self.baseline)
|
.field("baseline", &self.baseline)
|
||||||
.field("clips", &self.clips)
|
|
||||||
.field("children", &Children(&self.elements))
|
.field("children", &Children(&self.elements))
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
@ -107,9 +98,9 @@ impl<'a> Iterator for Elements<'a> {
|
|||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let (cursor, offset, frame) = self.stack.last_mut()?;
|
let (cursor, offset, frame) = self.stack.last_mut()?;
|
||||||
if let Some((pos, e)) = frame.elements.get(*cursor) {
|
if let Some((pos, e)) = frame.elements.get(*cursor) {
|
||||||
if let Element::Frame(f) = e {
|
if let Element::Group(g) = e {
|
||||||
let new_offset = *offset + *pos;
|
let new_offset = *offset + *pos;
|
||||||
self.stack.push((0, new_offset, f.as_ref()));
|
self.stack.push((0, new_offset, g.frame.as_ref()));
|
||||||
self.next()
|
self.next()
|
||||||
} else {
|
} else {
|
||||||
*cursor += 1;
|
*cursor += 1;
|
||||||
@ -128,6 +119,8 @@ impl<'a> Iterator for Elements<'a> {
|
|||||||
/// The building block frames are composed of.
|
/// The building block frames are composed of.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Element {
|
pub enum Element {
|
||||||
|
/// A group of elements.
|
||||||
|
Group(Group),
|
||||||
/// A run of shaped text.
|
/// A run of shaped text.
|
||||||
Text(Text),
|
Text(Text),
|
||||||
/// A geometric shape with optional fill and stroke.
|
/// A geometric shape with optional fill and stroke.
|
||||||
@ -136,8 +129,15 @@ pub enum Element {
|
|||||||
Image(ImageId, Size),
|
Image(ImageId, Size),
|
||||||
/// A link to an external resource and its trigger region.
|
/// A link to an external resource and its trigger region.
|
||||||
Link(String, Size),
|
Link(String, Size),
|
||||||
/// A subframe, which can be a clipping boundary.
|
}
|
||||||
Frame(Rc<Frame>),
|
|
||||||
|
/// A group of elements with optional clipping.
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Group {
|
||||||
|
/// The group's frame.
|
||||||
|
pub frame: Rc<Frame>,
|
||||||
|
/// Whether the frame should be a clipping boundary.
|
||||||
|
pub clips: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A run of shaped text.
|
/// A run of shaped text.
|
||||||
|
@ -75,12 +75,22 @@ impl Layout for ImageNode {
|
|||||||
|
|
||||||
// The position of the image so that it is centered in the canvas.
|
// The position of the image so that it is centered in the canvas.
|
||||||
let mut frame = Frame::new(canvas, canvas.h);
|
let mut frame = Frame::new(canvas, canvas.h);
|
||||||
frame.clips = self.fit == ImageFit::Cover;
|
|
||||||
frame.push(
|
frame.push(
|
||||||
(canvas - size).to_point() / 2.0,
|
(canvas - size).to_point() / 2.0,
|
||||||
Element::Image(self.id, size),
|
Element::Image(self.id, size),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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 cts = Constraints::new(regions.expand);
|
let mut cts = Constraints::new(regions.expand);
|
||||||
cts.exact = regions.current.to_spec().map(Some);
|
cts.exact = regions.current.to_spec().map(Some);
|
||||||
vec![frame.constrain(cts)]
|
vec![frame.constrain(cts)]
|
||||||
|
BIN
tests/ref/layout/background.png
Normal file
BIN
tests/ref/layout/background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
18
tests/typ/layout/background.typ
Normal file
18
tests/typ/layout/background.typ
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Test placing a background image on a page.
|
||||||
|
|
||||||
|
---
|
||||||
|
#page(paper: "a10", flip: true)
|
||||||
|
#font(fill: white)
|
||||||
|
#place(
|
||||||
|
dx: -10pt,
|
||||||
|
dy: -10pt,
|
||||||
|
image(
|
||||||
|
"../../res/tiger.jpg",
|
||||||
|
fit: "cover",
|
||||||
|
width: 100% + 20pt,
|
||||||
|
height: 100% + 20pt,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
#align(bottom, right)[
|
||||||
|
_Welcome to_ #underline[*Tigerland*]
|
||||||
|
]
|
@ -12,7 +12,7 @@ use walkdir::WalkDir;
|
|||||||
use typst::diag::Error;
|
use typst::diag::Error;
|
||||||
use typst::eval::{Smart, Value};
|
use typst::eval::{Smart, Value};
|
||||||
use typst::font::Face;
|
use typst::font::Face;
|
||||||
use typst::frame::{Element, Frame, Geometry, Shape, Stroke, Text};
|
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};
|
||||||
use typst::image::Image;
|
use typst::image::Image;
|
||||||
use typst::layout::layout;
|
use typst::layout::layout;
|
||||||
@ -431,30 +431,15 @@ fn draw_frame(
|
|||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
frame: &Frame,
|
frame: &Frame,
|
||||||
) {
|
) {
|
||||||
let mut storage;
|
|
||||||
let mask = if frame.clips {
|
|
||||||
let w = frame.size.w.to_f32();
|
|
||||||
let h = frame.size.h.to_f32();
|
|
||||||
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
|
|
||||||
let path = sk::PathBuilder::from_rect(rect).transform(ts).unwrap();
|
|
||||||
let rule = sk::FillRule::default();
|
|
||||||
storage = mask.clone();
|
|
||||||
if storage.intersect_path(&path, rule, false).is_none() {
|
|
||||||
// Fails if clipping rect is empty. In that case we just clip everything
|
|
||||||
// by returning.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
&storage
|
|
||||||
} else {
|
|
||||||
mask
|
|
||||||
};
|
|
||||||
|
|
||||||
for (pos, element) in &frame.elements {
|
for (pos, element) in &frame.elements {
|
||||||
let x = pos.x.to_f32();
|
let x = pos.x.to_f32();
|
||||||
let y = pos.y.to_f32();
|
let y = pos.y.to_f32();
|
||||||
let ts = ts.pre_translate(x, y);
|
let ts = ts.pre_translate(x, y);
|
||||||
|
|
||||||
match *element {
|
match *element {
|
||||||
|
Element::Group(ref group) => {
|
||||||
|
draw_group(canvas, ts, mask, ctx, group);
|
||||||
|
}
|
||||||
Element::Text(ref text) => {
|
Element::Text(ref text) => {
|
||||||
draw_text(canvas, ts, mask, ctx.fonts.get(text.face_id), text);
|
draw_text(canvas, ts, mask, ctx.fonts.get(text.face_id), text);
|
||||||
}
|
}
|
||||||
@ -469,13 +454,35 @@ fn draw_frame(
|
|||||||
let shape = Shape::filled(Geometry::Rect(s), fill);
|
let shape = Shape::filled(Geometry::Rect(s), fill);
|
||||||
draw_shape(canvas, ts, mask, &shape);
|
draw_shape(canvas, ts, mask, &shape);
|
||||||
}
|
}
|
||||||
Element::Frame(ref frame) => {
|
|
||||||
draw_frame(canvas, ts, mask, ctx, frame);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_group(
|
||||||
|
canvas: &mut sk::Pixmap,
|
||||||
|
ts: sk::Transform,
|
||||||
|
mask: &sk::ClipMask,
|
||||||
|
ctx: &Context,
|
||||||
|
group: &Group,
|
||||||
|
) {
|
||||||
|
if group.clips {
|
||||||
|
let w = group.frame.size.w.to_f32();
|
||||||
|
let h = group.frame.size.h.to_f32();
|
||||||
|
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
|
||||||
|
let path = sk::PathBuilder::from_rect(rect).transform(ts).unwrap();
|
||||||
|
let rule = sk::FillRule::default();
|
||||||
|
let mut mask = mask.clone();
|
||||||
|
if mask.intersect_path(&path, rule, false).is_none() {
|
||||||
|
// Fails if clipping rect is empty. In that case we just clip everything
|
||||||
|
// by returning.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
draw_frame(canvas, ts, &mask, ctx, &group.frame);
|
||||||
|
} else {
|
||||||
|
draw_frame(canvas, ts, mask, ctx, &group.frame);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_text(
|
fn draw_text(
|
||||||
canvas: &mut sk::Pixmap,
|
canvas: &mut sk::Pixmap,
|
||||||
ts: sk::Transform,
|
ts: sk::Transform,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user