Strokes
@ -24,7 +24,7 @@ fxhash = "0.2.1"
|
|||||||
image = { version = "0.23", default-features = false, features = ["png", "jpeg"] }
|
image = { version = "0.23", default-features = false, features = ["png", "jpeg"] }
|
||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
miniz_oxide = "0.4"
|
miniz_oxide = "0.4"
|
||||||
pdf-writer = { git = "https://github.com/typst/pdf-writer", rev = "a750b66" }
|
pdf-writer = { git = "https://github.com/typst/pdf-writer", rev = "f446079" }
|
||||||
rustybuzz = "0.4"
|
rustybuzz = "0.4"
|
||||||
serde = { version = "1", features = ["derive", "rc"] }
|
serde = { version = "1", features = ["derive", "rc"] }
|
||||||
ttf-parser = "0.12"
|
ttf-parser = "0.12"
|
||||||
@ -43,7 +43,7 @@ walkdir = { version = "2", optional = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
tiny-skia = "0.5"
|
tiny-skia = "0.6"
|
||||||
usvg = { version = "0.15", default-features = false }
|
usvg = { version = "0.15", default-features = false }
|
||||||
iai = { git = "https://github.com/reknih/iai" }
|
iai = { git = "https://github.com/reknih/iai" }
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ impl From<usize> for Value {
|
|||||||
|
|
||||||
impl From<RgbaColor> for Value {
|
impl From<RgbaColor> for Value {
|
||||||
fn from(v: RgbaColor) -> Self {
|
fn from(v: RgbaColor) -> Self {
|
||||||
Self::Color(Color::Rgba(v))
|
Self::Color(v.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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, Text};
|
use crate::frame::{Element, Frame, Geometry, 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;
|
||||||
@ -389,7 +389,7 @@ impl<'a> PageExporter<'a> {
|
|||||||
self.in_text_state = true;
|
self.in_text_state = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Element::Geometry(..) | Element::Image(..) if self.in_text_state => {
|
Element::Shape(_) | Element::Image(..) if self.in_text_state => {
|
||||||
self.content.end_text();
|
self.content.end_text();
|
||||||
self.in_text_state = false;
|
self.in_text_state = false;
|
||||||
}
|
}
|
||||||
@ -401,19 +401,11 @@ impl<'a> PageExporter<'a> {
|
|||||||
let y = y - offset.y.to_f32();
|
let y = y - offset.y.to_f32();
|
||||||
|
|
||||||
match *element {
|
match *element {
|
||||||
Element::Text(ref text) => {
|
Element::Text(ref text) => self.write_text(x, y, text),
|
||||||
self.write_text(x, y, text);
|
Element::Shape(ref shape) => self.write_shape(x, y, shape),
|
||||||
}
|
Element::Image(id, size) => self.write_image(x, y, id, size),
|
||||||
Element::Geometry(ref geometry, paint) => {
|
Element::Frame(ref frame) => self.write_frame(x, y, frame),
|
||||||
self.write_geometry(x, y, geometry, paint);
|
|
||||||
}
|
|
||||||
Element::Image(id, size) => {
|
|
||||||
self.write_image(x, y, id, size);
|
|
||||||
}
|
|
||||||
Element::Link(_, _) => {}
|
Element::Link(_, _) => {}
|
||||||
Element::Frame(ref frame) => {
|
|
||||||
self.write_frame(x, y, frame);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -482,79 +474,92 @@ impl<'a> PageExporter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write a geometrical shape into the content stream.
|
||||||
|
fn write_shape(&mut self, x: f32, y: f32, shape: &Shape) {
|
||||||
|
if shape.fill.is_none() && shape.stroke.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match shape.geometry {
|
||||||
|
Geometry::Rect(size) => {
|
||||||
|
let w = size.w.to_f32();
|
||||||
|
let h = size.h.to_f32();
|
||||||
|
if w > 0.0 && h > 0.0 {
|
||||||
|
self.content.rect(x, y - h, w, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Geometry::Ellipse(size) => {
|
||||||
|
let approx = geom::Path::ellipse(size);
|
||||||
|
self.write_path(x, y, &approx);
|
||||||
|
}
|
||||||
|
Geometry::Line(target) => {
|
||||||
|
let dx = target.x.to_f32();
|
||||||
|
let dy = target.y.to_f32();
|
||||||
|
self.content.move_to(x, y);
|
||||||
|
self.content.line_to(x + dx, y - dy);
|
||||||
|
}
|
||||||
|
Geometry::Path(ref path) => {
|
||||||
|
self.write_path(x, y, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.content.save_state();
|
||||||
|
|
||||||
|
if let Some(fill) = shape.fill {
|
||||||
|
self.write_fill(fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(stroke) = shape.stroke {
|
||||||
|
self.write_stroke(stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
match (shape.fill, shape.stroke) {
|
||||||
|
(None, None) => unreachable!(),
|
||||||
|
(Some(_), None) => self.content.fill_nonzero(),
|
||||||
|
(None, Some(_)) => self.content.stroke(),
|
||||||
|
(Some(_), Some(_)) => self.content.fill_nonzero_and_stroke(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.content.restore_state();
|
||||||
|
}
|
||||||
|
|
||||||
/// Write an image into the content stream.
|
/// Write an image into the content stream.
|
||||||
fn write_image(&mut self, x: f32, y: f32, id: ImageId, size: Size) {
|
fn write_image(&mut self, x: f32, y: f32, id: ImageId, size: Size) {
|
||||||
let name = format!("Im{}", self.image_map.map(id));
|
let name = format!("Im{}", self.image_map.map(id));
|
||||||
let w = size.w.to_f32();
|
let w = size.w.to_f32();
|
||||||
let h = size.h.to_f32();
|
let h = size.h.to_f32();
|
||||||
|
|
||||||
self.content.save_state();
|
self.content.save_state();
|
||||||
self.content.concat_matrix([w, 0.0, 0.0, h, x, y - h]);
|
self.content.concat_matrix([w, 0.0, 0.0, h, x, y - h]);
|
||||||
self.content.x_object(Name(name.as_bytes()));
|
self.content.x_object(Name(name.as_bytes()));
|
||||||
self.content.restore_state();
|
self.content.restore_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a geometrical shape into the content stream.
|
/// Write a path into a content stream.
|
||||||
fn write_geometry(&mut self, x: f32, y: f32, geometry: &Geometry, paint: Paint) {
|
fn write_path(&mut self, x: f32, y: f32, path: &geom::Path) {
|
||||||
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 {
|
for elem in &path.0 {
|
||||||
match elem {
|
match elem {
|
||||||
geom::PathElement::MoveTo(p) => {
|
geom::PathElement::MoveTo(p) => {
|
||||||
self.content.move_to(x + p.x.to_f32(), y + p.y.to_f32())
|
self.content.move_to(x + p.x.to_f32(), y - p.y.to_f32())
|
||||||
}
|
}
|
||||||
geom::PathElement::LineTo(p) => {
|
geom::PathElement::LineTo(p) => {
|
||||||
self.content.line_to(x + p.x.to_f32(), y + p.y.to_f32())
|
self.content.line_to(x + p.x.to_f32(), y - p.y.to_f32())
|
||||||
}
|
}
|
||||||
geom::PathElement::CubicTo(p1, p2, p3) => self.content.cubic_to(
|
geom::PathElement::CubicTo(p1, p2, p3) => self.content.cubic_to(
|
||||||
x + p1.x.to_f32(),
|
x + p1.x.to_f32(),
|
||||||
y + p1.y.to_f32(),
|
y - p1.y.to_f32(),
|
||||||
x + p2.x.to_f32(),
|
x + p2.x.to_f32(),
|
||||||
y + p2.y.to_f32(),
|
y - p2.y.to_f32(),
|
||||||
x + p3.x.to_f32(),
|
x + p3.x.to_f32(),
|
||||||
y + p3.y.to_f32(),
|
y - p3.y.to_f32(),
|
||||||
),
|
),
|
||||||
geom::PathElement::ClosePath => self.content.close_path(),
|
geom::PathElement::ClosePath => self.content.close_path(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
self.content.fill_nonzero();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a fill change into a content stream.
|
/// Write a fill change into a content stream.
|
||||||
fn write_fill(&mut self, fill: Paint) {
|
fn write_fill(&mut self, fill: Paint) {
|
||||||
let Paint::Color(Color::Rgba(c)) = fill;
|
let Paint::Solid(Color::Rgba(c)) = fill;
|
||||||
self.content.set_fill_rgb(
|
self.content.set_fill_rgb(
|
||||||
c.r as f32 / 255.0,
|
c.r as f32 / 255.0,
|
||||||
c.g as f32 / 255.0,
|
c.g as f32 / 255.0,
|
||||||
@ -563,14 +568,14 @@ impl<'a> PageExporter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Write a stroke change into a content stream.
|
/// Write a stroke change into a content stream.
|
||||||
fn write_stroke(&mut self, stroke: Paint, thickness: f32) {
|
fn write_stroke(&mut self, stroke: Stroke) {
|
||||||
let Paint::Color(Color::Rgba(c)) = stroke;
|
let Paint::Solid(Color::Rgba(c)) = stroke.paint;
|
||||||
self.content.set_stroke_rgb(
|
self.content.set_stroke_rgb(
|
||||||
c.r as f32 / 255.0,
|
c.r as f32 / 255.0,
|
||||||
c.g as f32 / 255.0,
|
c.g as f32 / 255.0,
|
||||||
c.b as f32 / 255.0,
|
c.b as f32 / 255.0,
|
||||||
);
|
);
|
||||||
self.content.set_line_width(thickness);
|
self.content.set_line_width(stroke.thickness.to_f32());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
14
src/font.rs
@ -187,11 +187,11 @@ pub struct Face {
|
|||||||
/// Metrics for a decorative line.
|
/// Metrics for a decorative line.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct LineMetrics {
|
pub struct LineMetrics {
|
||||||
/// The thickness of the line.
|
|
||||||
pub strength: Em,
|
|
||||||
/// The vertical offset of the line from the baseline. Positive goes
|
/// The vertical offset of the line from the baseline. Positive goes
|
||||||
/// upwards, negative downwards.
|
/// upwards, negative downwards.
|
||||||
pub position: Em,
|
pub position: Em,
|
||||||
|
/// The thickness of the line.
|
||||||
|
pub thickness: Em,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Face {
|
impl Face {
|
||||||
@ -218,22 +218,22 @@ impl Face {
|
|||||||
let underline = ttf.underline_metrics();
|
let underline = ttf.underline_metrics();
|
||||||
|
|
||||||
let strikethrough = LineMetrics {
|
let strikethrough = LineMetrics {
|
||||||
strength: strikeout
|
position: strikeout.map_or(Em::new(0.25), |s| to_em(s.position)),
|
||||||
|
thickness: strikeout
|
||||||
.or(underline)
|
.or(underline)
|
||||||
.map_or(Em::new(0.06), |s| to_em(s.thickness)),
|
.map_or(Em::new(0.06), |s| to_em(s.thickness)),
|
||||||
position: strikeout.map_or(Em::new(0.25), |s| to_em(s.position)),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let underline = LineMetrics {
|
let underline = LineMetrics {
|
||||||
strength: underline
|
position: underline.map_or(Em::new(-0.2), |s| to_em(s.position)),
|
||||||
|
thickness: underline
|
||||||
.or(strikeout)
|
.or(strikeout)
|
||||||
.map_or(Em::new(0.06), |s| to_em(s.thickness)),
|
.map_or(Em::new(0.06), |s| to_em(s.thickness)),
|
||||||
position: underline.map_or(Em::new(-0.2), |s| to_em(s.position)),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let overline = LineMetrics {
|
let overline = LineMetrics {
|
||||||
strength: underline.strength,
|
|
||||||
position: cap_height + Em::new(0.1),
|
position: cap_height + Em::new(0.1),
|
||||||
|
thickness: underline.thickness,
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
|
59
src/frame.rs
@ -128,14 +128,13 @@ 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 {
|
||||||
/// Shaped text.
|
/// A run of shaped text.
|
||||||
Text(Text),
|
Text(Text),
|
||||||
/// A geometric shape and the paint which with it should be filled or
|
/// A geometric shape with optional fill and stroke.
|
||||||
/// stroked (which one depends on the kind of geometry).
|
Shape(Shape),
|
||||||
Geometry(Geometry, Paint),
|
/// A raster image and its size.
|
||||||
/// A raster image.
|
|
||||||
Image(ImageId, Size),
|
Image(ImageId, Size),
|
||||||
/// A link to an external resource.
|
/// A link to an external resource and its trigger region.
|
||||||
Link(String, Size),
|
Link(String, Size),
|
||||||
/// A subframe, which can be a clipping boundary.
|
/// A subframe, which can be a clipping boundary.
|
||||||
Frame(Rc<Frame>),
|
Frame(Rc<Frame>),
|
||||||
@ -167,15 +166,51 @@ pub struct Glyph {
|
|||||||
pub x_offset: Em,
|
pub x_offset: Em,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A geometric shape.
|
/// A geometric shape with optional fill and stroke.
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Shape {
|
||||||
|
/// The shape's geometry.
|
||||||
|
pub geometry: Geometry,
|
||||||
|
/// The shape's background fill.
|
||||||
|
pub fill: Option<Paint>,
|
||||||
|
/// The shape's border stroke.
|
||||||
|
pub stroke: Option<Stroke>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shape {
|
||||||
|
/// Create a filled shape without a stroke.
|
||||||
|
pub fn filled(geometry: Geometry, fill: Paint) -> Self {
|
||||||
|
Self { geometry, fill: Some(fill), stroke: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a stroked shape without a fill.
|
||||||
|
pub fn stroked(geometry: Geometry, stroke: Stroke) -> Self {
|
||||||
|
Self {
|
||||||
|
geometry,
|
||||||
|
fill: None,
|
||||||
|
stroke: Some(stroke),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A shape's geometry.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Geometry {
|
pub enum Geometry {
|
||||||
/// A filled rectangle with its origin in the topleft corner.
|
/// A line to a point (relative to its position).
|
||||||
|
Line(Point),
|
||||||
|
/// A rectangle with its origin in the topleft corner.
|
||||||
Rect(Size),
|
Rect(Size),
|
||||||
/// A filled ellipse with its origin in the center.
|
/// A ellipse with its origin in the topleft corner.
|
||||||
Ellipse(Size),
|
Ellipse(Size),
|
||||||
/// A stroked line to a point (relative to its position) with a thickness.
|
/// A bezier path.
|
||||||
Line(Point, Length),
|
|
||||||
/// A filled bezier path.
|
|
||||||
Path(Path),
|
Path(Path),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A stroke of a geometric shape.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Stroke {
|
||||||
|
/// The stroke's paint.
|
||||||
|
pub paint: Paint,
|
||||||
|
/// The stroke's thickness.
|
||||||
|
pub thickness: Length,
|
||||||
|
}
|
||||||
|
@ -7,7 +7,16 @@ use super::*;
|
|||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||||
pub enum Paint {
|
pub enum Paint {
|
||||||
/// A solid color.
|
/// A solid color.
|
||||||
Color(Color),
|
Solid(Color),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for Paint
|
||||||
|
where
|
||||||
|
T: Into<Color>,
|
||||||
|
{
|
||||||
|
fn from(t: T) -> Self {
|
||||||
|
Self::Solid(t.into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A color in a dynamic format.
|
/// A color in a dynamic format.
|
||||||
@ -25,6 +34,12 @@ impl Debug for Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<RgbaColor> for Color {
|
||||||
|
fn from(rgba: RgbaColor) -> Self {
|
||||||
|
Self::Rgba(rgba)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An 8-bit RGBA color.
|
/// An 8-bit RGBA color.
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||||
pub struct RgbaColor {
|
pub struct RgbaColor {
|
||||||
|
@ -20,22 +20,35 @@ impl Path {
|
|||||||
Self(vec![])
|
Self(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a path that describes a rectangle.
|
||||||
|
pub fn rect(size: Size) -> Self {
|
||||||
|
let z = Length::zero();
|
||||||
|
let point = Point::new;
|
||||||
|
let mut path = Self::new();
|
||||||
|
path.move_to(point(z, z));
|
||||||
|
path.line_to(point(size.w, z));
|
||||||
|
path.line_to(point(size.w, size.h));
|
||||||
|
path.line_to(point(z, size.h));
|
||||||
|
path.close_path();
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a path that approximates an axis-aligned ellipse.
|
/// Create a path that approximates an axis-aligned ellipse.
|
||||||
pub fn ellipse(size: Size) -> Self {
|
pub fn ellipse(size: Size) -> Self {
|
||||||
// https://stackoverflow.com/a/2007782
|
// https://stackoverflow.com/a/2007782
|
||||||
|
let z = Length::zero();
|
||||||
let rx = size.w / 2.0;
|
let rx = size.w / 2.0;
|
||||||
let ry = size.h / 2.0;
|
let ry = size.h / 2.0;
|
||||||
let m = 0.551784;
|
let m = 0.551784;
|
||||||
let mx = m * rx;
|
let mx = m * rx;
|
||||||
let my = m * ry;
|
let my = m * ry;
|
||||||
let z = Length::zero();
|
let point = |x, y| Point::new(x + rx, y + ry);
|
||||||
let point = Point::new;
|
|
||||||
let mut path = Self::new();
|
let mut path = Self::new();
|
||||||
path.move_to(point(-rx, z));
|
path.move_to(point(-rx, z));
|
||||||
path.cubic_to(point(-rx, my), point(-mx, ry), point(z, ry));
|
path.cubic_to(point(-rx, -my), point(-mx, -ry), point(z, -ry));
|
||||||
path.cubic_to(point(mx, ry), point(rx, my), point(rx, z));
|
path.cubic_to(point(mx, -ry), point(rx, -my), point(rx, z));
|
||||||
path.cubic_to(point(rx, -my), point(mx, -ry), point(z, -ry));
|
path.cubic_to(point(rx, my), point(mx, ry), point(z, ry));
|
||||||
path.cubic_to(point(-mx, -ry), point(-rx, -my), point(z - rx, z));
|
path.cubic_to(point(-mx, ry), point(-rx, my), point(-rx, z));
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,20 +17,13 @@ pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
|
fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
|
||||||
let stroke = args.named("stroke")?.or_else(|| args.find());
|
let stroke = args.named("stroke")?.or_else(|| args.find()).map(Paint::Solid);
|
||||||
let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find());
|
let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find());
|
||||||
let offset = args.named("offset")?;
|
let offset = args.named("offset")?;
|
||||||
let extent = args.named("extent")?.unwrap_or_default();
|
let extent = args.named("extent")?.unwrap_or_default();
|
||||||
let body: Template = args.expect("body")?;
|
let body: Template = args.expect("body")?;
|
||||||
|
|
||||||
Ok(Value::Template(body.decorate(Decoration::Line(
|
Ok(Value::Template(body.decorate(Decoration::Line(
|
||||||
LineDecoration {
|
LineDecoration { kind, stroke, thickness, offset, extent },
|
||||||
kind,
|
|
||||||
stroke: stroke.map(Paint::Color),
|
|
||||||
thickness,
|
|
||||||
offset,
|
|
||||||
extent,
|
|
||||||
},
|
|
||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,12 +105,15 @@ impl LineDecoration {
|
|||||||
LineKind::Overline => face.overline,
|
LineKind::Overline => face.overline,
|
||||||
};
|
};
|
||||||
|
|
||||||
let stroke = self.stroke.unwrap_or(text.fill);
|
|
||||||
|
|
||||||
let thickness = self
|
let thickness = self
|
||||||
.thickness
|
.thickness
|
||||||
.map(|s| s.resolve(text.size))
|
.map(|s| s.resolve(text.size))
|
||||||
.unwrap_or(metrics.strength.to_length(text.size));
|
.unwrap_or(metrics.thickness.to_length(text.size));
|
||||||
|
|
||||||
|
let stroke = Stroke {
|
||||||
|
paint: self.stroke.unwrap_or(text.fill),
|
||||||
|
thickness,
|
||||||
|
};
|
||||||
|
|
||||||
let offset = self
|
let offset = self
|
||||||
.offset
|
.offset
|
||||||
@ -127,10 +123,9 @@ impl LineDecoration {
|
|||||||
let extent = self.extent.resolve(text.size);
|
let extent = self.extent.resolve(text.size);
|
||||||
|
|
||||||
let subpos = Point::new(pos.x - extent, pos.y + offset);
|
let subpos = Point::new(pos.x - extent, pos.y + offset);
|
||||||
let vector = Point::new(text.width + 2.0 * extent, Length::zero());
|
let target = Point::new(text.width + 2.0 * extent, Length::zero());
|
||||||
let line = Geometry::Line(vector, thickness);
|
let shape = Shape::stroked(Geometry::Line(target), stroke);
|
||||||
|
frame.push(subpos, Element::Shape(shape));
|
||||||
frame.push(subpos, Element::Geometry(line, stroke));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
let right = args.named("right")?;
|
let right = args.named("right")?;
|
||||||
let bottom = args.named("bottom")?;
|
let bottom = args.named("bottom")?;
|
||||||
let flip = args.named("flip")?;
|
let flip = args.named("flip")?;
|
||||||
let fill = args.named("fill")?;
|
let fill = args.named("fill")?.map(Paint::Solid);
|
||||||
|
|
||||||
ctx.template.modify(move |style| {
|
ctx.template.modify(move |style| {
|
||||||
let page = style.page_mut();
|
let page = style.page_mut();
|
||||||
@ -63,7 +63,7 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(fill) = fill {
|
if let Some(fill) = fill {
|
||||||
page.fill = Some(Paint::Color(fill));
|
page.fill = Some(fill);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -105,8 +105,8 @@ impl PageNode {
|
|||||||
// Add background fill if requested.
|
// Add background fill if requested.
|
||||||
if let Some(fill) = self.fill {
|
if let Some(fill) = self.fill {
|
||||||
for frame in &mut frames {
|
for frame in &mut frames {
|
||||||
let element = Element::Geometry(Geometry::Rect(frame.size), fill);
|
let shape = Shape::filled(Geometry::Rect(frame.size), fill);
|
||||||
Rc::make_mut(frame).prepend(Point::zero(), element);
|
Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,9 +7,7 @@ use crate::util::RcExt;
|
|||||||
pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||||
let width = args.named("width")?;
|
let width = args.named("width")?;
|
||||||
let height = args.named("height")?;
|
let height = args.named("height")?;
|
||||||
let fill = args.named("fill")?;
|
shape_impl(args, ShapeKind::Rect, width, height)
|
||||||
let body = args.find();
|
|
||||||
Ok(shape_impl(ShapeKind::Rect, width, height, fill, body))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `square`: A square with optional content.
|
/// `square`: A square with optional content.
|
||||||
@ -23,18 +21,14 @@ pub fn square(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
None => args.named("height")?,
|
None => args.named("height")?,
|
||||||
size => size,
|
size => size,
|
||||||
};
|
};
|
||||||
let fill = args.named("fill")?;
|
shape_impl(args, ShapeKind::Square, width, height)
|
||||||
let body = args.find();
|
|
||||||
Ok(shape_impl(ShapeKind::Square, width, height, fill, body))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `ellipse`: An ellipse with optional content.
|
/// `ellipse`: An ellipse with optional content.
|
||||||
pub fn ellipse(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
pub fn ellipse(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||||
let width = args.named("width")?;
|
let width = args.named("width")?;
|
||||||
let height = args.named("height")?;
|
let height = args.named("height")?;
|
||||||
let fill = args.named("fill")?;
|
shape_impl(args, ShapeKind::Ellipse, width, height)
|
||||||
let body = args.find();
|
|
||||||
Ok(shape_impl(ShapeKind::Ellipse, width, height, fill, body))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `circle`: A circle with optional content.
|
/// `circle`: A circle with optional content.
|
||||||
@ -48,30 +42,44 @@ pub fn circle(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
None => args.named("height")?,
|
None => args.named("height")?,
|
||||||
diameter => diameter,
|
diameter => diameter,
|
||||||
};
|
};
|
||||||
let fill = args.named("fill")?;
|
shape_impl(args, ShapeKind::Circle, width, height)
|
||||||
let body = args.find();
|
|
||||||
Ok(shape_impl(ShapeKind::Circle, width, height, fill, body))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shape_impl(
|
fn shape_impl(
|
||||||
|
args: &mut Args,
|
||||||
kind: ShapeKind,
|
kind: ShapeKind,
|
||||||
width: Option<Linear>,
|
width: Option<Linear>,
|
||||||
height: Option<Linear>,
|
height: Option<Linear>,
|
||||||
fill: Option<Color>,
|
) -> TypResult<Value> {
|
||||||
body: Option<Template>,
|
// The default appearance of a shape.
|
||||||
) -> Value {
|
let default = Stroke {
|
||||||
// Set default fill if there's no fill.
|
paint: RgbaColor::BLACK.into(),
|
||||||
let fill = fill.unwrap_or(Color::Rgba(RgbaColor::gray(175)));
|
thickness: Length::pt(1.0),
|
||||||
|
};
|
||||||
|
|
||||||
Value::Template(Template::from_inline(move |style| {
|
// Parse fill & stroke.
|
||||||
|
let fill = args.named("fill")?.map(Paint::Solid);
|
||||||
|
let stroke = match (args.named("stroke")?, args.named("thickness")?) {
|
||||||
|
(None, None) => fill.is_none().then(|| default),
|
||||||
|
(color, thickness) => Some(Stroke {
|
||||||
|
paint: color.map(Paint::Solid).unwrap_or(default.paint),
|
||||||
|
thickness: thickness.unwrap_or(default.thickness),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let padding = Sides::splat(args.named("padding")?.unwrap_or_default());
|
||||||
|
let body = args.find::<Template>();
|
||||||
|
|
||||||
|
Ok(Value::Template(Template::from_inline(move |style| {
|
||||||
ShapeNode {
|
ShapeNode {
|
||||||
kind,
|
kind,
|
||||||
fill: Some(Paint::Color(fill)),
|
fill,
|
||||||
child: body.as_ref().map(|body| body.pack(style)),
|
stroke,
|
||||||
|
child: body.as_ref().map(|body| body.pack(style).padded(padding)),
|
||||||
}
|
}
|
||||||
.pack()
|
.pack()
|
||||||
.sized(width, height)
|
.sized(width, height)
|
||||||
}))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Places its child into a sizable and fillable shape.
|
/// Places its child into a sizable and fillable shape.
|
||||||
@ -79,8 +87,10 @@ fn shape_impl(
|
|||||||
pub struct ShapeNode {
|
pub struct ShapeNode {
|
||||||
/// Which shape to place the child into.
|
/// Which shape to place the child into.
|
||||||
pub kind: ShapeKind,
|
pub kind: ShapeKind,
|
||||||
/// How to fill the shape, if at all.
|
/// How to fill the shape.
|
||||||
pub fill: Option<Paint>,
|
pub fill: Option<Paint>,
|
||||||
|
/// How the stroke the shape.
|
||||||
|
pub stroke: Option<Stroke>,
|
||||||
/// The child node to place into the shape, if any.
|
/// The child node to place into the shape, if any.
|
||||||
pub child: Option<PackedNode>,
|
pub child: Option<PackedNode>,
|
||||||
}
|
}
|
||||||
@ -160,18 +170,20 @@ impl Layout for ShapeNode {
|
|||||||
Frame::new(size, size.h)
|
Frame::new(size, size.h)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add background fill if desired.
|
// Add fill and/or stroke.
|
||||||
if let Some(fill) = self.fill {
|
if self.fill.is_some() || self.stroke.is_some() {
|
||||||
let (pos, geometry) = match self.kind {
|
let geometry = match self.kind {
|
||||||
ShapeKind::Square | ShapeKind::Rect => {
|
ShapeKind::Square | ShapeKind::Rect => Geometry::Rect(frame.size),
|
||||||
(Point::zero(), Geometry::Rect(frame.size))
|
ShapeKind::Circle | ShapeKind::Ellipse => Geometry::Ellipse(frame.size),
|
||||||
}
|
|
||||||
ShapeKind::Circle | ShapeKind::Ellipse => {
|
|
||||||
(frame.size.to_point() / 2.0, Geometry::Ellipse(frame.size))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
frame.prepend(pos, Element::Geometry(geometry, fill));
|
let shape = Shape {
|
||||||
|
geometry,
|
||||||
|
fill: self.fill,
|
||||||
|
stroke: self.stroke,
|
||||||
|
};
|
||||||
|
|
||||||
|
frame.prepend(Point::zero(), Element::Shape(shape));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure frame size matches regions size if expansion is on.
|
// Ensure frame size matches regions size if expansion is on.
|
||||||
|
@ -163,12 +163,12 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
let fallback = args.named("fallback")?;
|
let fallback = args.named("fallback")?;
|
||||||
let style = args.named("style")?;
|
let style = args.named("style")?;
|
||||||
let weight = args.named("weight")?;
|
let weight = args.named("weight")?;
|
||||||
let tracking = args.named("tracking")?;
|
|
||||||
let stretch = args.named("stretch")?;
|
let stretch = args.named("stretch")?;
|
||||||
let size = args.named::<Linear>("size")?.or_else(|| args.find());
|
let size = args.named::<Linear>("size")?.or_else(|| args.find());
|
||||||
|
let tracking = args.named("tracking")?.map(Em::new);
|
||||||
let top_edge = args.named("top-edge")?;
|
let top_edge = args.named("top-edge")?;
|
||||||
let bottom_edge = args.named("bottom-edge")?;
|
let bottom_edge = args.named("bottom-edge")?;
|
||||||
let fill = args.named("fill")?.or_else(|| args.find());
|
let fill = args.named("fill")?.or_else(|| args.find()).map(Paint::Solid);
|
||||||
let kerning = args.named("kerning")?;
|
let kerning = args.named("kerning")?;
|
||||||
let smallcaps = args.named("smallcaps")?;
|
let smallcaps = args.named("smallcaps")?;
|
||||||
let alternates = args.named("alternates")?;
|
let alternates = args.named("alternates")?;
|
||||||
@ -182,7 +182,6 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
let slashed_zero = args.named("slashed-zero")?;
|
let slashed_zero = args.named("slashed-zero")?;
|
||||||
let fractions = args.named("fractions")?;
|
let fractions = args.named("fractions")?;
|
||||||
let features = args.named("features")?;
|
let features = args.named("features")?;
|
||||||
|
|
||||||
let body = args.find::<Template>();
|
let body = args.find::<Template>();
|
||||||
|
|
||||||
macro_rules! set {
|
macro_rules! set {
|
||||||
@ -195,30 +194,19 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
|
|
||||||
let f = move |style_: &mut Style| {
|
let f = move |style_: &mut Style| {
|
||||||
let text = style_.text_mut();
|
let text = style_.text_mut();
|
||||||
|
|
||||||
if let Some(size) = size {
|
|
||||||
text.size = size.resolve(text.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(fill) = fill {
|
|
||||||
text.fill = Paint::Color(fill);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(tracking) = tracking {
|
|
||||||
text.tracking = Em::new(tracking);
|
|
||||||
}
|
|
||||||
|
|
||||||
set!(text.variant.style => style);
|
|
||||||
set!(text.variant.weight => weight);
|
|
||||||
set!(text.variant.stretch => stretch);
|
|
||||||
set!(text.top_edge => top_edge);
|
|
||||||
set!(text.bottom_edge => bottom_edge);
|
|
||||||
set!(text.fallback => fallback);
|
|
||||||
|
|
||||||
set!(text.families_mut().list => list.clone());
|
set!(text.families_mut().list => list.clone());
|
||||||
set!(text.families_mut().serif => serif.clone());
|
set!(text.families_mut().serif => serif.clone());
|
||||||
set!(text.families_mut().sans_serif => sans_serif.clone());
|
set!(text.families_mut().sans_serif => sans_serif.clone());
|
||||||
set!(text.families_mut().monospace => monospace.clone());
|
set!(text.families_mut().monospace => monospace.clone());
|
||||||
|
set!(text.fallback => fallback);
|
||||||
|
set!(text.variant.style => style);
|
||||||
|
set!(text.variant.weight => weight);
|
||||||
|
set!(text.variant.stretch => stretch);
|
||||||
|
set!(text.size => size.map(|v| v.resolve(text.size)));
|
||||||
|
set!(text.tracking => tracking);
|
||||||
|
set!(text.top_edge => top_edge);
|
||||||
|
set!(text.bottom_edge => bottom_edge);
|
||||||
|
set!(text.fill => fill);
|
||||||
set!(text.features_mut().kerning => kerning);
|
set!(text.features_mut().kerning => kerning);
|
||||||
set!(text.features_mut().smallcaps => smallcaps);
|
set!(text.features_mut().smallcaps => smallcaps);
|
||||||
set!(text.features_mut().alternates => alternates);
|
set!(text.features_mut().alternates => alternates);
|
||||||
|
@ -86,7 +86,7 @@ pub fn str(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
|
|
||||||
/// `rgb`: Create an RGB(A) color.
|
/// `rgb`: Create an RGB(A) color.
|
||||||
pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||||
Ok(Value::Color(Color::Rgba(
|
Ok(Value::from(
|
||||||
if let Some(string) = args.find::<Spanned<EcoString>>() {
|
if let Some(string) = args.find::<Spanned<EcoString>>() {
|
||||||
match RgbaColor::from_str(&string.v) {
|
match RgbaColor::from_str(&string.v) {
|
||||||
Ok(color) => color,
|
Ok(color) => color,
|
||||||
@ -106,7 +106,7 @@ pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
};
|
};
|
||||||
RgbaColor::new(f(r)?, f(g)?, f(b)?, f(a)?)
|
RgbaColor::new(f(r)?, f(g)?, f(b)?, f(a)?)
|
||||||
},
|
},
|
||||||
)))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `abs`: The absolute value of a numeric value.
|
/// `abs`: The absolute value of a numeric value.
|
||||||
|
@ -224,7 +224,7 @@ impl Default for TextStyle {
|
|||||||
},
|
},
|
||||||
top_edge: VerticalFontMetric::CapHeight,
|
top_edge: VerticalFontMetric::CapHeight,
|
||||||
bottom_edge: VerticalFontMetric::Baseline,
|
bottom_edge: VerticalFontMetric::Baseline,
|
||||||
fill: Paint::Color(Color::Rgba(RgbaColor::BLACK)),
|
fill: RgbaColor::BLACK.into(),
|
||||||
families: Rc::new(FamilyStyle::default()),
|
families: Rc::new(FamilyStyle::default()),
|
||||||
features: Rc::new(FontFeatures::default()),
|
features: Rc::new(FontFeatures::default()),
|
||||||
tracking: Em::zero(),
|
tracking: Em::zero(),
|
||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
@ -3,17 +3,18 @@
|
|||||||
---
|
---
|
||||||
// Default circle.
|
// Default circle.
|
||||||
#circle()
|
#circle()
|
||||||
|
#circle[Hey]
|
||||||
|
|
||||||
---
|
---
|
||||||
// Test auto sizing.
|
// Test auto sizing.
|
||||||
|
|
||||||
Auto-sized circle. \
|
Auto-sized circle. \
|
||||||
#circle(fill: rgb("eb5278"))[
|
#circle(fill: rgb("eb5278"), thickness: 2pt,
|
||||||
#align(center, center)[But, soft!]
|
align(center, center)[But, soft!]
|
||||||
]
|
)
|
||||||
|
|
||||||
Center-aligned rect in auto-sized circle.
|
Center-aligned rect in auto-sized circle.
|
||||||
#circle(fill: forest,
|
#circle(fill: forest, stroke: conifer,
|
||||||
align(center, center,
|
align(center, center,
|
||||||
rect(fill: conifer, pad(5pt)[But, soft!])
|
rect(fill: conifer, pad(5pt)[But, soft!])
|
||||||
)
|
)
|
||||||
@ -21,17 +22,18 @@ Center-aligned rect in auto-sized circle.
|
|||||||
|
|
||||||
Rect in auto-sized circle. \
|
Rect in auto-sized circle. \
|
||||||
#circle(fill: forest,
|
#circle(fill: forest,
|
||||||
rect(fill: conifer)[
|
rect(fill: conifer, stroke: white, padding: 4pt)[
|
||||||
|
#font(8pt)
|
||||||
But, soft! what light through yonder window breaks?
|
But, soft! what light through yonder window breaks?
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
Expanded by height.
|
Expanded by height.
|
||||||
#circle(fill: conifer)[A \ B \ C]
|
#circle(stroke: black, align(center)[A \ B \ C])
|
||||||
|
|
||||||
---
|
---
|
||||||
// Ensure circle directly in rect works.
|
// Ensure circle directly in rect works.
|
||||||
#rect(width: 40pt, height: 30pt, circle(fill: forest))
|
#rect(width: 40pt, height: 30pt, fill: forest, circle(fill: conifer))
|
||||||
|
|
||||||
---
|
---
|
||||||
// Test relative sizing.
|
// Test relative sizing.
|
||||||
|
@ -17,6 +17,7 @@ Rect in ellipse in fixed rect. \
|
|||||||
)
|
)
|
||||||
|
|
||||||
Auto-sized ellipse. \
|
Auto-sized ellipse. \
|
||||||
#ellipse(fill: conifer)[
|
#ellipse(fill: conifer, stroke: forest, thickness: 3pt, padding: 3pt)[
|
||||||
|
#font(8pt)
|
||||||
But, soft! what light through yonder window breaks?
|
But, soft! what light through yonder window breaks?
|
||||||
]
|
]
|
||||||
|
@ -8,10 +8,15 @@
|
|||||||
#page(width: 150pt)
|
#page(width: 150pt)
|
||||||
|
|
||||||
// Fit to text.
|
// Fit to text.
|
||||||
#rect(fill: conifer)[Textbox]
|
#rect(fill: conifer, padding: 3pt)[Textbox]
|
||||||
|
|
||||||
// Empty with fixed width and height.
|
// Empty with fixed width and height.
|
||||||
#rect(width: 3cm, height: 12pt, fill: rgb("ed8a4c"))
|
#block(rect(
|
||||||
|
height: 15pt,
|
||||||
|
fill: rgb("46b3c2"),
|
||||||
|
stroke: rgb("234994"),
|
||||||
|
thickness: 2pt,
|
||||||
|
))
|
||||||
|
|
||||||
// Fixed width, text height.
|
// Fixed width, text height.
|
||||||
#rect(width: 2cm, fill: rgb("9650d6"), pad(5pt)[Fixed and padded])
|
#rect(width: 2cm, fill: rgb("9650d6"), pad(5pt)[Fixed and padded])
|
||||||
|
@ -7,16 +7,16 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
// Test auto-sized square.
|
// Test auto-sized square.
|
||||||
#square(fill: eastern)[
|
#square(fill: eastern, padding: 5pt)[
|
||||||
#font(fill: white, weight: "bold")
|
#font(fill: white, weight: "bold")
|
||||||
#align(center, pad(5pt)[Typst])
|
Typst
|
||||||
]
|
]
|
||||||
|
|
||||||
---
|
---
|
||||||
// Test relative-sized child.
|
// Test relative-sized child.
|
||||||
#square(fill: eastern)[
|
#square(fill: eastern)[
|
||||||
#rect(width: 10pt, height: 5pt, fill: conifer) \
|
#rect(width: 10pt, height: 5pt, fill: conifer) \
|
||||||
#rect(width: 40%, height: 5pt, fill: conifer)
|
#rect(width: 40%, height: 5pt, stroke: conifer)
|
||||||
]
|
]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
145
tests/typeset.rs
@ -12,7 +12,7 @@ use walkdir::WalkDir;
|
|||||||
use typst::diag::Error;
|
use typst::diag::Error;
|
||||||
use typst::eval::Value;
|
use typst::eval::Value;
|
||||||
use typst::font::Face;
|
use typst::font::Face;
|
||||||
use typst::frame::{Element, Frame, Geometry, Text};
|
use typst::frame::{Element, Frame, Geometry, 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;
|
||||||
@ -388,8 +388,8 @@ 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 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 height = pad + frames.iter().map(|l| l.size.h + pad).sum::<Length>();
|
||||||
|
|
||||||
let pxw = (dpp * width.to_pt() as f32) as u32;
|
let pxw = (dpp * width.to_f32()) as u32;
|
||||||
let pxh = (dpp * height.to_pt() as f32) as u32;
|
let pxh = (dpp * height.to_f32()) as u32;
|
||||||
if pxw > 4000 || pxh > 4000 {
|
if pxw > 4000 || pxh > 4000 {
|
||||||
panic!(
|
panic!(
|
||||||
"overlarge image: {} by {} ({:?} x {:?})",
|
"overlarge image: {} by {} ({:?} x {:?})",
|
||||||
@ -405,20 +405,20 @@ fn draw(ctx: &Context, frames: &[Rc<Frame>], dpp: f32) -> sk::Pixmap {
|
|||||||
let path = sk::PathBuilder::from_rect(rect);
|
let path = sk::PathBuilder::from_rect(rect);
|
||||||
mask.set_path(pxw, pxh, &path, sk::FillRule::default(), false);
|
mask.set_path(pxw, pxh, &path, sk::FillRule::default(), false);
|
||||||
|
|
||||||
let mut ts = sk::Transform::from_scale(dpp, dpp)
|
let mut ts =
|
||||||
.pre_translate(pad.to_pt() as f32, pad.to_pt() as f32);
|
sk::Transform::from_scale(dpp, dpp).pre_translate(pad.to_f32(), pad.to_f32());
|
||||||
|
|
||||||
for frame in frames {
|
for frame in frames {
|
||||||
let mut background = sk::Paint::default();
|
let mut background = sk::Paint::default();
|
||||||
background.set_color(sk::Color::WHITE);
|
background.set_color(sk::Color::WHITE);
|
||||||
|
|
||||||
let w = frame.size.w.to_pt() as f32;
|
let w = frame.size.w.to_f32();
|
||||||
let h = frame.size.h.to_pt() as f32;
|
let h = frame.size.h.to_f32();
|
||||||
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
|
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
|
||||||
canvas.fill_rect(rect, &background, ts, None);
|
canvas.fill_rect(rect, &background, ts, None);
|
||||||
|
|
||||||
draw_frame(&mut canvas, ts, &mask, ctx, frame);
|
draw_frame(&mut canvas, ts, &mask, ctx, frame);
|
||||||
ts = ts.pre_translate(0.0, (frame.size.h + pad).to_pt() as f32);
|
ts = ts.pre_translate(0.0, (frame.size.h + pad).to_f32());
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas
|
canvas
|
||||||
@ -433,8 +433,8 @@ fn draw_frame(
|
|||||||
) {
|
) {
|
||||||
let mut storage;
|
let mut storage;
|
||||||
let mask = if frame.clips {
|
let mask = if frame.clips {
|
||||||
let w = frame.size.w.to_pt() as f32;
|
let w = frame.size.w.to_f32();
|
||||||
let h = frame.size.h.to_pt() as f32;
|
let h = frame.size.h.to_f32();
|
||||||
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
|
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
|
||||||
let path = sk::PathBuilder::from_rect(rect).transform(ts).unwrap();
|
let path = sk::PathBuilder::from_rect(rect).transform(ts).unwrap();
|
||||||
let rule = sk::FillRule::default();
|
let rule = sk::FillRule::default();
|
||||||
@ -450,24 +450,24 @@ fn draw_frame(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (pos, element) in &frame.elements {
|
for (pos, element) in &frame.elements {
|
||||||
let x = pos.x.to_pt() as f32;
|
let x = pos.x.to_f32();
|
||||||
let y = pos.y.to_pt() as 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::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);
|
||||||
}
|
}
|
||||||
Element::Geometry(ref geometry, paint) => {
|
Element::Shape(ref shape) => {
|
||||||
draw_geometry(canvas, ts, mask, geometry, paint);
|
draw_shape(canvas, ts, mask, shape);
|
||||||
}
|
}
|
||||||
Element::Image(id, size) => {
|
Element::Image(id, size) => {
|
||||||
draw_image(canvas, ts, mask, ctx.images.get(id), size);
|
draw_image(canvas, ts, mask, ctx.images.get(id), size);
|
||||||
}
|
}
|
||||||
Element::Link(_, s) => {
|
Element::Link(_, s) => {
|
||||||
let outline = Geometry::Rect(s);
|
let fill = RgbaColor::new(40, 54, 99, 40).into();
|
||||||
let paint = Paint::Color(Color::Rgba(RgbaColor::new(40, 54, 99, 40)));
|
let shape = Shape::filled(Geometry::Rect(s), fill);
|
||||||
draw_geometry(canvas, ts, mask, &outline, paint);
|
draw_shape(canvas, ts, mask, &shape);
|
||||||
}
|
}
|
||||||
Element::Frame(ref frame) => {
|
Element::Frame(ref frame) => {
|
||||||
draw_frame(canvas, ts, mask, ctx, frame);
|
draw_frame(canvas, ts, mask, ctx, frame);
|
||||||
@ -484,15 +484,15 @@ fn draw_text(
|
|||||||
text: &Text,
|
text: &Text,
|
||||||
) {
|
) {
|
||||||
let ttf = face.ttf();
|
let ttf = face.ttf();
|
||||||
let size = text.size.to_pt() as f32;
|
let size = text.size.to_f32();
|
||||||
let units_per_em = ttf.units_per_em() as f32;
|
let units_per_em = face.units_per_em as f32;
|
||||||
let pixels_per_em = text.size.to_pt() as f32 * ts.sy;
|
let pixels_per_em = text.size.to_f32() * ts.sy;
|
||||||
let scale = size / units_per_em;
|
let scale = size / units_per_em;
|
||||||
|
|
||||||
let mut x = 0.0;
|
let mut x = 0.0;
|
||||||
for glyph in &text.glyphs {
|
for glyph in &text.glyphs {
|
||||||
let glyph_id = GlyphId(glyph.id);
|
let glyph_id = GlyphId(glyph.id);
|
||||||
let offset = x + glyph.x_offset.to_length(text.size).to_pt() as f32;
|
let offset = x + glyph.x_offset.to_length(text.size).to_f32();
|
||||||
let ts = ts.pre_translate(offset, 0.0);
|
let ts = ts.pre_translate(offset, 0.0);
|
||||||
|
|
||||||
if let Some(tree) = ttf
|
if let Some(tree) = ttf
|
||||||
@ -535,56 +535,59 @@ fn draw_text(
|
|||||||
// Otherwise, draw normal outline.
|
// Otherwise, draw normal outline.
|
||||||
let mut builder = WrappedPathBuilder(sk::PathBuilder::new());
|
let mut builder = WrappedPathBuilder(sk::PathBuilder::new());
|
||||||
if ttf.outline_glyph(glyph_id, &mut builder).is_some() {
|
if ttf.outline_glyph(glyph_id, &mut builder).is_some() {
|
||||||
// Flip vertically because font designed coordinate system is Y-up.
|
// Flip vertically because font design coordinate system is Y-up.
|
||||||
let ts = ts.pre_scale(scale, -scale);
|
let ts = ts.pre_scale(scale, -scale);
|
||||||
let path = builder.0.finish().unwrap();
|
let path = builder.0.finish().unwrap();
|
||||||
let mut paint = convert_typst_paint(text.fill);
|
let paint = convert_typst_paint(text.fill);
|
||||||
paint.anti_alias = true;
|
|
||||||
canvas.fill_path(&path, &paint, sk::FillRule::default(), ts, Some(mask));
|
canvas.fill_path(&path, &paint, sk::FillRule::default(), ts, Some(mask));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
x += glyph.x_advance.to_length(text.size).to_pt() as f32;
|
x += glyph.x_advance.to_length(text.size).to_f32();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_geometry(
|
fn draw_shape(
|
||||||
canvas: &mut sk::Pixmap,
|
canvas: &mut sk::Pixmap,
|
||||||
ts: sk::Transform,
|
ts: sk::Transform,
|
||||||
mask: &sk::ClipMask,
|
mask: &sk::ClipMask,
|
||||||
geometry: &Geometry,
|
shape: &Shape,
|
||||||
paint: Paint,
|
|
||||||
) {
|
) {
|
||||||
let paint = convert_typst_paint(paint);
|
let path = match shape.geometry {
|
||||||
let rule = sk::FillRule::default();
|
Geometry::Rect(size) => {
|
||||||
|
let w = size.w.to_f32();
|
||||||
match *geometry {
|
let h = size.h.to_f32();
|
||||||
Geometry::Rect(Size { w: width, h: height }) => {
|
|
||||||
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();
|
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
|
||||||
canvas.fill_rect(rect, &paint, ts, Some(mask));
|
sk::PathBuilder::from_rect(rect)
|
||||||
}
|
}
|
||||||
Geometry::Ellipse(size) => {
|
Geometry::Ellipse(size) => {
|
||||||
let path = convert_typst_path(&geom::Path::ellipse(size));
|
let approx = geom::Path::ellipse(size);
|
||||||
canvas.fill_path(&path, &paint, rule, ts, Some(mask));
|
convert_typst_path(&approx)
|
||||||
}
|
}
|
||||||
Geometry::Line(target, thickness) => {
|
Geometry::Line(target) => {
|
||||||
let path = {
|
|
||||||
let mut builder = sk::PathBuilder::new();
|
let mut builder = sk::PathBuilder::new();
|
||||||
builder.line_to(target.x.to_pt() as f32, target.y.to_pt() as f32);
|
builder.line_to(target.x.to_f32(), target.y.to_f32());
|
||||||
builder.finish().unwrap()
|
builder.finish().unwrap()
|
||||||
|
}
|
||||||
|
Geometry::Path(ref path) => convert_typst_path(path),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut stroke = sk::Stroke::default();
|
if let Some(fill) = shape.fill {
|
||||||
stroke.width = thickness.to_pt() as f32;
|
let mut paint = convert_typst_paint(fill);
|
||||||
canvas.stroke_path(&path, &paint, &stroke, ts, Some(mask));
|
if matches!(shape.geometry, Geometry::Rect(_)) {
|
||||||
|
paint.anti_alias = false;
|
||||||
}
|
}
|
||||||
Geometry::Path(ref path) => {
|
|
||||||
let path = convert_typst_path(path);
|
let rule = sk::FillRule::default();
|
||||||
canvas.fill_path(&path, &paint, rule, ts, Some(mask));
|
canvas.fill_path(&path, &paint, rule, ts, Some(mask));
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
if let Some(Stroke { paint, thickness }) = shape.stroke {
|
||||||
|
let paint = convert_typst_paint(paint);
|
||||||
|
let mut stroke = sk::Stroke::default();
|
||||||
|
stroke.width = thickness.to_f32();
|
||||||
|
canvas.stroke_path(&path, &paint, &stroke, ts, Some(mask));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_image(
|
fn draw_image(
|
||||||
@ -600,8 +603,8 @@ fn draw_image(
|
|||||||
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
|
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
|
||||||
}
|
}
|
||||||
|
|
||||||
let view_width = size.w.to_pt() as f32;
|
let view_width = size.w.to_f32();
|
||||||
let view_height = size.h.to_pt() as f32;
|
let view_height = size.h.to_f32();
|
||||||
let scale_x = view_width as f32 / pixmap.width() as f32;
|
let scale_x = view_width as f32 / pixmap.width() as f32;
|
||||||
let scale_y = view_height as f32 / pixmap.height() as f32;
|
let scale_y = view_height as f32 / pixmap.height() as f32;
|
||||||
|
|
||||||
@ -619,25 +622,32 @@ fn draw_image(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn convert_typst_paint(paint: Paint) -> sk::Paint<'static> {
|
fn convert_typst_paint(paint: Paint) -> sk::Paint<'static> {
|
||||||
let Paint::Color(Color::Rgba(c)) = paint;
|
let Paint::Solid(Color::Rgba(c)) = paint;
|
||||||
let mut paint = sk::Paint::default();
|
let mut paint = sk::Paint::default();
|
||||||
paint.set_color_rgba8(c.r, c.g, c.b, c.a);
|
paint.set_color_rgba8(c.r, c.g, c.b, c.a);
|
||||||
|
paint.anti_alias = true;
|
||||||
paint
|
paint
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_typst_path(path: &geom::Path) -> sk::Path {
|
fn convert_typst_path(path: &geom::Path) -> sk::Path {
|
||||||
let mut builder = sk::PathBuilder::new();
|
let mut builder = sk::PathBuilder::new();
|
||||||
let f = |v: Length| v.to_pt() as f32;
|
|
||||||
for elem in &path.0 {
|
for elem in &path.0 {
|
||||||
match elem {
|
match elem {
|
||||||
PathElement::MoveTo(p) => {
|
PathElement::MoveTo(p) => {
|
||||||
builder.move_to(f(p.x), f(p.y));
|
builder.move_to(p.x.to_f32(), p.y.to_f32());
|
||||||
}
|
}
|
||||||
PathElement::LineTo(p) => {
|
PathElement::LineTo(p) => {
|
||||||
builder.line_to(f(p.x), f(p.y));
|
builder.line_to(p.x.to_f32(), p.y.to_f32());
|
||||||
}
|
}
|
||||||
PathElement::CubicTo(p1, p2, p3) => {
|
PathElement::CubicTo(p1, p2, p3) => {
|
||||||
builder.cubic_to(f(p1.x), f(p1.y), f(p2.x), f(p2.y), f(p3.x), f(p3.y));
|
builder.cubic_to(
|
||||||
|
p1.x.to_f32(),
|
||||||
|
p1.y.to_f32(),
|
||||||
|
p2.x.to_f32(),
|
||||||
|
p2.y.to_f32(),
|
||||||
|
p3.x.to_f32(),
|
||||||
|
p3.y.to_f32(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
PathElement::ClosePath => {
|
PathElement::ClosePath => {
|
||||||
builder.close();
|
builder.close();
|
||||||
@ -648,21 +658,17 @@ fn convert_typst_path(path: &geom::Path) -> sk::Path {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn convert_usvg_transform(transform: usvg::Transform) -> sk::Transform {
|
fn convert_usvg_transform(transform: usvg::Transform) -> sk::Transform {
|
||||||
let g = |v: f64| v as f32;
|
|
||||||
let usvg::Transform { a, b, c, d, e, f } = transform;
|
let usvg::Transform { a, b, c, d, e, f } = transform;
|
||||||
sk::Transform::from_row(g(a), g(b), g(c), g(d), g(e), g(f))
|
sk::Transform::from_row(a as _, b as _, c as _, d as _, e as _, f as _)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_usvg_fill(fill: &usvg::Fill) -> (sk::Paint<'static>, sk::FillRule) {
|
fn convert_usvg_fill(fill: &usvg::Fill) -> (sk::Paint<'static>, sk::FillRule) {
|
||||||
let mut paint = sk::Paint::default();
|
let mut paint = sk::Paint::default();
|
||||||
paint.anti_alias = true;
|
paint.anti_alias = true;
|
||||||
|
|
||||||
match fill.paint {
|
if let usvg::Paint::Color(usvg::Color { red, green, blue }) = fill.paint {
|
||||||
usvg::Paint::Color(usvg::Color { red, green, blue }) => {
|
|
||||||
paint.set_color_rgba8(red, green, blue, fill.opacity.to_u8())
|
paint.set_color_rgba8(red, green, blue, fill.opacity.to_u8())
|
||||||
}
|
}
|
||||||
usvg::Paint::Link(_) => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let rule = match fill.rule {
|
let rule = match fill.rule {
|
||||||
usvg::FillRule::NonZero => sk::FillRule::Winding,
|
usvg::FillRule::NonZero => sk::FillRule::Winding,
|
||||||
@ -674,17 +680,16 @@ fn convert_usvg_fill(fill: &usvg::Fill) -> (sk::Paint<'static>, sk::FillRule) {
|
|||||||
|
|
||||||
fn convert_usvg_path(path: &usvg::PathData) -> sk::Path {
|
fn convert_usvg_path(path: &usvg::PathData) -> sk::Path {
|
||||||
let mut builder = sk::PathBuilder::new();
|
let mut builder = sk::PathBuilder::new();
|
||||||
let f = |v: f64| v as f32;
|
|
||||||
for seg in path.iter() {
|
for seg in path.iter() {
|
||||||
match *seg {
|
match *seg {
|
||||||
usvg::PathSegment::MoveTo { x, y } => {
|
usvg::PathSegment::MoveTo { x, y } => {
|
||||||
builder.move_to(f(x), f(y));
|
builder.move_to(x as _, y as _);
|
||||||
}
|
}
|
||||||
usvg::PathSegment::LineTo { x, y } => {
|
usvg::PathSegment::LineTo { x, y } => {
|
||||||
builder.line_to(f(x), f(y));
|
builder.line_to(x as _, y as _);
|
||||||
}
|
}
|
||||||
usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => {
|
usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => {
|
||||||
builder.cubic_to(f(x1), f(y1), f(x2), f(y2), f(x), f(y));
|
builder.cubic_to(x1 as _, y1 as _, x2 as _, y2 as _, x as _, y as _);
|
||||||
}
|
}
|
||||||
usvg::PathSegment::ClosePath => {
|
usvg::PathSegment::ClosePath => {
|
||||||
builder.close();
|
builder.close();
|
||||||
@ -717,3 +722,15 @@ impl OutlineBuilder for WrappedPathBuilder {
|
|||||||
self.0.close();
|
self.0.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|