mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Implement space extension (multipage) ➕
This commit is contained in:
parent
a3c667895e
commit
f2f05e07b0
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,4 +2,4 @@
|
|||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
things
|
things
|
||||||
test-cache
|
tests/cache
|
||||||
|
@ -11,6 +11,9 @@ byteorder = "1"
|
|||||||
smallvec = "0.6.10"
|
smallvec = "0.6.10"
|
||||||
unicode-xid = "0.1.0"
|
unicode-xid = "0.1.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
regex = "1"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "typstc"
|
name = "typstc"
|
||||||
path = "src/bin/main.rs"
|
path = "src/bin/main.rs"
|
||||||
|
@ -80,7 +80,7 @@ impl<'d, W: Write> ExportProcess<'d, W> {
|
|||||||
) -> PdfResult<ExportProcess<'d, W>>
|
) -> PdfResult<ExportProcess<'d, W>>
|
||||||
{
|
{
|
||||||
let (fonts, font_remap) = Self::subset_fonts(layouts, font_loader)?;
|
let (fonts, font_remap) = Self::subset_fonts(layouts, font_loader)?;
|
||||||
let offsets = Self::calculate_offset(layouts.count(), fonts.len());
|
let offsets = Self::calculate_offsets(layouts.count(), fonts.len());
|
||||||
|
|
||||||
Ok(ExportProcess {
|
Ok(ExportProcess {
|
||||||
writer: PdfWriter::new(target),
|
writer: PdfWriter::new(target),
|
||||||
@ -155,7 +155,7 @@ impl<'d, W: Write> ExportProcess<'d, W> {
|
|||||||
|
|
||||||
/// We need to know in advance which IDs to use for which objects to cross-reference them.
|
/// We need to know in advance which IDs to use for which objects to cross-reference them.
|
||||||
/// Therefore, we calculate them in the beginning.
|
/// Therefore, we calculate them in the beginning.
|
||||||
fn calculate_offset(layout_count: usize, font_count: usize) -> Offsets {
|
fn calculate_offsets(layout_count: usize, font_count: usize) -> Offsets {
|
||||||
let catalog = 1;
|
let catalog = 1;
|
||||||
let page_tree = catalog + 1;
|
let page_tree = catalog + 1;
|
||||||
let pages = (page_tree + 1, page_tree + layout_count as Ref);
|
let pages = (page_tree + 1, page_tree + layout_count as Ref);
|
||||||
@ -203,7 +203,11 @@ impl<'d, W: Write> ExportProcess<'d, W> {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
// The page objects (non-root nodes in the page tree).
|
// The page objects (non-root nodes in the page tree).
|
||||||
for (id, page) in ids(self.offsets.pages).zip(self.layouts) {
|
let iter = ids(self.offsets.pages)
|
||||||
|
.zip(ids(self.offsets.contents))
|
||||||
|
.zip(self.layouts);
|
||||||
|
|
||||||
|
for ((page_id, content_id), page) in iter {
|
||||||
let rect = Rect::new(
|
let rect = Rect::new(
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
@ -212,10 +216,10 @@ impl<'d, W: Write> ExportProcess<'d, W> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
self.writer.write_obj(
|
self.writer.write_obj(
|
||||||
id,
|
page_id,
|
||||||
Page::new(self.offsets.page_tree)
|
Page::new(self.offsets.page_tree)
|
||||||
.media_box(rect)
|
.media_box(rect)
|
||||||
.contents(ids(self.offsets.contents)),
|
.content(content_id),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,22 +21,19 @@ pub struct FlexLayouter {
|
|||||||
ctx: FlexContext,
|
ctx: FlexContext,
|
||||||
units: Vec<FlexUnit>,
|
units: Vec<FlexUnit>,
|
||||||
|
|
||||||
actions: LayoutActionList,
|
stack: StackLayouter,
|
||||||
usable: Size2D,
|
usable_width: Size,
|
||||||
dimensions: Size2D,
|
|
||||||
cursor: Size2D,
|
|
||||||
|
|
||||||
run: FlexRun,
|
run: FlexRun,
|
||||||
next_glue: Option<Layout>,
|
cached_glue: Option<Layout>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The context for flex layouting.
|
/// The context for flex layouting.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct FlexContext {
|
pub struct FlexContext {
|
||||||
/// The space to layout the boxes in.
|
|
||||||
pub space: LayoutSpace,
|
pub space: LayoutSpace,
|
||||||
/// The spacing between two lines of boxes.
|
/// The spacing between two lines of boxes.
|
||||||
pub flex_spacing: Size,
|
pub flex_spacing: Size,
|
||||||
|
pub extra_space: Option<LayoutSpace>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FlexUnit {
|
enum FlexUnit {
|
||||||
@ -49,7 +46,7 @@ enum FlexUnit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct FlexRun {
|
struct FlexRun {
|
||||||
content: Vec<(Size2D, Layout)>,
|
content: Vec<(Size, Layout)>,
|
||||||
size: Size2D,
|
size: Size2D,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,17 +57,17 @@ impl FlexLayouter {
|
|||||||
ctx,
|
ctx,
|
||||||
units: vec![],
|
units: vec![],
|
||||||
|
|
||||||
actions: LayoutActionList::new(),
|
stack: StackLayouter::new(StackContext {
|
||||||
usable: ctx.space.usable(),
|
space: ctx.space,
|
||||||
dimensions: match ctx.space.alignment {
|
extra_space: ctx.extra_space,
|
||||||
Alignment::Left => Size2D::zero(),
|
}),
|
||||||
Alignment::Right => Size2D::with_x(ctx.space.usable().x),
|
|
||||||
|
usable_width: ctx.space.usable().x,
|
||||||
|
run: FlexRun {
|
||||||
|
content: vec![],
|
||||||
|
size: Size2D::zero()
|
||||||
},
|
},
|
||||||
|
cached_glue: None,
|
||||||
cursor: Size2D::new(ctx.space.padding.left, ctx.space.padding.top),
|
|
||||||
|
|
||||||
run: FlexRun::new(),
|
|
||||||
next_glue: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,12 +87,14 @@ impl FlexLayouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the justified layout.
|
/// Compute the justified layout.
|
||||||
pub fn finish(mut self) -> LayoutResult<Layout> {
|
///
|
||||||
|
/// The layouter is not consumed by this to prevent ownership problems
|
||||||
|
/// with borrowed layouters. The state of the layouter is not reset.
|
||||||
|
/// Therefore, it should not be further used after calling `finish`.
|
||||||
|
pub fn finish(&mut self) -> LayoutResult<MultiLayout> {
|
||||||
// Move the units out of the layout because otherwise, we run into
|
// Move the units out of the layout because otherwise, we run into
|
||||||
// ownership problems.
|
// ownership problems.
|
||||||
let units = self.units;
|
let units = std::mem::replace(&mut self.units, vec![]);
|
||||||
self.units = Vec::new();
|
|
||||||
|
|
||||||
for unit in units {
|
for unit in units {
|
||||||
match unit {
|
match unit {
|
||||||
FlexUnit::Boxed(boxed) => self.layout_box(boxed)?,
|
FlexUnit::Boxed(boxed) => self.layout_box(boxed)?,
|
||||||
@ -104,17 +103,88 @@ impl FlexLayouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Finish the last flex run.
|
// Finish the last flex run.
|
||||||
self.finish_flex_run();
|
self.finish_run()?;
|
||||||
|
|
||||||
Ok(Layout {
|
self.stack.finish()
|
||||||
dimensions: if self.ctx.space.shrink_to_fit {
|
}
|
||||||
self.dimensions.padded(self.ctx.space.padding)
|
|
||||||
|
/// Layout a content box into the current flex run or start a new run if
|
||||||
|
/// it does not fit.
|
||||||
|
fn layout_box(&mut self, boxed: Layout) -> LayoutResult<()> {
|
||||||
|
let glue_width = self
|
||||||
|
.cached_glue
|
||||||
|
.as_ref()
|
||||||
|
.map(|layout| layout.dimensions.x)
|
||||||
|
.unwrap_or(Size::zero());
|
||||||
|
|
||||||
|
let new_line_width = self.run.size.x + glue_width + boxed.dimensions.x;
|
||||||
|
|
||||||
|
if self.overflows_line(new_line_width) {
|
||||||
|
self.cached_glue = None;
|
||||||
|
|
||||||
|
// If the box does not even fit on its own line, then we try
|
||||||
|
// it in the next space, or we have to give up if there is none.
|
||||||
|
if self.overflows_line(boxed.dimensions.x) {
|
||||||
|
if self.ctx.extra_space.is_some() {
|
||||||
|
self.stack.finish_layout(true)?;
|
||||||
|
return self.layout_box(boxed);
|
||||||
} else {
|
} else {
|
||||||
self.ctx.space.dimensions
|
return Err(LayoutError::NotEnoughSpace("cannot fit box into flex run"));
|
||||||
},
|
}
|
||||||
actions: self.actions.into_vec(),
|
}
|
||||||
debug_render: true,
|
|
||||||
})
|
self.finish_run()?;
|
||||||
|
} else {
|
||||||
|
// Only add the glue if we did not move to a new line.
|
||||||
|
self.flush_glue();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.add_to_run(boxed);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_glue(&mut self, glue: Layout) {
|
||||||
|
self.flush_glue();
|
||||||
|
self.cached_glue = Some(glue);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_glue(&mut self) {
|
||||||
|
if let Some(glue) = self.cached_glue.take() {
|
||||||
|
let new_line_width = self.run.size.x + glue.dimensions.x;
|
||||||
|
if !self.overflows_line(new_line_width) {
|
||||||
|
self.add_to_run(glue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_to_run(&mut self, layout: Layout) {
|
||||||
|
let x = self.run.size.x;
|
||||||
|
|
||||||
|
self.run.size.x += layout.dimensions.x;
|
||||||
|
self.run.size.y = crate::size::max(self.run.size.y, layout.dimensions.y);
|
||||||
|
|
||||||
|
self.run.content.push((x, layout));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_run(&mut self) -> LayoutResult<()> {
|
||||||
|
self.run.size.y += self.ctx.flex_spacing;
|
||||||
|
|
||||||
|
let mut actions = LayoutActionList::new();
|
||||||
|
for (x, layout) in self.run.content.drain(..) {
|
||||||
|
let position = Size2D::with_x(x);
|
||||||
|
actions.add_layout(position, layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.stack.add(Layout {
|
||||||
|
dimensions: self.run.size,
|
||||||
|
actions: actions.into_vec(),
|
||||||
|
debug_render: false,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.run.size = Size2D::zero();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this layouter contains any items.
|
/// Whether this layouter contains any items.
|
||||||
@ -122,91 +192,7 @@ impl FlexLayouter {
|
|||||||
self.units.is_empty()
|
self.units.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_box(&mut self, boxed: Layout) -> LayoutResult<()> {
|
fn overflows_line(&self, line: Size) -> bool {
|
||||||
let next_glue_width = self
|
line > self.usable_width
|
||||||
.next_glue
|
|
||||||
.as_ref()
|
|
||||||
.map(|g| g.dimensions.x)
|
|
||||||
.unwrap_or(Size::zero());
|
|
||||||
|
|
||||||
let new_line_width = self.run.size.x + next_glue_width + boxed.dimensions.x;
|
|
||||||
|
|
||||||
if self.overflows(new_line_width) {
|
|
||||||
// If the box does not even fit on its own line, then
|
|
||||||
// we can't do anything.
|
|
||||||
if self.overflows(boxed.dimensions.x) {
|
|
||||||
return Err(LayoutError::NotEnoughSpace);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.next_glue = None;
|
|
||||||
self.finish_flex_run();
|
|
||||||
} else {
|
|
||||||
// Only add the glue if we did not move to a new line.
|
|
||||||
self.flush_glue();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.add_to_flex_run(boxed);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout_glue(&mut self, glue: Layout) {
|
|
||||||
self.flush_glue();
|
|
||||||
self.next_glue = Some(glue);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush_glue(&mut self) {
|
|
||||||
if let Some(glue) = self.next_glue.take() {
|
|
||||||
self.add_to_flex_run(glue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_to_flex_run(&mut self, layout: Layout) {
|
|
||||||
let position = self.cursor;
|
|
||||||
|
|
||||||
self.cursor.x += layout.dimensions.x;
|
|
||||||
self.run.size.x += layout.dimensions.x;
|
|
||||||
self.run.size.y = crate::size::max(self.run.size.y, layout.dimensions.y);
|
|
||||||
|
|
||||||
self.run.content.push((position, layout));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish_flex_run(&mut self) {
|
|
||||||
// Add all layouts from the current flex run at the correct positions.
|
|
||||||
match self.ctx.space.alignment {
|
|
||||||
Alignment::Left => {
|
|
||||||
for (position, layout) in self.run.content.drain(..) {
|
|
||||||
self.actions.add_layout(position, layout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Alignment::Right => {
|
|
||||||
let extra_space = Size2D::with_x(self.usable.x - self.run.size.x);
|
|
||||||
for (position, layout) in self.run.content.drain(..) {
|
|
||||||
self.actions.add_layout(position + extra_space, layout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.dimensions.x = crate::size::max(self.dimensions.x, self.run.size.x);
|
|
||||||
self.dimensions.y += self.ctx.flex_spacing;
|
|
||||||
self.dimensions.y += self.run.size.y;
|
|
||||||
|
|
||||||
self.cursor.x = self.ctx.space.padding.left;
|
|
||||||
self.cursor.y += self.run.size.y + self.ctx.flex_spacing;
|
|
||||||
self.run.size = Size2D::zero();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn overflows(&self, line: Size) -> bool {
|
|
||||||
line > self.usable.x
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FlexRun {
|
|
||||||
fn new() -> FlexRun {
|
|
||||||
FlexRun {
|
|
||||||
content: vec![],
|
|
||||||
size: Size2D::zero()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ impl Layout {
|
|||||||
self.dimensions.x.to_pt(),
|
self.dimensions.x.to_pt(),
|
||||||
self.dimensions.y.to_pt()
|
self.dimensions.y.to_pt()
|
||||||
)?;
|
)?;
|
||||||
|
writeln!(f, "{}", self.actions.len())?;
|
||||||
for action in &self.actions {
|
for action in &self.actions {
|
||||||
action.serialize(f)?;
|
action.serialize(f)?;
|
||||||
writeln!(f)?;
|
writeln!(f)?;
|
||||||
@ -93,6 +94,17 @@ impl MultiLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MultiLayout {
|
||||||
|
/// Serialize this collection of layouts into an output buffer.
|
||||||
|
pub fn serialize<W: Write>(&self, f: &mut W) -> io::Result<()> {
|
||||||
|
writeln!(f, "{}", self.count())?;
|
||||||
|
for layout in self {
|
||||||
|
layout.serialize(f)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IntoIterator for MultiLayout {
|
impl IntoIterator for MultiLayout {
|
||||||
type Item = Layout;
|
type Item = Layout;
|
||||||
type IntoIter = std::vec::IntoIter<Layout>;
|
type IntoIter = std::vec::IntoIter<Layout>;
|
||||||
@ -112,7 +124,7 @@ impl<'a> IntoIterator for &'a MultiLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The general context for layouting.
|
/// The general context for layouting.
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct LayoutContext<'a, 'p> {
|
pub struct LayoutContext<'a, 'p> {
|
||||||
pub loader: &'a SharedFontLoader<'p>,
|
pub loader: &'a SharedFontLoader<'p>,
|
||||||
pub style: &'a TextStyle,
|
pub style: &'a TextStyle,
|
||||||
@ -154,7 +166,7 @@ pub enum Alignment {
|
|||||||
/// The error type for layouting.
|
/// The error type for layouting.
|
||||||
pub enum LayoutError {
|
pub enum LayoutError {
|
||||||
/// There is not enough space to add an item.
|
/// There is not enough space to add an item.
|
||||||
NotEnoughSpace,
|
NotEnoughSpace(&'static str),
|
||||||
/// There was no suitable font for the given character.
|
/// There was no suitable font for the given character.
|
||||||
NoSuitableFont(char),
|
NoSuitableFont(char),
|
||||||
/// An error occured while gathering font data.
|
/// An error occured while gathering font data.
|
||||||
@ -167,7 +179,7 @@ pub type LayoutResult<T> = Result<T, LayoutError>;
|
|||||||
error_type! {
|
error_type! {
|
||||||
err: LayoutError,
|
err: LayoutError,
|
||||||
show: f => match err {
|
show: f => match err {
|
||||||
LayoutError::NotEnoughSpace => write!(f, "not enough space"),
|
LayoutError::NotEnoughSpace(desc) => write!(f, "not enough space: {}", desc),
|
||||||
LayoutError::NoSuitableFont(c) => write!(f, "no suitable font for '{}'", c),
|
LayoutError::NoSuitableFont(c) => write!(f, "no suitable font for '{}'", c),
|
||||||
LayoutError::Font(err) => write!(f, "font error: {}", err),
|
LayoutError::Font(err) => write!(f, "font error: {}", err),
|
||||||
},
|
},
|
||||||
|
@ -5,44 +5,38 @@ use super::*;
|
|||||||
/// The boxes are arranged vertically, each layout gettings it's own "line".
|
/// The boxes are arranged vertically, each layout gettings it's own "line".
|
||||||
pub struct StackLayouter {
|
pub struct StackLayouter {
|
||||||
ctx: StackContext,
|
ctx: StackContext,
|
||||||
|
layouts: MultiLayout,
|
||||||
actions: LayoutActionList,
|
actions: LayoutActionList,
|
||||||
|
|
||||||
|
space: LayoutSpace,
|
||||||
usable: Size2D,
|
usable: Size2D,
|
||||||
dimensions: Size2D,
|
dimensions: Size2D,
|
||||||
cursor: Size2D,
|
cursor: Size2D,
|
||||||
|
in_extra_space: bool,
|
||||||
|
started: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The context for stack layouting.
|
/// The context for stack layouting.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct StackContext {
|
pub struct StackContext {
|
||||||
/// The space to layout the boxes in.
|
|
||||||
pub space: LayoutSpace,
|
pub space: LayoutSpace,
|
||||||
|
pub extra_space: Option<LayoutSpace>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StackLayouter {
|
impl StackLayouter {
|
||||||
/// Create a new stack layouter.
|
/// Create a new stack layouter.
|
||||||
pub fn new(ctx: StackContext) -> StackLayouter {
|
pub fn new(ctx: StackContext) -> StackLayouter {
|
||||||
let space = ctx.space;
|
|
||||||
|
|
||||||
StackLayouter {
|
StackLayouter {
|
||||||
ctx,
|
ctx,
|
||||||
|
layouts: MultiLayout::new(),
|
||||||
actions: LayoutActionList::new(),
|
actions: LayoutActionList::new(),
|
||||||
|
|
||||||
|
space: ctx.space,
|
||||||
usable: ctx.space.usable(),
|
usable: ctx.space.usable(),
|
||||||
dimensions: match ctx.space.alignment {
|
dimensions: start_dimensions(ctx.space),
|
||||||
Alignment::Left => Size2D::zero(),
|
cursor: start_cursor(ctx.space),
|
||||||
Alignment::Right => Size2D::with_x(space.usable().x),
|
in_extra_space: false,
|
||||||
},
|
started: true,
|
||||||
|
|
||||||
cursor: Size2D::new(
|
|
||||||
// If left-align, the cursor points to the top-left corner of
|
|
||||||
// each box. If we right-align, it points to the top-right
|
|
||||||
// corner.
|
|
||||||
match ctx.space.alignment {
|
|
||||||
Alignment::Left => space.padding.left,
|
|
||||||
Alignment::Right => space.dimensions.x - space.padding.right,
|
|
||||||
},
|
|
||||||
space.padding.top,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,19 +47,30 @@ impl StackLayouter {
|
|||||||
|
|
||||||
/// Add a sublayout to the bottom.
|
/// Add a sublayout to the bottom.
|
||||||
pub fn add(&mut self, layout: Layout) -> LayoutResult<()> {
|
pub fn add(&mut self, layout: Layout) -> LayoutResult<()> {
|
||||||
|
if !self.started {
|
||||||
|
self.start_new_space()?;
|
||||||
|
}
|
||||||
|
|
||||||
let new_dimensions = Size2D {
|
let new_dimensions = Size2D {
|
||||||
x: crate::size::max(self.dimensions.x, layout.dimensions.x),
|
x: crate::size::max(self.dimensions.x, layout.dimensions.x),
|
||||||
y: self.dimensions.y + layout.dimensions.y,
|
y: self.dimensions.y + layout.dimensions.y,
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.overflows(new_dimensions) {
|
if self.overflows(new_dimensions) {
|
||||||
return Err(LayoutError::NotEnoughSpace);
|
if self.ctx.extra_space.is_some() &&
|
||||||
|
!(self.in_extra_space && self.overflows(layout.dimensions))
|
||||||
|
{
|
||||||
|
self.finish_layout(true)?;
|
||||||
|
return self.add(layout);
|
||||||
|
} else {
|
||||||
|
return Err(LayoutError::NotEnoughSpace("cannot fit box into stack"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine where to put the box. When we right-align it, we want the
|
// Determine where to put the box. When we right-align it, we want the
|
||||||
// cursor to point to the top-right corner of the box. Therefore, the
|
// cursor to point to the top-right corner of the box. Therefore, the
|
||||||
// position has to be moved to the left by the width of the box.
|
// position has to be moved to the left by the width of the box.
|
||||||
let position = match self.ctx.space.alignment {
|
let position = match self.space.alignment {
|
||||||
Alignment::Left => self.cursor,
|
Alignment::Left => self.cursor,
|
||||||
Alignment::Right => self.cursor - Size2D::with_x(layout.dimensions.x),
|
Alignment::Right => self.cursor - Size2D::with_x(layout.dimensions.x),
|
||||||
};
|
};
|
||||||
@ -88,26 +93,74 @@ impl StackLayouter {
|
|||||||
|
|
||||||
/// Add vertical space after the last layout.
|
/// Add vertical space after the last layout.
|
||||||
pub fn add_space(&mut self, space: Size) -> LayoutResult<()> {
|
pub fn add_space(&mut self, space: Size) -> LayoutResult<()> {
|
||||||
if self.overflows(self.dimensions + Size2D::with_y(space)) {
|
if !self.started {
|
||||||
return Err(LayoutError::NotEnoughSpace);
|
self.start_new_space()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let new_dimensions = self.dimensions + Size2D::with_y(space);
|
||||||
|
|
||||||
|
if self.overflows(new_dimensions) {
|
||||||
|
if self.ctx.extra_space.is_some() {
|
||||||
|
self.finish_layout(false)?;
|
||||||
|
} else {
|
||||||
|
return Err(LayoutError::NotEnoughSpace("cannot fit space into stack"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
self.cursor.y += space;
|
self.cursor.y += space;
|
||||||
self.dimensions.y += space;
|
self.dimensions.y += space;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish the layouting.
|
/// Finish the layouting.
|
||||||
pub fn finish(self) -> Layout {
|
///
|
||||||
Layout {
|
/// The layouter is not consumed by this to prevent ownership problems.
|
||||||
dimensions: if self.ctx.space.shrink_to_fit {
|
/// It should not be used further.
|
||||||
self.dimensions.padded(self.ctx.space.padding)
|
pub fn finish(&mut self) -> LayoutResult<MultiLayout> {
|
||||||
|
if self.started {
|
||||||
|
self.finish_layout(false)?;
|
||||||
|
}
|
||||||
|
Ok(std::mem::replace(&mut self.layouts, MultiLayout::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finish the current layout and start a new one in an extra space
|
||||||
|
/// (if there is an extra space).
|
||||||
|
///
|
||||||
|
/// If `start_new_empty` is true, a new empty layout will be started. Otherwise,
|
||||||
|
/// the new layout only emerges when new content is added.
|
||||||
|
pub fn finish_layout(&mut self, start_new_empty: bool) -> LayoutResult<()> {
|
||||||
|
let actions = std::mem::replace(&mut self.actions, LayoutActionList::new());
|
||||||
|
self.layouts.add(Layout {
|
||||||
|
dimensions: if self.space.shrink_to_fit {
|
||||||
|
self.dimensions.padded(self.space.padding)
|
||||||
} else {
|
} else {
|
||||||
self.ctx.space.dimensions
|
self.space.dimensions
|
||||||
},
|
},
|
||||||
actions: self.actions.into_vec(),
|
actions: actions.into_vec(),
|
||||||
debug_render: true,
|
debug_render: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.started = false;
|
||||||
|
|
||||||
|
if start_new_empty {
|
||||||
|
self.start_new_space()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_new_space(&mut self) -> LayoutResult<()> {
|
||||||
|
if let Some(space) = self.ctx.extra_space {
|
||||||
|
self.started = true;
|
||||||
|
self.space = space;
|
||||||
|
self.usable = space.usable();
|
||||||
|
self.dimensions = start_dimensions(space);
|
||||||
|
self.cursor = start_cursor(space);
|
||||||
|
self.in_extra_space = true;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(LayoutError::NotEnoughSpace("no extra space to start"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,10 +174,30 @@ impl StackLayouter {
|
|||||||
|
|
||||||
/// Whether this layouter contains any items.
|
/// Whether this layouter contains any items.
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.actions.is_empty()
|
self.layouts.is_empty() && self.actions.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn overflows(&self, dimensions: Size2D) -> bool {
|
fn overflows(&self, dimensions: Size2D) -> bool {
|
||||||
!self.usable.fits(dimensions)
|
!self.usable.fits(dimensions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_dimensions(space: LayoutSpace) -> Size2D {
|
||||||
|
match space.alignment {
|
||||||
|
Alignment::Left => Size2D::zero(),
|
||||||
|
Alignment::Right => Size2D::with_x(space.usable().x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_cursor(space: LayoutSpace) -> Size2D {
|
||||||
|
Size2D {
|
||||||
|
// If left-align, the cursor points to the top-left corner of
|
||||||
|
// each box. If we right-align, it points to the top-right
|
||||||
|
// corner.
|
||||||
|
x: match space.alignment {
|
||||||
|
Alignment::Left => space.padding.left,
|
||||||
|
Alignment::Right => space.dimensions.x - space.padding.right,
|
||||||
|
},
|
||||||
|
y: space.padding.top,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,14 +19,13 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
|
|||||||
fn new(ctx: LayoutContext<'a, 'p>) -> TreeLayouter<'a, 'p> {
|
fn new(ctx: LayoutContext<'a, 'p>) -> TreeLayouter<'a, 'p> {
|
||||||
TreeLayouter {
|
TreeLayouter {
|
||||||
ctx,
|
ctx,
|
||||||
stack: StackLayouter::new(StackContext { space: ctx.space }),
|
stack: StackLayouter::new(StackContext {
|
||||||
|
space: ctx.space,
|
||||||
|
extra_space: ctx.extra_space
|
||||||
|
}),
|
||||||
flex: FlexLayouter::new(FlexContext {
|
flex: FlexLayouter::new(FlexContext {
|
||||||
space: LayoutSpace {
|
space: flex_space(ctx.space),
|
||||||
dimensions: ctx.space.usable(),
|
extra_space: ctx.extra_space.map(|s| flex_space(s)),
|
||||||
padding: SizeBox::zero(),
|
|
||||||
alignment: ctx.space.alignment,
|
|
||||||
shrink_to_fit: true,
|
|
||||||
},
|
|
||||||
flex_spacing: flex_spacing(&ctx.style),
|
flex_spacing: flex_spacing(&ctx.style),
|
||||||
}),
|
}),
|
||||||
style: Cow::Borrowed(ctx.style),
|
style: Cow::Borrowed(ctx.style),
|
||||||
@ -48,10 +47,8 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
|
|||||||
|
|
||||||
// Finish the current flex layouting process.
|
// Finish the current flex layouting process.
|
||||||
Node::Newline => {
|
Node::Newline => {
|
||||||
self.layout_flex()?;
|
|
||||||
|
|
||||||
let space = paragraph_spacing(&self.style);
|
let space = paragraph_spacing(&self.style);
|
||||||
self.stack.add_space(space)?;
|
self.layout_flex(space)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle the text styles.
|
// Toggle the text styles.
|
||||||
@ -70,12 +67,10 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
|
|||||||
fn finish(mut self) -> LayoutResult<MultiLayout> {
|
fn finish(mut self) -> LayoutResult<MultiLayout> {
|
||||||
// If there are remainings, add them to the layout.
|
// If there are remainings, add them to the layout.
|
||||||
if !self.flex.is_empty() {
|
if !self.flex.is_empty() {
|
||||||
self.layout_flex()?;
|
self.layout_flex(Size::zero())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(MultiLayout {
|
self.stack.finish()
|
||||||
layouts: vec![self.stack.finish()],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add text to the flex layout. If `glue` is true, the text will be a glue
|
/// Add text to the flex layout. If `glue` is true, the text will be a glue
|
||||||
@ -98,29 +93,38 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Finish the current flex layout and add it the stack.
|
/// Finish the current flex layout and add it the stack.
|
||||||
fn layout_flex(&mut self) -> LayoutResult<()> {
|
fn layout_flex(&mut self, after_space: Size) -> LayoutResult<()> {
|
||||||
if self.flex.is_empty() {
|
if self.flex.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let layouts = self.flex.finish()?;
|
||||||
|
self.stack.add_many(layouts)?;
|
||||||
|
self.stack.add_space(after_space)?;
|
||||||
|
|
||||||
let mut ctx = self.flex.ctx();
|
let mut ctx = self.flex.ctx();
|
||||||
ctx.space.dimensions = self.stack.remaining();
|
ctx.space.dimensions = self.stack.remaining();
|
||||||
ctx.flex_spacing = flex_spacing(&self.style);
|
ctx.flex_spacing = flex_spacing(&self.style);
|
||||||
|
|
||||||
let next = FlexLayouter::new(ctx);
|
self.flex = FlexLayouter::new(ctx);
|
||||||
let flex = std::mem::replace(&mut self.flex, next);
|
|
||||||
let boxed = flex.finish()?;
|
|
||||||
|
|
||||||
self.stack.add(boxed)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layout a function.
|
/// Layout a function.
|
||||||
fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> {
|
fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> {
|
||||||
let mut ctx = self.ctx;
|
let mut ctx = self.ctx;
|
||||||
ctx.style = &self.style;
|
ctx.style = &self.style;
|
||||||
|
|
||||||
ctx.space.dimensions = self.stack.remaining();
|
ctx.space.dimensions = self.stack.remaining();
|
||||||
ctx.space.padding = SizeBox::zero();
|
ctx.space.padding = SizeBox::zero();
|
||||||
ctx.space.shrink_to_fit = true;
|
ctx.space.shrink_to_fit = false;
|
||||||
|
|
||||||
|
if let Some(space) = ctx.extra_space.as_mut() {
|
||||||
|
space.dimensions = space.dimensions.unpadded(space.padding);
|
||||||
|
space.padding = SizeBox::zero();
|
||||||
|
space.shrink_to_fit = false;
|
||||||
|
}
|
||||||
|
|
||||||
let commands = func.body.layout(ctx)?;
|
let commands = func.body.layout(ctx)?;
|
||||||
|
|
||||||
@ -137,6 +141,15 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flex_space(space: LayoutSpace) -> LayoutSpace {
|
||||||
|
LayoutSpace {
|
||||||
|
dimensions: space.usable(),
|
||||||
|
padding: SizeBox::zero(),
|
||||||
|
alignment: space.alignment,
|
||||||
|
shrink_to_fit: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn flex_spacing(style: &TextStyle) -> Size {
|
fn flex_spacing(style: &TextStyle) -> Size {
|
||||||
(style.line_spacing - 1.0) * Size::pt(style.font_size)
|
(style.line_spacing - 1.0) * Size::pt(style.font_size)
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ macro_rules! error_type {
|
|||||||
|
|
||||||
impl std::fmt::Display for $err {
|
impl std::fmt::Display for $err {
|
||||||
fn fmt(&self, $f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, $f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
#[allow(unused)]
|
||||||
let $var = self;
|
let $var = self;
|
||||||
$show
|
$show
|
||||||
}
|
}
|
||||||
@ -22,6 +23,7 @@ macro_rules! error_type {
|
|||||||
impl std::error::Error for $err {
|
impl std::error::Error for $err {
|
||||||
// The source method is only generated if an implementation was given.
|
// The source method is only generated if an implementation was given.
|
||||||
$(fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
$(fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
#[allow(unused)]
|
||||||
let $var = self;
|
let $var = self;
|
||||||
$source
|
$source
|
||||||
})*
|
})*
|
||||||
|
14
src/size.rs
14
src/size.rs
@ -131,6 +131,15 @@ impl Size2D {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a [`Size2D`] reduced by the paddings of the given box.
|
||||||
|
#[inline]
|
||||||
|
pub fn unpadded(&self, padding: SizeBox) -> Size2D {
|
||||||
|
Size2D {
|
||||||
|
x: self.x - padding.left - padding.right,
|
||||||
|
y: self.y - padding.top - padding.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the given [`Size2D`] fits into this one, that is,
|
/// Whether the given [`Size2D`] fits into this one, that is,
|
||||||
/// both coordinate values are smaller.
|
/// both coordinate values are smaller.
|
||||||
#[inline]
|
#[inline]
|
||||||
@ -189,6 +198,11 @@ debug_display!(Size);
|
|||||||
/// An error which can be returned when parsing a size.
|
/// An error which can be returned when parsing a size.
|
||||||
pub struct ParseSizeError;
|
pub struct ParseSizeError;
|
||||||
|
|
||||||
|
error_type! {
|
||||||
|
err: ParseSizeError,
|
||||||
|
show: f => write!(f, "failed to parse size"),
|
||||||
|
}
|
||||||
|
|
||||||
impl FromStr for Size {
|
impl FromStr for Size {
|
||||||
type Err = ParseSizeError;
|
type Err = ParseSizeError;
|
||||||
|
|
||||||
|
@ -3,12 +3,16 @@ use std::io::{BufWriter, Read, Write};
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use regex::{Regex, Captures};
|
||||||
|
|
||||||
use typst::export::pdf::PdfExporter;
|
use typst::export::pdf::PdfExporter;
|
||||||
use typst::layout::LayoutAction;
|
use typst::layout::LayoutAction;
|
||||||
use typst::toddle::query::FileSystemFontProvider;
|
use typst::toddle::query::FileSystemFontProvider;
|
||||||
|
use typst::size::{Size, Size2D, SizeBox};
|
||||||
|
use typst::style::PageStyle;
|
||||||
use typst::Typesetter;
|
use typst::Typesetter;
|
||||||
|
|
||||||
const CACHE_DIR: &str = "test-cache";
|
const CACHE_DIR: &str = "tests/cache";
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut perfect_match = false;
|
let mut perfect_match = false;
|
||||||
@ -31,6 +35,10 @@ fn main() {
|
|||||||
for entry in fs::read_dir("tests/layouts/").unwrap() {
|
for entry in fs::read_dir("tests/layouts/").unwrap() {
|
||||||
let path = entry.unwrap().path();
|
let path = entry.unwrap().path();
|
||||||
|
|
||||||
|
if path.extension() != Some(std::ffi::OsStr::new("typ")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let name = path.file_stem().unwrap().to_str().unwrap();
|
let name = path.file_stem().unwrap().to_str().unwrap();
|
||||||
|
|
||||||
let matches = if perfect_match {
|
let matches = if perfect_match {
|
||||||
@ -51,31 +59,41 @@ fn main() {
|
|||||||
|
|
||||||
/// Create a _PDF_ with a name from the source code.
|
/// Create a _PDF_ with a name from the source code.
|
||||||
fn test(name: &str, src: &str) {
|
fn test(name: &str, src: &str) {
|
||||||
print!("Testing: {}", name);
|
println!("Testing: {}", name);
|
||||||
|
|
||||||
|
let (src, size) = preprocess(src);
|
||||||
|
|
||||||
let mut typesetter = Typesetter::new();
|
let mut typesetter = Typesetter::new();
|
||||||
let provider = FileSystemFontProvider::from_listing("fonts/fonts.toml").unwrap();
|
let provider = FileSystemFontProvider::from_listing("fonts/fonts.toml").unwrap();
|
||||||
typesetter.add_font_provider(provider.clone());
|
typesetter.add_font_provider(provider.clone());
|
||||||
|
|
||||||
|
if let Some(dimensions) = size {
|
||||||
|
typesetter.set_page_style(PageStyle {
|
||||||
|
dimensions,
|
||||||
|
margins: SizeBox::zero()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
// Layout into box layout.
|
// Layout into box layout.
|
||||||
let tree = typesetter.parse(src).unwrap();
|
let tree = typesetter.parse(&src).unwrap();
|
||||||
let layout = typesetter.layout(&tree).unwrap();
|
let layouts = typesetter.layout(&tree).unwrap();
|
||||||
|
|
||||||
let end = Instant::now();
|
let end = Instant::now();
|
||||||
let duration = end - start;
|
let duration = end - start;
|
||||||
println!(" [{:?}]", duration);
|
println!(" => {:?}", duration);
|
||||||
|
println!();
|
||||||
|
|
||||||
// Write the serialed layout file.
|
// Write the serialed layout file.
|
||||||
let path = format!("{}/serialized/{}.box", CACHE_DIR, name);
|
let path = format!("{}/serialized/{}.lay", CACHE_DIR, name);
|
||||||
let mut file = File::create(path).unwrap();
|
let mut file = File::create(path).unwrap();
|
||||||
|
|
||||||
// Find all used fonts and their filenames.
|
// Find all used fonts and their filenames.
|
||||||
let mut map = Vec::new();
|
let mut map = Vec::new();
|
||||||
let mut loader = typesetter.loader().borrow_mut();
|
let mut loader = typesetter.loader().borrow_mut();
|
||||||
let single = &layout.layouts[0];
|
for layout in &layouts {
|
||||||
for action in &single.actions {
|
for action in &layout.actions {
|
||||||
if let LayoutAction::SetFont(index, _) = action {
|
if let LayoutAction::SetFont(index, _) = action {
|
||||||
if map.iter().find(|(i, _)| i == index).is_none() {
|
if map.iter().find(|(i, _)| i == index).is_none() {
|
||||||
let (_, provider_index) = loader.get_provider_and_index(*index);
|
let (_, provider_index) = loader.get_provider_and_index(*index);
|
||||||
@ -84,6 +102,7 @@ fn test(name: &str, src: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
drop(loader);
|
drop(loader);
|
||||||
|
|
||||||
// Write the font mapping into the serialization file.
|
// Write the font mapping into the serialization file.
|
||||||
@ -91,7 +110,8 @@ fn test(name: &str, src: &str) {
|
|||||||
for (index, path) in map {
|
for (index, path) in map {
|
||||||
writeln!(file, "{} {}", index, path).unwrap();
|
writeln!(file, "{} {}", index, path).unwrap();
|
||||||
}
|
}
|
||||||
single.serialize(&mut file).unwrap();
|
|
||||||
|
layouts.serialize(&mut file).unwrap();
|
||||||
|
|
||||||
// Render the layout into a PNG.
|
// Render the layout into a PNG.
|
||||||
Command::new("python")
|
Command::new("python")
|
||||||
@ -104,5 +124,69 @@ fn test(name: &str, src: &str) {
|
|||||||
let path = format!("{}/pdf/{}.pdf", CACHE_DIR, name);
|
let path = format!("{}/pdf/{}.pdf", CACHE_DIR, name);
|
||||||
let file = BufWriter::new(File::create(path).unwrap());
|
let file = BufWriter::new(File::create(path).unwrap());
|
||||||
let exporter = PdfExporter::new();
|
let exporter = PdfExporter::new();
|
||||||
exporter.export(&layout, typesetter.loader(), file).unwrap();
|
exporter.export(&layouts, typesetter.loader(), file).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preprocess<'a>(src: &'a str) -> (String, Option<Size2D>) {
|
||||||
|
let include_regex = Regex::new(r"\{include:((.|\.|\-)*)\}").unwrap();
|
||||||
|
let lorem_regex = Regex::new(r"\{lorem:(\d*)\}").unwrap();
|
||||||
|
let size_regex = Regex::new(r"\{(size:(([\d\w]*)\*([\d\w]*)))\}").unwrap();
|
||||||
|
|
||||||
|
let mut size = None;
|
||||||
|
|
||||||
|
let mut preprocessed = size_regex.replace_all(&src, |cap: &Captures| {
|
||||||
|
let width_str = cap.get(3).unwrap().as_str();
|
||||||
|
let height_str = cap.get(4).unwrap().as_str();
|
||||||
|
|
||||||
|
let width = width_str.parse::<Size>().unwrap();
|
||||||
|
let height = height_str.parse::<Size>().unwrap();
|
||||||
|
|
||||||
|
size = Some(Size2D::new(width, height));
|
||||||
|
|
||||||
|
"".to_string()
|
||||||
|
}).to_string();
|
||||||
|
|
||||||
|
let mut changed = true;
|
||||||
|
while changed {
|
||||||
|
changed = false;
|
||||||
|
preprocessed = include_regex.replace_all(&preprocessed, |cap: &Captures| {
|
||||||
|
changed = true;
|
||||||
|
let filename = cap.get(1).unwrap().as_str();
|
||||||
|
|
||||||
|
let path = format!("tests/layouts/{}", filename);
|
||||||
|
let mut file = File::open(path).unwrap();
|
||||||
|
let mut buf = String::new();
|
||||||
|
file.read_to_string(&mut buf).unwrap();
|
||||||
|
buf
|
||||||
|
}).to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
preprocessed= lorem_regex.replace_all(&preprocessed, |cap: &Captures| {
|
||||||
|
let num_str = cap.get(1).unwrap().as_str();
|
||||||
|
let num_words = num_str.parse::<usize>().unwrap();
|
||||||
|
|
||||||
|
generate_lorem(num_words)
|
||||||
|
}).to_string();
|
||||||
|
|
||||||
|
(preprocessed, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_lorem(num_words: usize) -> String {
|
||||||
|
const LOREM: [&str; 69] = [
|
||||||
|
"Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit.", "Etiam",
|
||||||
|
"suscipit", "porta", "pretium.", "Donec", "eu", "lorem", "hendrerit,", "scelerisque",
|
||||||
|
"lectus", "at,", "consequat", "ligula.", "Nulla", "elementum", "massa", "et", "viverra",
|
||||||
|
"consectetur.", "Donec", "blandit", "metus", "ut", "ipsum", "commodo", "congue.", "Nullam",
|
||||||
|
"auctor,", "mi", "vel", "tristique", "venenatis,", "nisl", "nunc", "tristique", "diam,",
|
||||||
|
"aliquam", "pellentesque", "lorem", "massa", "vel", "neque.", "Sed", "malesuada", "ante",
|
||||||
|
"nisi,", "sit", "amet", "auctor", "risus", "fermentum", "in.", "Sed", "blandit", "mollis",
|
||||||
|
"mi,", "non", "tristique", "nisi", "fringilla", "at."
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
for i in 0 .. num_words {
|
||||||
|
buf.push_str(LOREM[i % LOREM.len()]);
|
||||||
|
buf.push(' ');
|
||||||
|
}
|
||||||
|
buf
|
||||||
}
|
}
|
||||||
|
2
tests/layouts/pagebreaks.typ
Normal file
2
tests/layouts/pagebreaks.typ
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
{size:200pt*200pt}
|
||||||
|
{lorem:400}
|
@ -1,88 +0,0 @@
|
|||||||
[align: right][
|
|
||||||
[bold][Scene 5: _The Tower of London_]
|
|
||||||
|
|
||||||
[italic][Enter Mortimer, brought in a chair, and Gaolers.]
|
|
||||||
|
|
||||||
*Mortimer.* Kind keepers of my weak decaying age,
|
|
||||||
Let dying Mortimer here rest himself.
|
|
||||||
Even like a man new haled from the rack,
|
|
||||||
So fare my limbs with long imprisonment;
|
|
||||||
And these grey locks, the pursuivants of death,
|
|
||||||
Nestor-like aged in an age of care,
|
|
||||||
Argue the end of Edmund Mortimer.
|
|
||||||
These eyes, like lamps whose wasting oil is spent,
|
|
||||||
Wax dim, as drawing to their exigent;
|
|
||||||
Weak shoulders, overborne with burdening grief,
|
|
||||||
And pithless arms, like to a withered vine
|
|
||||||
That droops his sapless branches to the ground.
|
|
||||||
Yet are these feet, whose strengthless stay is numb,
|
|
||||||
Unable to support this lump of clay,
|
|
||||||
Swift-winged with desire to get a grave,
|
|
||||||
As witting I no other comfort have.
|
|
||||||
But tell me, keeper, will my nephew come?
|
|
||||||
|
|
||||||
*First Keeper.* Richard Plantagenet, my lord, will come.
|
|
||||||
We sent unto the Temple, unto his chamber;
|
|
||||||
And answer was return'd that he will come.
|
|
||||||
|
|
||||||
*Mortimer.* Enough; my soul shall then be satisfied.
|
|
||||||
Poor gentleman! his wrong doth equal mine.
|
|
||||||
Since Henry Monmouth first began to reign,
|
|
||||||
Before whose glory I was great in arms,
|
|
||||||
This loathsome sequestration have I had;
|
|
||||||
And even since then hath Richard been obscur'd,
|
|
||||||
Depriv'd of honour and inheritance.
|
|
||||||
But now the arbitrator of despairs,
|
|
||||||
Just Death, kind umpire of men's miseries,
|
|
||||||
With sweet enlargement doth dismiss me hence.
|
|
||||||
I would his troubles likewise were expir'd,
|
|
||||||
That so he might recover what was lost.
|
|
||||||
|
|
||||||
|
|
||||||
[italic][Enter Richard Plantagenet]
|
|
||||||
|
|
||||||
*First Keeper.* My lord, your loving nephew now is come.
|
|
||||||
|
|
||||||
*Mortimer.* Richard Plantagenet, my friend, is he come?
|
|
||||||
|
|
||||||
*Plantagenet.* Ay, noble uncle, thus ignobly us'd,
|
|
||||||
Your nephew, late despised Richard, comes.
|
|
||||||
|
|
||||||
*Mortimer.* Direct mine arms I may embrace his neck
|
|
||||||
And in his bosom spend my latter gasp.
|
|
||||||
O, tell me when my lips do touch his cheeks,
|
|
||||||
That I may kindly give one fainting kiss.
|
|
||||||
And now declare, sweet stem from York's great stock,
|
|
||||||
Why didst thou say of late thou wert despis'd?
|
|
||||||
|
|
||||||
*Plantagenet.* First, lean thine aged back against mine arm;
|
|
||||||
And, in that ease, I'll tell thee my disease.
|
|
||||||
This day, in argument upon a case,
|
|
||||||
Some words there grew 'twixt Somerset and me;
|
|
||||||
Among which terms he us'd his lavish tongue
|
|
||||||
And did upbraid me with my father's death;
|
|
||||||
Which obloquy set bars before my tongue,
|
|
||||||
Else with the like I had requited him.
|
|
||||||
Therefore, good uncle, for my father's sake,
|
|
||||||
In honour of a true Plantagenet,
|
|
||||||
And for alliance sake, declare the cause
|
|
||||||
My father, Earl of Cambridge, lost his head.
|
|
||||||
|
|
||||||
*Mortimer.* That cause, fair nephew, that imprison'd me
|
|
||||||
And hath detain'd me all my flow'ring youth
|
|
||||||
Within a loathsome dungeon, there to pine,
|
|
||||||
Was cursed instrument of his decease.
|
|
||||||
|
|
||||||
*Plantagenet.* Discover more at large what cause that was,
|
|
||||||
For I am ignorant and cannot guess.
|
|
||||||
|
|
||||||
*Mortimer.* I will, if that my fading breath permit
|
|
||||||
And death approach not ere my tale be done.
|
|
||||||
Henry the Fourth, grandfather to this king,
|
|
||||||
Depos'd his nephew Richard, Edward's son,
|
|
||||||
The first-begotten and the lawful heir
|
|
||||||
Of Edward king, the third of that descent;
|
|
||||||
During whose reign the Percies of the north,
|
|
||||||
Finding his usurpation most unjust,
|
|
||||||
Endeavour'd my advancement to the throne ...
|
|
||||||
]
|
|
8
tests/layouts/shakespeare.typ
Normal file
8
tests/layouts/shakespeare.typ
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Basic unboxed
|
||||||
|
{include:shakespeare.tpl}
|
||||||
|
|
||||||
|
// Boxed, but still left-aligned
|
||||||
|
[align: left][{include:shakespeare.tpl}]
|
||||||
|
|
||||||
|
// Boxed, and right-aligned
|
||||||
|
[align: right][{include:shakespeare.tpl}]
|
@ -1,8 +1,5 @@
|
|||||||
_Multiline:_
|
_Multiline:_
|
||||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
|
{lorem:45}
|
||||||
eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
|
|
||||||
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
|
|
||||||
clita kasd gubergren, no sea takimata sanctus est.
|
|
||||||
|
|
||||||
_Emoji:_ Hello World! 🌍
|
_Emoji:_ Hello World! 🌍
|
||||||
|
|
@ -5,36 +5,73 @@ from PIL import Image, ImageDraw, ImageFont
|
|||||||
|
|
||||||
|
|
||||||
BASE = os.path.dirname(__file__)
|
BASE = os.path.dirname(__file__)
|
||||||
CACHE_DIR = os.path.join(BASE, "../test-cache/");
|
CACHE_DIR = os.path.join(BASE, "cache/");
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
assert len(sys.argv) == 2, "usage: python render.py <name>"
|
assert len(sys.argv) == 2, "usage: python render.py <name>"
|
||||||
name = sys.argv[1]
|
name = sys.argv[1]
|
||||||
|
|
||||||
filename = os.path.join(CACHE_DIR, f"serialized/{name}.box")
|
filename = os.path.join(CACHE_DIR, f"serialized/{name}.lay")
|
||||||
with open(filename, encoding="utf-8") as file:
|
with open(filename, encoding="utf-8") as file:
|
||||||
lines = [line[:-1] for line in file.readlines()]
|
lines = [line[:-1] for line in file.readlines()]
|
||||||
|
|
||||||
fonts = {}
|
renderer = MultiboxRenderer(lines)
|
||||||
font_count = int(lines[0])
|
renderer.render()
|
||||||
for i in range(font_count):
|
image = renderer.export()
|
||||||
parts = lines[1 + i].split(' ', 1)
|
|
||||||
index = int(parts[0])
|
|
||||||
path = parts[1]
|
|
||||||
fonts[index] = os.path.join(BASE, "../fonts", path)
|
|
||||||
|
|
||||||
width, height = (float(s) for s in lines[font_count + 1].split())
|
|
||||||
|
|
||||||
renderer = Renderer(fonts, width, height)
|
|
||||||
for command in lines[font_count + 2:]:
|
|
||||||
renderer.execute(command)
|
|
||||||
|
|
||||||
pathlib.Path(os.path.join(CACHE_DIR, "rendered")).mkdir(parents=True, exist_ok=True)
|
pathlib.Path(os.path.join(CACHE_DIR, "rendered")).mkdir(parents=True, exist_ok=True)
|
||||||
renderer.export(name)
|
image.save(CACHE_DIR + "rendered/" + name + ".png")
|
||||||
|
|
||||||
|
|
||||||
class Renderer:
|
class MultiboxRenderer:
|
||||||
|
def __init__(self, lines):
|
||||||
|
self.combined = None
|
||||||
|
|
||||||
|
self.fonts = {}
|
||||||
|
font_count = int(lines[0])
|
||||||
|
for i in range(font_count):
|
||||||
|
parts = lines[i + 1].split(' ', 1)
|
||||||
|
index = int(parts[0])
|
||||||
|
path = parts[1]
|
||||||
|
self.fonts[index] = os.path.join(BASE, "../fonts", path)
|
||||||
|
|
||||||
|
self.content = lines[font_count + 1:]
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
images = []
|
||||||
|
|
||||||
|
layout_count = int(self.content[0])
|
||||||
|
start = 1
|
||||||
|
|
||||||
|
for _ in range(layout_count):
|
||||||
|
width, height = (float(s) for s in self.content[start].split())
|
||||||
|
action_count = int(self.content[start + 1])
|
||||||
|
start += 2
|
||||||
|
|
||||||
|
renderer = BoxRenderer(self.fonts, width, height)
|
||||||
|
for i in range(action_count):
|
||||||
|
command = self.content[start + i]
|
||||||
|
renderer.execute(command)
|
||||||
|
|
||||||
|
images.append(renderer.export())
|
||||||
|
start += action_count
|
||||||
|
|
||||||
|
width = max(image.width for image in images) + 20
|
||||||
|
height = sum(image.height for image in images) + 10 * (len(images) + 1)
|
||||||
|
|
||||||
|
self.combined = Image.new('RGBA', (width, height))
|
||||||
|
|
||||||
|
cursor = 10
|
||||||
|
for image in images:
|
||||||
|
self.combined.paste(image, (10, cursor))
|
||||||
|
cursor += 10 + image.height
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
return self.combined
|
||||||
|
|
||||||
|
|
||||||
|
class BoxRenderer:
|
||||||
def __init__(self, fonts, width, height):
|
def __init__(self, fonts, width, height):
|
||||||
self.fonts = fonts
|
self.fonts = fonts
|
||||||
self.size = (pix(width), pix(height))
|
self.size = (pix(width), pix(height))
|
||||||
@ -102,8 +139,8 @@ class Renderer:
|
|||||||
else:
|
else:
|
||||||
raise Exception("invalid command")
|
raise Exception("invalid command")
|
||||||
|
|
||||||
def export(self, name):
|
def export(self):
|
||||||
self.img.save(CACHE_DIR + "rendered/" + name + ".png")
|
return self.img
|
||||||
|
|
||||||
|
|
||||||
def pix(points):
|
def pix(points):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user