Square, circle and ellipse 🔵

This commit is contained in:
Laurenz 2021-03-20 20:19:30 +01:00
parent 6cb9fe9064
commit 898728f260
34 changed files with 671 additions and 252 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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`].

View File

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

View File

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

View File

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

View File

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

View File

@ -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`.

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 803 B

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
tests/ref/library/rect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

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

View 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)

View 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?
]

View File

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

View 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)

View File

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

View 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?
]

View File

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