Square, circle and ellipse 🔵
@ -122,6 +122,7 @@ impl Exec for RawNode {
|
||||
ctx.push(FixedNode {
|
||||
width: None,
|
||||
height: None,
|
||||
aspect: None,
|
||||
child: StackNode {
|
||||
dirs: ctx.state.dirs,
|
||||
aligns: ctx.state.aligns,
|
||||
|
@ -15,8 +15,8 @@ use ttf_parser::{name_id, GlyphId};
|
||||
|
||||
use crate::color::Color;
|
||||
use crate::env::{Env, ImageResource, ResourceId};
|
||||
use crate::geom::Length;
|
||||
use crate::layout::{Element, Fill, Frame, Shape};
|
||||
use crate::geom::{self, Length, Size};
|
||||
use crate::layout::{Element, Fill, Frame, Image, Shape};
|
||||
|
||||
/// Export a collection of frames into a _PDF_ document.
|
||||
///
|
||||
@ -134,63 +134,59 @@ impl<'a> PdfExporter<'a> {
|
||||
let mut face = FaceId::MAX;
|
||||
let mut size = Length::ZERO;
|
||||
let mut fill: Option<Fill> = None;
|
||||
let mut change_color = |content: &mut Content, new_fill: Fill| {
|
||||
if fill != Some(new_fill) {
|
||||
match new_fill {
|
||||
Fill::Color(Color::Rgba(c)) => {
|
||||
content.fill_rgb(
|
||||
c.r as f32 / 255.0,
|
||||
c.g as f32 / 255.0,
|
||||
c.b as f32 / 255.0,
|
||||
);
|
||||
}
|
||||
Fill::Image(_) => todo!(),
|
||||
}
|
||||
fill = Some(new_fill);
|
||||
}
|
||||
};
|
||||
|
||||
for (pos, element) in &page.elements {
|
||||
let x = pos.x.to_pt() as f32;
|
||||
let y = (page.size.height - pos.y).to_pt() as f32;
|
||||
|
||||
match element {
|
||||
Element::Image(image) => {
|
||||
let name = format!("Im{}", self.images.map(image.res));
|
||||
let size = image.size;
|
||||
let y = (page.size.height - pos.y - size.height).to_pt() as f32;
|
||||
let w = size.width.to_pt() as f32;
|
||||
let h = size.height.to_pt() as f32;
|
||||
&Element::Image(Image { res, size: Size { width, height } }) => {
|
||||
let name = format!("Im{}", self.images.map(res));
|
||||
let w = width.to_pt() as f32;
|
||||
let h = height.to_pt() as f32;
|
||||
|
||||
content.save_state();
|
||||
content.matrix(w, 0.0, 0.0, h, x, y);
|
||||
content.matrix(w, 0.0, 0.0, h, x, y - h);
|
||||
content.x_object(Name(name.as_bytes()));
|
||||
content.restore_state();
|
||||
}
|
||||
|
||||
Element::Geometry(geometry) => {
|
||||
content.save_state();
|
||||
change_color(&mut content, geometry.fill);
|
||||
write_fill(&mut content, geometry.fill);
|
||||
|
||||
match &geometry.shape {
|
||||
Shape::Rect(r) => {
|
||||
let w = r.width.to_pt() as f32;
|
||||
let h = r.height.to_pt() as f32;
|
||||
let y = (page.size.height - pos.y - r.height).to_pt() as f32;
|
||||
match geometry.shape {
|
||||
Shape::Rect(Size { width, height }) => {
|
||||
let w = width.to_pt() as f32;
|
||||
let h = height.to_pt() as f32;
|
||||
if w > 0.0 && h > 0.0 {
|
||||
content.rect(x, y, w, h, false, true);
|
||||
content.rect(x, y - h, w, h, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
Shape::Ellipse(size) => {
|
||||
let path = geom::ellipse_path(size);
|
||||
write_path(&mut content, x, y, &path, false, true);
|
||||
}
|
||||
|
||||
Shape::Path(ref path) => {
|
||||
write_path(&mut content, x, y, path, false, true)
|
||||
}
|
||||
}
|
||||
|
||||
content.restore_state();
|
||||
}
|
||||
|
||||
Element::Text(shaped) => {
|
||||
change_color(&mut content, shaped.color);
|
||||
if fill != Some(shaped.color) {
|
||||
write_fill(&mut content, shaped.color);
|
||||
fill = Some(shaped.color);
|
||||
}
|
||||
|
||||
let mut text = content.text();
|
||||
|
||||
// Then, also check if we need to
|
||||
// issue a font switching action.
|
||||
// Then, also check if we need to issue a font switching
|
||||
// action.
|
||||
if shaped.face != face || shaped.font_size != size {
|
||||
face = shaped.face;
|
||||
size = shaped.font_size;
|
||||
@ -199,8 +195,6 @@ impl<'a> PdfExporter<'a> {
|
||||
text.font(Name(name.as_bytes()), size.to_pt() as f32);
|
||||
}
|
||||
|
||||
let x = pos.x.to_pt() as f32;
|
||||
let y = (page.size.height - pos.y).to_pt() as f32;
|
||||
text.matrix(1.0, 0.0, 0.0, 1.0, x, y);
|
||||
text.show(&shaped.encode_glyphs_be());
|
||||
}
|
||||
@ -365,6 +359,97 @@ impl<'a> PdfExporter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a fill change into a content stream.
|
||||
fn write_fill(content: &mut Content, fill: Fill) {
|
||||
match fill {
|
||||
Fill::Color(Color::Rgba(c)) => {
|
||||
content.fill_rgb(c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0);
|
||||
}
|
||||
Fill::Image(_) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a path into a content stream.
|
||||
fn write_path(
|
||||
content: &mut Content,
|
||||
x: f32,
|
||||
y: f32,
|
||||
path: &geom::Path,
|
||||
stroke: bool,
|
||||
fill: bool,
|
||||
) {
|
||||
let f = |length: Length| length.to_pt() as f32;
|
||||
let mut builder = content.path(stroke, fill);
|
||||
for elem in &path.0 {
|
||||
match elem {
|
||||
geom::PathElement::MoveTo(p) => builder.move_to(x + f(p.x), y + f(p.y)),
|
||||
geom::PathElement::LineTo(p) => builder.line_to(x + f(p.x), y + f(p.y)),
|
||||
geom::PathElement::CubicTo(p1, p2, p3) => builder.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 => builder.close_path(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// The compression level for the deflating.
|
||||
const DEFLATE_LEVEL: u8 = 6;
|
||||
|
||||
/// Encode an image with a suitable filter.
|
||||
///
|
||||
/// Skips the alpha channel as that's encoded separately.
|
||||
fn encode_image(img: &ImageResource) -> ImageResult<(Vec<u8>, Filter, ColorSpace)> {
|
||||
let mut data = vec![];
|
||||
let (filter, space) = match (img.format, &img.buf) {
|
||||
// 8-bit gray JPEG.
|
||||
(ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => {
|
||||
img.buf.write_to(&mut data, img.format)?;
|
||||
(Filter::DctDecode, ColorSpace::DeviceGray)
|
||||
}
|
||||
|
||||
// 8-bit Rgb JPEG (Cmyk JPEGs get converted to Rgb earlier).
|
||||
(ImageFormat::Jpeg, DynamicImage::ImageRgb8(_)) => {
|
||||
img.buf.write_to(&mut data, img.format)?;
|
||||
(Filter::DctDecode, ColorSpace::DeviceRgb)
|
||||
}
|
||||
|
||||
// TODO: Encode flate streams with PNG-predictor?
|
||||
|
||||
// 8-bit gray PNG.
|
||||
(ImageFormat::Png, DynamicImage::ImageLuma8(luma)) => {
|
||||
data = deflate::compress_to_vec_zlib(&luma.as_raw(), DEFLATE_LEVEL);
|
||||
(Filter::FlateDecode, ColorSpace::DeviceGray)
|
||||
}
|
||||
|
||||
// Anything else (including Rgb(a) PNGs).
|
||||
(_, buf) => {
|
||||
let (width, height) = buf.dimensions();
|
||||
let mut pixels = Vec::with_capacity(3 * width as usize * height as usize);
|
||||
for (_, _, Rgba([r, g, b, _])) in buf.pixels() {
|
||||
pixels.push(r);
|
||||
pixels.push(g);
|
||||
pixels.push(b);
|
||||
}
|
||||
|
||||
data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL);
|
||||
(Filter::FlateDecode, ColorSpace::DeviceRgb)
|
||||
}
|
||||
};
|
||||
Ok((data, filter, space))
|
||||
}
|
||||
|
||||
/// Encode an image's alpha channel if present.
|
||||
fn encode_alpha(img: &ImageResource) -> (Vec<u8>, Filter) {
|
||||
let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
|
||||
let data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL);
|
||||
(data, Filter::FlateDecode)
|
||||
}
|
||||
|
||||
/// We need to know exactly which indirect reference id will be used for which
|
||||
/// objects up-front to correctly declare the document catalogue, page tree and
|
||||
/// so on. These offsets are computed in the beginning and stored here.
|
||||
@ -485,56 +570,3 @@ where
|
||||
self.to_layout.iter().copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// The compression level for the deflating.
|
||||
const DEFLATE_LEVEL: u8 = 6;
|
||||
|
||||
/// Encode an image with a suitable filter.
|
||||
///
|
||||
/// Skips the alpha channel as that's encoded separately.
|
||||
fn encode_image(img: &ImageResource) -> ImageResult<(Vec<u8>, Filter, ColorSpace)> {
|
||||
let mut data = vec![];
|
||||
let (filter, space) = match (img.format, &img.buf) {
|
||||
// 8-bit gray JPEG.
|
||||
(ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => {
|
||||
img.buf.write_to(&mut data, img.format)?;
|
||||
(Filter::DctDecode, ColorSpace::DeviceGray)
|
||||
}
|
||||
|
||||
// 8-bit Rgb JPEG (Cmyk JPEGs get converted to Rgb earlier).
|
||||
(ImageFormat::Jpeg, DynamicImage::ImageRgb8(_)) => {
|
||||
img.buf.write_to(&mut data, img.format)?;
|
||||
(Filter::DctDecode, ColorSpace::DeviceRgb)
|
||||
}
|
||||
|
||||
// TODO: Encode flate streams with PNG-predictor?
|
||||
|
||||
// 8-bit gray PNG.
|
||||
(ImageFormat::Png, DynamicImage::ImageLuma8(luma)) => {
|
||||
data = deflate::compress_to_vec_zlib(&luma.as_raw(), DEFLATE_LEVEL);
|
||||
(Filter::FlateDecode, ColorSpace::DeviceGray)
|
||||
}
|
||||
|
||||
// Anything else (including Rgb(a) PNGs).
|
||||
(_, buf) => {
|
||||
let (width, height) = buf.dimensions();
|
||||
let mut pixels = Vec::with_capacity(3 * width as usize * height as usize);
|
||||
for (_, _, Rgba([r, g, b, _])) in buf.pixels() {
|
||||
pixels.push(r);
|
||||
pixels.push(g);
|
||||
pixels.push(b);
|
||||
}
|
||||
|
||||
data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL);
|
||||
(Filter::FlateDecode, ColorSpace::DeviceRgb)
|
||||
}
|
||||
};
|
||||
Ok((data, filter, space))
|
||||
}
|
||||
|
||||
/// Encode an image's alpha channel if present.
|
||||
fn encode_alpha(img: &ImageResource) -> (Vec<u8>, Filter) {
|
||||
let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
|
||||
let data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL);
|
||||
(data, Filter::FlateDecode)
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ impl Length {
|
||||
}
|
||||
|
||||
/// Create a length from a number of raw units.
|
||||
pub fn raw(raw: f64) -> Self {
|
||||
pub const fn raw(raw: f64) -> Self {
|
||||
Self { raw }
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ impl Length {
|
||||
}
|
||||
|
||||
/// Get the value of this length in raw units.
|
||||
pub fn to_raw(self) -> f64 {
|
||||
pub const fn to_raw(self) -> f64 {
|
||||
self.raw
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ mod dir;
|
||||
mod gen;
|
||||
mod length;
|
||||
mod linear;
|
||||
mod path;
|
||||
mod point;
|
||||
mod relative;
|
||||
mod sides;
|
||||
@ -20,6 +21,7 @@ pub use dir::*;
|
||||
pub use gen::*;
|
||||
pub use length::*;
|
||||
pub use linear::*;
|
||||
pub use path::*;
|
||||
pub use point::*;
|
||||
pub use relative::*;
|
||||
pub use sides::*;
|
||||
|
60
src/geom/path.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use super::*;
|
||||
|
||||
/// A bezier path.
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
pub struct Path(pub Vec<PathElement>);
|
||||
|
||||
/// An element in a bezier path.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PathElement {
|
||||
MoveTo(Point),
|
||||
LineTo(Point),
|
||||
CubicTo(Point, Point, Point),
|
||||
ClosePath,
|
||||
}
|
||||
|
||||
impl Path {
|
||||
/// Create an empty path.
|
||||
pub fn new() -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
|
||||
/// Push a [`MoveTo`](PathElement::MoveTo) element.
|
||||
pub fn move_to(&mut self, p: Point) {
|
||||
self.0.push(PathElement::MoveTo(p));
|
||||
}
|
||||
|
||||
/// Push a [`LineTo`](PathElement::LineTo) element.
|
||||
pub fn line_to(&mut self, p: Point) {
|
||||
self.0.push(PathElement::LineTo(p));
|
||||
}
|
||||
|
||||
/// Push a [`CubicTo`](PathElement::CubicTo) element.
|
||||
pub fn cubic_to(&mut self, p1: Point, p2: Point, p3: Point) {
|
||||
self.0.push(PathElement::CubicTo(p1, p2, p3));
|
||||
}
|
||||
|
||||
/// Push a [`ClosePath`](PathElement::ClosePath) element.
|
||||
pub fn close_path(&mut self) {
|
||||
self.0.push(PathElement::ClosePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a path that approximates an axis-aligned ellipse.
|
||||
pub fn ellipse_path(size: Size) -> Path {
|
||||
// https://stackoverflow.com/a/2007782
|
||||
let rx = size.width / 2.0;
|
||||
let ry = size.height / 2.0;
|
||||
let m = 0.551784;
|
||||
let mx = m * rx;
|
||||
let my = m * ry;
|
||||
let z = Length::ZERO;
|
||||
let point = Point::new;
|
||||
let mut path = Path::new();
|
||||
path.move_to(point(-rx, z));
|
||||
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(rx, -my), point(mx, -ry), point(z, -ry));
|
||||
path.cubic_to(point(-mx, -ry), point(-rx, -my), point(z - rx, z));
|
||||
path
|
||||
}
|
@ -26,8 +26,8 @@ impl Relative {
|
||||
|
||||
/// Resolve this relative to the given `length`.
|
||||
pub fn resolve(self, length: Length) -> Length {
|
||||
// Zero wins over infinity.
|
||||
if self.is_zero() {
|
||||
// We don't want NaNs.
|
||||
if length.is_infinite() {
|
||||
Length::ZERO
|
||||
} else {
|
||||
self.get() * length
|
||||
|
@ -34,7 +34,7 @@ impl<T> Sides<T> {
|
||||
}
|
||||
|
||||
impl Sides<Linear> {
|
||||
/// Resolve the linear margins relative to the given `size`.
|
||||
/// Resolve the linear sides relative to the given `size`.
|
||||
pub fn resolve(self, size: Size) -> Sides<Length> {
|
||||
Sides {
|
||||
left: self.left.resolve(size.width),
|
||||
|
@ -28,7 +28,8 @@ impl Size {
|
||||
|
||||
/// Whether the other size fits into this one (smaller width and height).
|
||||
pub fn fits(self, other: Self) -> bool {
|
||||
self.width >= other.width && self.height >= other.height
|
||||
const EPS: Length = Length::raw(1e-6);
|
||||
self.width + EPS >= other.width && self.height + EPS >= other.height
|
||||
}
|
||||
|
||||
/// Whether both components are finite.
|
||||
@ -45,6 +46,11 @@ impl Size {
|
||||
pub fn is_nan(self) -> bool {
|
||||
self.width.is_nan() || self.height.is_nan()
|
||||
}
|
||||
|
||||
/// Convert to a point.
|
||||
pub fn to_point(self) -> Point {
|
||||
Point::new(self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
impl Get<SpecAxis> for Size {
|
||||
|
@ -3,25 +3,38 @@ use super::*;
|
||||
/// A node that places a rectangular filled background behind its child.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BackgroundNode {
|
||||
/// The kind of shape to use as a background.
|
||||
pub shape: BackgroundShape,
|
||||
/// The background fill.
|
||||
pub fill: Fill,
|
||||
/// The child node to be filled.
|
||||
pub child: Node,
|
||||
}
|
||||
|
||||
/// The kind of shape to use as a background.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum BackgroundShape {
|
||||
Rect,
|
||||
Ellipse,
|
||||
}
|
||||
|
||||
impl Layout for BackgroundNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
let mut layouted = self.child.layout(ctx, areas);
|
||||
let mut fragment = self.child.layout(ctx, areas);
|
||||
|
||||
for frame in layouted.frames_mut() {
|
||||
let element = Element::Geometry(Geometry {
|
||||
shape: Shape::Rect(frame.size),
|
||||
fill: self.fill,
|
||||
});
|
||||
frame.elements.insert(0, (Point::ZERO, element));
|
||||
for frame in fragment.frames_mut() {
|
||||
let (point, shape) = match self.shape {
|
||||
BackgroundShape::Rect => (Point::ZERO, Shape::Rect(frame.size)),
|
||||
BackgroundShape::Ellipse => {
|
||||
(frame.size.to_point() / 2.0, Shape::Ellipse(frame.size))
|
||||
}
|
||||
};
|
||||
|
||||
let element = Element::Geometry(Geometry { shape, fill: self.fill });
|
||||
frame.elements.insert(0, (point, element));
|
||||
}
|
||||
|
||||
layouted
|
||||
fragment
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,10 @@ pub struct FixedNode {
|
||||
pub width: Option<Linear>,
|
||||
/// The fixed height, if any.
|
||||
pub height: Option<Linear>,
|
||||
/// The fixed aspect ratio between width and height, if any.
|
||||
///
|
||||
/// The resulting frame will satisfy `width = aspect * height`.
|
||||
pub aspect: Option<f64>,
|
||||
/// The child node whose size to fix.
|
||||
pub child: Node,
|
||||
}
|
||||
@ -14,18 +18,26 @@ pub struct FixedNode {
|
||||
impl Layout for FixedNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
let Areas { current, full, .. } = areas;
|
||||
let size = Size::new(
|
||||
|
||||
let full = Size::new(
|
||||
self.width.map(|w| w.resolve(full.width)).unwrap_or(current.width),
|
||||
self.height.map(|h| h.resolve(full.height)).unwrap_or(current.height),
|
||||
);
|
||||
|
||||
let mut size = full;
|
||||
if let Some(aspect) = self.aspect {
|
||||
// Shrink the size to ensure that the aspect ratio can be satisfied.
|
||||
let width = size.width.min(aspect * size.height);
|
||||
size = Size::new(width, width / aspect);
|
||||
}
|
||||
|
||||
let fill_if = |cond| if cond { Expand::Fill } else { Expand::Fit };
|
||||
let expand = Spec::new(
|
||||
fill_if(self.width.is_some()),
|
||||
fill_if(self.height.is_some()),
|
||||
);
|
||||
|
||||
let areas = Areas::once(size, expand);
|
||||
let areas = Areas::once(size, full, expand).with_aspect(self.aspect);
|
||||
self.child.layout(ctx, &areas)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use super::Shaped;
|
||||
use crate::color::Color;
|
||||
use crate::env::ResourceId;
|
||||
use crate::geom::{Point, Size};
|
||||
use crate::geom::{Path, Point, Size};
|
||||
|
||||
/// A finished layout with elements at fixed positions.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@ -59,8 +59,12 @@ pub struct Geometry {
|
||||
/// Some shape.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Shape {
|
||||
/// A rectangle.
|
||||
/// A rectangle with its origin in the topleft corner.
|
||||
Rect(Size),
|
||||
/// An ellipse with its origin in the center.
|
||||
Ellipse(Size),
|
||||
/// A bezier path.
|
||||
Path(Path),
|
||||
}
|
||||
|
||||
/// The kind of graphic fill to be applied to a [`Shape`].
|
||||
|
@ -58,8 +58,7 @@ impl PageRun {
|
||||
/// Layout the page run.
|
||||
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Frame> {
|
||||
let areas = Areas::repeat(self.size, Spec::uniform(Expand::Fill));
|
||||
let layouted = self.child.layout(ctx, &areas);
|
||||
layouted.into_frames()
|
||||
self.child.layout(ctx, &areas).into_frames()
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,21 +88,17 @@ pub struct Areas {
|
||||
pub last: Option<Size>,
|
||||
/// Whether the frames resulting from layouting into this areas should be
|
||||
/// shrunk to fit their content or expanded to fill the area.
|
||||
///
|
||||
/// This property is handled partially by the par layouter and fully by the
|
||||
/// stack layouter.
|
||||
pub expand: Spec<Expand>,
|
||||
/// The aspect ratio the resulting frame should respect.
|
||||
///
|
||||
/// This property is only handled by the stack layouter.
|
||||
pub aspect: Option<f64>,
|
||||
}
|
||||
|
||||
impl Areas {
|
||||
/// Create a new length-1 sequence of areas with just one `area`.
|
||||
pub fn once(size: Size, expand: Spec<Expand>) -> Self {
|
||||
Self {
|
||||
current: size,
|
||||
full: size,
|
||||
backlog: vec![],
|
||||
last: None,
|
||||
expand,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new sequence of areas that repeats `area` indefinitely.
|
||||
pub fn repeat(size: Size, expand: Spec<Expand>) -> Self {
|
||||
Self {
|
||||
@ -112,6 +107,40 @@ impl Areas {
|
||||
backlog: vec![],
|
||||
last: Some(size),
|
||||
expand,
|
||||
aspect: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new length-1 sequence of areas with just one `area`.
|
||||
pub fn once(size: Size, full: Size, expand: Spec<Expand>) -> Self {
|
||||
Self {
|
||||
current: size,
|
||||
full,
|
||||
backlog: vec![],
|
||||
last: None,
|
||||
expand,
|
||||
aspect: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder-style method for setting the aspect ratio.
|
||||
pub fn with_aspect(mut self, aspect: Option<f64>) -> Self {
|
||||
self.aspect = aspect;
|
||||
self
|
||||
}
|
||||
|
||||
/// Map all areas.
|
||||
pub fn map<F>(&self, mut f: F) -> Self
|
||||
where
|
||||
F: FnMut(Size) -> Size,
|
||||
{
|
||||
Self {
|
||||
current: f(self.current),
|
||||
full: f(self.full),
|
||||
backlog: self.backlog.iter().copied().map(|s| f(s)).collect(),
|
||||
last: self.last.map(f),
|
||||
expand: self.expand,
|
||||
aspect: self.aspect,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,12 +13,12 @@ impl Layout for PadNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
let areas = shrink(areas, self.padding);
|
||||
|
||||
let mut layouted = self.child.layout(ctx, &areas);
|
||||
for frame in layouted.frames_mut() {
|
||||
let mut fragment = self.child.layout(ctx, &areas);
|
||||
for frame in fragment.frames_mut() {
|
||||
pad(frame, self.padding);
|
||||
}
|
||||
|
||||
layouted
|
||||
fragment
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,23 +30,30 @@ impl From<PadNode> for AnyNode {
|
||||
|
||||
/// Shrink all areas by the padding.
|
||||
fn shrink(areas: &Areas, padding: Sides<Linear>) -> Areas {
|
||||
let shrink = |size| size - padding.resolve(size).size();
|
||||
Areas {
|
||||
current: shrink(areas.current),
|
||||
full: shrink(areas.full),
|
||||
backlog: areas.backlog.iter().copied().map(shrink).collect(),
|
||||
last: areas.last.map(shrink),
|
||||
expand: areas.expand,
|
||||
}
|
||||
areas.map(|size| size - padding.resolve(size).size())
|
||||
}
|
||||
|
||||
/// Enlarge the frame and move all elements inwards.
|
||||
/// Pad the frame and move all elements inwards.
|
||||
fn pad(frame: &mut Frame, padding: Sides<Linear>) {
|
||||
let padding = padding.resolve(frame.size);
|
||||
let padded = solve(padding, frame.size);
|
||||
let padding = padding.resolve(padded);
|
||||
let origin = Point::new(padding.left, padding.top);
|
||||
|
||||
frame.size += padding.size();
|
||||
frame.size = padded;
|
||||
for (point, _) in &mut frame.elements {
|
||||
*point += origin;
|
||||
}
|
||||
}
|
||||
|
||||
/// Solve for the size `padded` that satisfies (approximately):
|
||||
/// `padded - padding.resolve(padded).size() == size`
|
||||
fn solve(padding: Sides<Linear>, size: Size) -> Size {
|
||||
fn solve_axis(length: Length, padding: Linear) -> Length {
|
||||
(length + padding.abs) / (1.0 - padding.rel.get())
|
||||
}
|
||||
|
||||
Size::new(
|
||||
solve_axis(size.width, padding.left + padding.right),
|
||||
solve_axis(size.height, padding.top + padding.bottom),
|
||||
)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ pub struct ParNode {
|
||||
|
||||
impl Layout for ParNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
let mut layouter = ParLayouter::new(self, areas.clone());
|
||||
let mut layouter = ParLayouter::new(self.dirs, self.line_spacing, areas.clone());
|
||||
for child in &self.children {
|
||||
match child.layout(ctx, &layouter.areas) {
|
||||
Fragment::Spacing(spacing) => layouter.push_spacing(spacing),
|
||||
@ -57,12 +57,12 @@ struct ParLayouter {
|
||||
}
|
||||
|
||||
impl ParLayouter {
|
||||
fn new(par: &ParNode, areas: Areas) -> Self {
|
||||
fn new(dirs: LayoutDirs, line_spacing: Length, areas: Areas) -> Self {
|
||||
Self {
|
||||
main: par.dirs.main.axis(),
|
||||
cross: par.dirs.cross.axis(),
|
||||
dirs: par.dirs,
|
||||
line_spacing: par.line_spacing,
|
||||
main: dirs.main.axis(),
|
||||
cross: dirs.cross.axis(),
|
||||
dirs,
|
||||
line_spacing,
|
||||
areas,
|
||||
finished: vec![],
|
||||
lines: vec![],
|
||||
@ -134,12 +134,12 @@ impl ParLayouter {
|
||||
}
|
||||
|
||||
fn finish_line(&mut self) {
|
||||
let expand = self.areas.expand.switch(self.dirs);
|
||||
let full_size = {
|
||||
let expand = self.areas.expand.switch(self.dirs);
|
||||
let full = self.areas.full.switch(self.dirs);
|
||||
Gen::new(
|
||||
self.line_size.main,
|
||||
expand.cross.resolve(self.line_size.cross.min(full.cross), full.cross),
|
||||
expand.cross.resolve(self.line_size.cross, full.cross),
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -16,7 +16,7 @@ pub struct StackNode {
|
||||
|
||||
impl Layout for StackNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
let mut layouter = StackLayouter::new(self, areas.clone());
|
||||
let mut layouter = StackLayouter::new(self.dirs, areas.clone());
|
||||
for child in &self.children {
|
||||
match child.layout(ctx, &layouter.areas) {
|
||||
Fragment::Spacing(spacing) => layouter.push_spacing(spacing),
|
||||
@ -49,10 +49,10 @@ struct StackLayouter {
|
||||
}
|
||||
|
||||
impl StackLayouter {
|
||||
fn new(stack: &StackNode, areas: Areas) -> Self {
|
||||
fn new(dirs: LayoutDirs, areas: Areas) -> Self {
|
||||
Self {
|
||||
main: stack.dirs.main.axis(),
|
||||
dirs: stack.dirs,
|
||||
main: dirs.main.axis(),
|
||||
dirs,
|
||||
areas,
|
||||
finished: vec![],
|
||||
frames: vec![],
|
||||
@ -93,12 +93,27 @@ impl StackLayouter {
|
||||
|
||||
fn finish_area(&mut self) {
|
||||
let full_size = {
|
||||
let expand = self.areas.expand.switch(self.dirs);
|
||||
let full = self.areas.full.switch(self.dirs);
|
||||
Gen::new(
|
||||
expand.main.resolve(self.used.main.min(full.main), full.main),
|
||||
expand.cross.resolve(self.used.cross.min(full.cross), full.cross),
|
||||
)
|
||||
let expand = self.areas.expand;
|
||||
let full = self.areas.full;
|
||||
let current = self.areas.current;
|
||||
let used = self.used.switch(self.dirs).to_size();
|
||||
|
||||
let mut size = Size::new(
|
||||
expand.horizontal.resolve(used.width, full.width),
|
||||
expand.vertical.resolve(used.height, full.height),
|
||||
);
|
||||
|
||||
if let Some(aspect) = self.areas.aspect {
|
||||
let width = size
|
||||
.width
|
||||
.max(aspect * size.height)
|
||||
.min(current.width)
|
||||
.min((current.height + used.height) / aspect);
|
||||
|
||||
size = Size::new(width, width / aspect);
|
||||
}
|
||||
|
||||
size.switch(self.dirs)
|
||||
};
|
||||
|
||||
let mut output = Frame::new(full_size.switch(self.dirs).to_size());
|
||||
|
@ -16,7 +16,7 @@ use super::*;
|
||||
/// - Font Stretch: `stretch`, of type `relative`, between 0.5 and 2.0.
|
||||
/// - Top edge of the font: `top-edge`, of type `vertical-font-metric`.
|
||||
/// - Bottom edge of the font: `bottom-edge`, of type `vertical-font-metric`.
|
||||
/// - Fill color the glyphs: `color`, of type `color`.
|
||||
/// - Color the glyphs: `color`, of type `color`.
|
||||
/// - Serif family definition: `serif`, of type `font-familiy-list`.
|
||||
/// - Sans-serif family definition: `sans-serif`, of type `font-familiy-list`.
|
||||
/// - Monospace family definition: `monospace`, of type `font-familiy-list`.
|
||||
|
@ -25,7 +25,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let loaded = ctx.env.resources.load(&path.v, ImageResource::parse);
|
||||
if let Some((res, img)) = loaded {
|
||||
let dimensions = img.buf.dimensions();
|
||||
ctx.push(NodeImage {
|
||||
ctx.push(ImageNode {
|
||||
res,
|
||||
dimensions,
|
||||
width,
|
||||
@ -41,7 +41,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
|
||||
/// An image node.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct NodeImage {
|
||||
struct ImageNode {
|
||||
/// How to align this image node in its parent.
|
||||
aligns: LayoutAligns,
|
||||
/// The resource id of the image file.
|
||||
@ -54,7 +54,7 @@ struct NodeImage {
|
||||
height: Option<Linear>,
|
||||
}
|
||||
|
||||
impl Layout for NodeImage {
|
||||
impl Layout for ImageNode {
|
||||
fn layout(&self, _: &mut LayoutContext, areas: &Areas) -> Fragment {
|
||||
let Areas { current, full, .. } = areas;
|
||||
|
||||
@ -90,8 +90,8 @@ impl Layout for NodeImage {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NodeImage> for AnyNode {
|
||||
fn from(image: NodeImage) -> Self {
|
||||
impl From<ImageNode> for AnyNode {
|
||||
fn from(image: ImageNode) -> Self {
|
||||
Self::new(image)
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,8 @@ pub fn new() -> Scope {
|
||||
}
|
||||
|
||||
func!("align", align);
|
||||
func!("circle", circle);
|
||||
func!("ellipse", ellipse);
|
||||
func!("font", font);
|
||||
func!("h", h);
|
||||
func!("image", image);
|
||||
@ -58,6 +60,7 @@ pub fn new() -> Scope {
|
||||
func!("page", page);
|
||||
func!("pagebreak", pagebreak);
|
||||
func!("paragraph", par);
|
||||
func!("square", square);
|
||||
func!("rect", rect);
|
||||
func!("repr", repr);
|
||||
func!("rgb", rgb);
|
||||
|
@ -1,49 +1,162 @@
|
||||
use super::*;
|
||||
use crate::layout::{BackgroundNode, Fill, FixedNode};
|
||||
use std::f64::consts::SQRT_2;
|
||||
|
||||
/// `rect`: Create a rectangular box.
|
||||
use super::*;
|
||||
use crate::color::Color;
|
||||
use crate::layout::{BackgroundNode, BackgroundShape, Fill, FixedNode, PadNode};
|
||||
|
||||
/// `rect`: Create a rectangle.
|
||||
///
|
||||
/// # Positional parameters
|
||||
/// - Body: optional, of type `template`.
|
||||
///
|
||||
/// # Named parameters
|
||||
/// - Width of the box: `width`, of type `linear` relative to parent width.
|
||||
/// - Height of the box: `height`, of type `linear` relative to parent height.
|
||||
/// - Main layouting direction: `main-dir`, of type `direction`.
|
||||
/// - Cross layouting direction: `cross-dir`, of type `direction`.
|
||||
/// - Fill color of the box: `fill`, of type `color`.
|
||||
/// - Width: `width`, of type `linear` relative to parent width.
|
||||
/// - Height: `height`, of type `linear` relative to parent height.
|
||||
/// - Fill color: `fill`, of type `color`.
|
||||
///
|
||||
/// # Return value
|
||||
/// A template that places the body into a rectangle.
|
||||
///
|
||||
/// # Relevant types and constants
|
||||
/// - Type `direction`
|
||||
/// - `ltr` (left to right)
|
||||
/// - `rtl` (right to left)
|
||||
/// - `ttb` (top to bottom)
|
||||
/// - `btt` (bottom to top)
|
||||
pub fn rect(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let width = args.get(ctx, "width");
|
||||
let height = args.get(ctx, "height");
|
||||
let main = args.get(ctx, "main-dir");
|
||||
let cross = args.get(ctx, "cross-dir");
|
||||
let fill = args.get(ctx, "fill");
|
||||
let body = args.find::<TemplateValue>(ctx).unwrap_or_default();
|
||||
rect_impl("rect", width, height, None, fill, body)
|
||||
}
|
||||
|
||||
Value::template("box", move |ctx| {
|
||||
/// `square`: Create a square.
|
||||
///
|
||||
/// # Positional parameters
|
||||
/// - Body: optional, of type `template`.
|
||||
///
|
||||
/// # Named parameters
|
||||
/// - Side length: `length`, of type `length`.
|
||||
/// - Width: `width`, of type `linear` relative to parent width.
|
||||
/// - Height: `height`, of type `linear` relative to parent height.
|
||||
/// - Fill color: `fill`, of type `color`.
|
||||
///
|
||||
/// Note that you can specify only one of `length`, `width` and `height`. The
|
||||
/// width and height parameters exist so that you can size the square relative
|
||||
/// to its parent's size, which isn't possible by setting the side length.
|
||||
///
|
||||
/// # Return value
|
||||
/// A template that places the body into a square.
|
||||
pub fn square(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let length = args.get::<Length>(ctx, "length").map(Linear::from);
|
||||
let width = length.or_else(|| args.get(ctx, "width"));
|
||||
let height = if width.is_none() { args.get(ctx, "height") } else { None };
|
||||
let fill = args.get(ctx, "fill");
|
||||
let body = args.find::<TemplateValue>(ctx).unwrap_or_default();
|
||||
rect_impl("square", width, height, Some(1.0), fill, body)
|
||||
}
|
||||
|
||||
fn rect_impl(
|
||||
name: &str,
|
||||
width: Option<Linear>,
|
||||
height: Option<Linear>,
|
||||
aspect: Option<f64>,
|
||||
fill: Option<Color>,
|
||||
body: TemplateValue,
|
||||
) -> Value {
|
||||
Value::template(name, move |ctx| {
|
||||
let snapshot = ctx.state.clone();
|
||||
|
||||
ctx.set_dirs(Gen::new(main, cross));
|
||||
|
||||
let child = ctx.exec(&body).into();
|
||||
let fixed = FixedNode { width, height, child };
|
||||
let node = FixedNode { width, height, aspect, child };
|
||||
|
||||
if let Some(color) = fill {
|
||||
ctx.push(BackgroundNode {
|
||||
shape: BackgroundShape::Rect,
|
||||
fill: Fill::Color(color),
|
||||
child: fixed.into(),
|
||||
child: node.into(),
|
||||
});
|
||||
} else {
|
||||
ctx.push(fixed);
|
||||
ctx.push(node);
|
||||
}
|
||||
|
||||
ctx.state = snapshot;
|
||||
})
|
||||
}
|
||||
|
||||
/// `ellipse`: Create an ellipse.
|
||||
///
|
||||
/// # Positional parameters
|
||||
/// - Body: optional, of type `template`.
|
||||
///
|
||||
/// # Named parameters
|
||||
/// - Width: `width`, of type `linear` relative to parent width.
|
||||
/// - Height: `height`, of type `linear` relative to parent height.
|
||||
/// - Fill color: `fill`, of type `color`.
|
||||
///
|
||||
/// # Return value
|
||||
/// A template that places the body into an ellipse.
|
||||
pub fn ellipse(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let width = args.get(ctx, "width");
|
||||
let height = args.get(ctx, "height");
|
||||
let fill = args.get(ctx, "fill");
|
||||
let body = args.find::<TemplateValue>(ctx).unwrap_or_default();
|
||||
ellipse_impl("ellipse", width, height, None, fill, body)
|
||||
}
|
||||
|
||||
/// `circle`: Create a circle.
|
||||
///
|
||||
/// # Positional parameters
|
||||
/// - Body: optional, of type `template`.
|
||||
///
|
||||
/// # Named parameters
|
||||
/// - Radius: `radius`, of type `length`.
|
||||
/// - Width: `width`, of type `linear` relative to parent width.
|
||||
/// - Height: `height`, of type `linear` relative to parent height.
|
||||
/// - Fill color: `fill`, of type `color`.
|
||||
///
|
||||
/// Note that you can specify only one of `radius`, `width` and `height`. The
|
||||
/// width and height parameters exist so that you can size the circle relative
|
||||
/// to its parent's size, which isn't possible by setting the radius.
|
||||
///
|
||||
/// # Return value
|
||||
/// A template that places the body into a circle.
|
||||
pub fn circle(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
let radius = args.get::<Length>(ctx, "radius").map(|r| 2.0 * Linear::from(r));
|
||||
let width = radius.or_else(|| args.get(ctx, "width"));
|
||||
let height = if width.is_none() { args.get(ctx, "height") } else { None };
|
||||
let fill = args.get(ctx, "fill");
|
||||
let body = args.find::<TemplateValue>(ctx).unwrap_or_default();
|
||||
ellipse_impl("circle", width, height, Some(1.0), fill, body)
|
||||
}
|
||||
|
||||
fn ellipse_impl(
|
||||
name: &str,
|
||||
width: Option<Linear>,
|
||||
height: Option<Linear>,
|
||||
aspect: Option<f64>,
|
||||
fill: Option<Color>,
|
||||
body: TemplateValue,
|
||||
) -> Value {
|
||||
Value::template(name, move |ctx| {
|
||||
// This padding ratio ensures that the rectangular padded area fits
|
||||
// perfectly into the ellipse.
|
||||
const PAD: f64 = 0.5 - SQRT_2 / 4.0;
|
||||
|
||||
let snapshot = ctx.state.clone();
|
||||
let child = ctx.exec(&body).into();
|
||||
let node = FixedNode {
|
||||
width,
|
||||
height,
|
||||
aspect,
|
||||
child: PadNode {
|
||||
padding: Sides::uniform(Relative::new(PAD).into()),
|
||||
child,
|
||||
}
|
||||
.into(),
|
||||
};
|
||||
|
||||
if let Some(color) = fill {
|
||||
ctx.push(BackgroundNode {
|
||||
shape: BackgroundShape::Ellipse,
|
||||
fill: Fill::Color(color),
|
||||
child: node.into(),
|
||||
});
|
||||
} else {
|
||||
ctx.push(node);
|
||||
}
|
||||
|
||||
ctx.state = snapshot;
|
||||
|
@ -9,7 +9,7 @@ use crate::layout::SpacingNode;
|
||||
/// # Return value
|
||||
/// A template that adds horizontal spacing.
|
||||
pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
spacing(ctx, args, SpecAxis::Horizontal)
|
||||
spacing_impl(ctx, args, SpecAxis::Horizontal)
|
||||
}
|
||||
|
||||
/// `v`: Add vertical spacing.
|
||||
@ -20,11 +20,10 @@ pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
/// # Return value
|
||||
/// A template that adds vertical spacing.
|
||||
pub fn v(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
spacing(ctx, args, SpecAxis::Vertical)
|
||||
spacing_impl(ctx, args, SpecAxis::Vertical)
|
||||
}
|
||||
|
||||
/// Apply spacing along a specific axis.
|
||||
fn spacing(ctx: &mut EvalContext, args: &mut FuncArgs, axis: SpecAxis) -> Value {
|
||||
fn spacing_impl(ctx: &mut EvalContext, args: &mut FuncArgs, axis: SpecAxis) -> Value {
|
||||
let spacing: Option<Linear> = args.require(ctx, "spacing");
|
||||
Value::template("spacing", move |ctx| {
|
||||
if let Some(linear) = spacing {
|
||||
|
BIN
tests/ref/library/circle.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
tests/ref/library/ellipse.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 803 B After Width: | Height: | Size: 1.3 KiB |
BIN
tests/ref/library/rect.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.1 KiB |
BIN
tests/ref/library/square.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
@ -3,7 +3,7 @@
|
||||
|
||||
---
|
||||
#test(type("hi"), "string")
|
||||
#test(repr([Hi #rect[there]]), "[Hi [<node box>]]")
|
||||
#test(repr([Hi #rect[there]]), "[Hi [<node rect>]]")
|
||||
|
||||
---
|
||||
// Check the output.
|
||||
|
41
tests/typ/library/circle.typ
Normal file
@ -0,0 +1,41 @@
|
||||
// Test the `circle` function.
|
||||
|
||||
---
|
||||
// Test auto sizing.
|
||||
|
||||
Auto-sized circle. \
|
||||
#circle(fill: #eb5278, align(center, center, [But, soft!]))
|
||||
|
||||
Center-aligned rect in auto-sized circle.
|
||||
#circle(fill: #43a127)[
|
||||
#align(center, center)
|
||||
#rect(fill: #9feb52, pad(5pt)[But, soft!])
|
||||
]
|
||||
|
||||
100%-width rect in auto-sized circle. \
|
||||
#circle(fill: #43a127, rect(width: 100%, fill: #9feb52)[
|
||||
But, soft! what light through yonder window breaks?
|
||||
])
|
||||
|
||||
Expanded by height.
|
||||
#circle(fill: #9feb52)[A \ B \ C]
|
||||
|
||||
---
|
||||
// Test relative sizing.
|
||||
#rect(width: 100%, height: 50pt, fill: #aaa)[
|
||||
#align(center, center)
|
||||
#font(color: #fff)
|
||||
#circle(radius: 10pt, fill: #239DAD)[A]
|
||||
#circle(height: 60%, fill: #239DAD)[B]
|
||||
#circle(width: 20% + 20pt, fill: #239DAD)[C]
|
||||
]
|
||||
|
||||
---
|
||||
// Radius wins over width and height.
|
||||
// Error: 2:23-2:34 unexpected argument
|
||||
// Error: 1:36-1:49 unexpected argument
|
||||
#circle(radius: 10pt, width: 50pt, height: 100pt, fill: #239DAD)
|
||||
|
||||
// Width wins over height.
|
||||
// Error: 22-34 unexpected argument
|
||||
#circle(width: 20pt, height: 50pt, fill: #239DAD)
|
16
tests/typ/library/ellipse.typ
Normal file
@ -0,0 +1,16 @@
|
||||
// Test the `ellipse` function.
|
||||
|
||||
---
|
||||
100% rect in 100% ellipse in fixed rect. \
|
||||
#rect(width: 3cm, height: 2cm, fill: #2a631a)[
|
||||
#ellipse(width: 100%, height: 100%, fill: #43a127)[
|
||||
#rect(width: 100%, height: 100%, fill: #9feb52)[
|
||||
#align(center, center)[Stuff inside an ellipse!]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Auto-sized ellipse. \
|
||||
#ellipse(fill: #9feb52)[
|
||||
But, soft! what light through yonder window breaks?
|
||||
]
|
@ -4,3 +4,20 @@
|
||||
First of two
|
||||
#pagebreak()
|
||||
#page(height: 40pt)
|
||||
|
||||
---
|
||||
// Make sure that you can't do page related stuff in a shape.
|
||||
A
|
||||
#rect[
|
||||
B
|
||||
// Error: 16 cannot modify page from here
|
||||
#pagebreak()
|
||||
|
||||
// Error: 11-15 cannot modify page from here
|
||||
#page("a4")
|
||||
]
|
||||
C
|
||||
|
||||
// No consequences from the page("A4") call here.
|
||||
#pagebreak()
|
||||
D
|
||||
|
27
tests/typ/library/rect.typ
Normal file
@ -0,0 +1,27 @@
|
||||
// Test shapes.
|
||||
|
||||
---
|
||||
// Test the `rect` function.
|
||||
|
||||
#page(width: 150pt)
|
||||
|
||||
// Fit to text.
|
||||
#rect(fill: #9feb52)[Textbox]
|
||||
|
||||
// Empty with fixed width and height.
|
||||
#rect(width: 3cm, height: 12pt, fill: #CB4CED)
|
||||
|
||||
// Fixed width, text height.
|
||||
#rect(width: 2cm, fill: #9650D6, pad(5pt)[Fixed and padded])
|
||||
|
||||
// Page width, fixed height.
|
||||
#rect(height: 1cm, width: 100%, fill: #734CED)[Topleft]
|
||||
|
||||
// Not visible, but creates a gap between the boxes above and below
|
||||
// due to line spacing.
|
||||
#rect(width: 2in, fill: #ff0000)
|
||||
|
||||
// These are in a row!
|
||||
#rect(width: 0.5in, height: 10pt, fill: #D6CD67)
|
||||
#rect(width: 0.5in, height: 10pt, fill: #EDD466)
|
||||
#rect(width: 0.5in, height: 10pt, fill: #E3BE62)
|
@ -1,42 +0,0 @@
|
||||
// Test shapes.
|
||||
|
||||
---
|
||||
// Test `rect` function.
|
||||
|
||||
#page("a8", flip: true)
|
||||
|
||||
// Fixed width, should have text height.
|
||||
#rect(width: 2cm, fill: #9650D6)[Legal]
|
||||
|
||||
Sometimes there is no box.
|
||||
|
||||
// Fixed height, should span line.
|
||||
#rect(height: 1cm, width: 100%, fill: #734CED)[B]
|
||||
|
||||
// Empty with fixed width and height.
|
||||
#rect(width: 6cm, height: 12pt, fill: #CB4CED)
|
||||
|
||||
// Not visible, but creates a gap between the boxes above and below.
|
||||
#rect(width: 2in, fill: #ff0000)
|
||||
|
||||
// These are in a row!
|
||||
#rect(width: 0.5in, height: 10pt, fill: #D6CD67)
|
||||
#rect(width: 0.5in, height: 10pt, fill: #EDD466)
|
||||
#rect(width: 0.5in, height: 10pt, fill: #E3BE62)
|
||||
|
||||
---
|
||||
// Make sure that you can't do page related stuff in a shape.
|
||||
A
|
||||
#rect[
|
||||
B
|
||||
// Error: 16 cannot modify page from here
|
||||
#pagebreak()
|
||||
|
||||
// Error: 11-15 cannot modify page from here
|
||||
#page("a4")
|
||||
]
|
||||
C
|
||||
|
||||
// No consequences from the page("A4") call here.
|
||||
#pagebreak()
|
||||
D
|
31
tests/typ/library/square.typ
Normal file
@ -0,0 +1,31 @@
|
||||
// Test the `square` function.
|
||||
|
||||
---
|
||||
Auto-sized square. \
|
||||
#square(fill: #239DAD)[
|
||||
#align(center)
|
||||
#pad(5pt)[
|
||||
#font(color: #fff, weight: bold)
|
||||
Typst \
|
||||
]
|
||||
]
|
||||
|
||||
---
|
||||
// Length wins over width and height.
|
||||
// Error: 2:9-2:20 unexpected argument
|
||||
// Error: 1:22-1:34 unexpected argument
|
||||
#square(width: 10cm, height: 20cm, length: 1cm, fill: #eb5278)
|
||||
|
||||
---
|
||||
// Test height overflow.
|
||||
#page(width: 75pt, height: 100pt)
|
||||
#square(fill: #9feb52)[
|
||||
But, soft! what light through yonder window breaks?
|
||||
]
|
||||
|
||||
---
|
||||
// Test width overflow.
|
||||
#page(width: 100pt, height: 75pt)
|
||||
#square(fill: #9feb52)[
|
||||
But, soft! what light through yonder window breaks?
|
||||
]
|
@ -8,18 +8,19 @@ use std::rc::Rc;
|
||||
use fontdock::fs::FsIndex;
|
||||
use image::{GenericImageView, Rgba};
|
||||
use tiny_skia::{
|
||||
Canvas, Color, ColorU8, FillRule, FilterQuality, Paint, PathBuilder, Pattern, Pixmap,
|
||||
Rect, SpreadMode, Transform,
|
||||
Canvas, Color, ColorU8, FillRule, FilterQuality, Paint, Pattern, Pixmap, Rect,
|
||||
SpreadMode, Transform,
|
||||
};
|
||||
use ttf_parser::OutlineBuilder;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use typst::color;
|
||||
use typst::diag::{Diag, DiagSet, Level, Pass};
|
||||
use typst::env::{Env, FsIndexExt, ImageResource, ResourceLoader};
|
||||
use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value};
|
||||
use typst::exec::State;
|
||||
use typst::export::pdf;
|
||||
use typst::geom::{Length, Point, Sides, Size};
|
||||
use typst::geom::{self, Length, Point, Sides, Size};
|
||||
use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, Shaped};
|
||||
use typst::library;
|
||||
use typst::parse::{LineMap, Scanner};
|
||||
@ -418,7 +419,7 @@ fn draw_text(env: &Env, canvas: &mut Canvas, pos: Point, shaped: &Shaped) {
|
||||
let y = pos.y.to_pt() as f32;
|
||||
let scale = (shaped.font_size / units_per_em as f64).to_pt() as f32;
|
||||
|
||||
let mut builder = WrappedPathBuilder(PathBuilder::new());
|
||||
let mut builder = WrappedPathBuilder::default();
|
||||
face.outline_glyph(glyph, &mut builder);
|
||||
|
||||
if let Some(path) = builder.0.finish() {
|
||||
@ -426,7 +427,7 @@ fn draw_text(env: &Env, canvas: &mut Canvas, pos: Point, shaped: &Shaped) {
|
||||
.transform(&Transform::from_row(scale, 0.0, 0.0, -scale, x, y).unwrap())
|
||||
.unwrap();
|
||||
|
||||
let mut paint = paint_from_fill(shaped.color);
|
||||
let mut paint = convert_fill(shaped.color);
|
||||
paint.anti_alias = true;
|
||||
|
||||
canvas.fill_path(&placed, &paint, FillRule::default());
|
||||
@ -438,28 +439,27 @@ fn draw_geometry(_: &Env, canvas: &mut Canvas, pos: Point, element: &Geometry) {
|
||||
let x = pos.x.to_pt() as f32;
|
||||
let y = pos.y.to_pt() as f32;
|
||||
|
||||
let paint = paint_from_fill(element.fill);
|
||||
let paint = convert_fill(element.fill);
|
||||
let rule = FillRule::default();
|
||||
|
||||
match &element.shape {
|
||||
Shape::Rect(s) => {
|
||||
let (w, h) = (s.width.to_pt() as f32, s.height.to_pt() as f32);
|
||||
canvas.fill_rect(Rect::from_xywh(x, y, w, h).unwrap(), &paint);
|
||||
match element.shape {
|
||||
Shape::Rect(Size { width, height }) => {
|
||||
let w = width.to_pt() as f32;
|
||||
let h = height.to_pt() as f32;
|
||||
let rect = Rect::from_xywh(x, y, w, h).unwrap();
|
||||
canvas.fill_rect(rect, &paint);
|
||||
}
|
||||
Shape::Ellipse(size) => {
|
||||
let path = convert_path(x, y, &geom::ellipse_path(size));
|
||||
canvas.fill_path(&path, &paint, rule);
|
||||
}
|
||||
Shape::Path(ref path) => {
|
||||
let path = convert_path(x, y, path);
|
||||
canvas.fill_path(&path, &paint, rule);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn paint_from_fill(fill: Fill) -> Paint<'static> {
|
||||
let mut paint = Paint::default();
|
||||
match fill {
|
||||
Fill::Color(c) => match c {
|
||||
typst::color::Color::Rgba(c) => paint.set_color_rgba8(c.r, c.g, c.b, c.a),
|
||||
},
|
||||
Fill::Image(_) => todo!(),
|
||||
}
|
||||
|
||||
paint
|
||||
}
|
||||
|
||||
fn draw_image(env: &Env, canvas: &mut Canvas, pos: Point, element: &Image) {
|
||||
let img = &env.resources.loaded::<ImageResource>(element.res);
|
||||
|
||||
@ -492,7 +492,40 @@ fn draw_image(env: &Env, canvas: &mut Canvas, pos: Point, element: &Image) {
|
||||
);
|
||||
}
|
||||
|
||||
struct WrappedPathBuilder(PathBuilder);
|
||||
fn convert_fill(fill: Fill) -> Paint<'static> {
|
||||
let mut paint = Paint::default();
|
||||
match fill {
|
||||
Fill::Color(c) => match c {
|
||||
color::Color::Rgba(c) => paint.set_color_rgba8(c.r, c.g, c.b, c.a),
|
||||
},
|
||||
Fill::Image(_) => todo!(),
|
||||
}
|
||||
paint
|
||||
}
|
||||
|
||||
fn convert_path(x: f32, y: f32, path: &geom::Path) -> tiny_skia::Path {
|
||||
let f = |length: Length| length.to_pt() as f32;
|
||||
let mut builder = tiny_skia::PathBuilder::new();
|
||||
for elem in &path.0 {
|
||||
match elem {
|
||||
geom::PathElement::MoveTo(p) => builder.move_to(x + f(p.x), y + f(p.y)),
|
||||
geom::PathElement::LineTo(p) => builder.line_to(x + f(p.x), y + f(p.y)),
|
||||
geom::PathElement::CubicTo(p1, p2, p3) => builder.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 => builder.close(),
|
||||
};
|
||||
}
|
||||
builder.finish().unwrap()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WrappedPathBuilder(tiny_skia::PathBuilder);
|
||||
|
||||
impl OutlineBuilder for WrappedPathBuilder {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
|