Implement space extension (multipage)

This commit is contained in:
Laurenz 2019-10-16 21:31:14 +02:00
parent a3c667895e
commit f2f05e07b0
16 changed files with 451 additions and 304 deletions

2
.gitignore vendored
View File

@ -2,4 +2,4 @@
**/*.rs.bk
Cargo.lock
things
test-cache
tests/cache

View File

@ -11,6 +11,9 @@ byteorder = "1"
smallvec = "0.6.10"
unicode-xid = "0.1.0"
[dev-dependencies]
regex = "1"
[[bin]]
name = "typstc"
path = "src/bin/main.rs"

View File

@ -80,7 +80,7 @@ impl<'d, W: Write> ExportProcess<'d, W> {
) -> PdfResult<ExportProcess<'d, W>>
{
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 {
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.
/// 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 page_tree = catalog + 1;
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).
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(
0.0,
0.0,
@ -212,10 +216,10 @@ impl<'d, W: Write> ExportProcess<'d, W> {
);
self.writer.write_obj(
id,
page_id,
Page::new(self.offsets.page_tree)
.media_box(rect)
.contents(ids(self.offsets.contents)),
.content(content_id),
)?;
}

View File

@ -21,22 +21,19 @@ pub struct FlexLayouter {
ctx: FlexContext,
units: Vec<FlexUnit>,
actions: LayoutActionList,
usable: Size2D,
dimensions: Size2D,
cursor: Size2D,
stack: StackLayouter,
usable_width: Size,
run: FlexRun,
next_glue: Option<Layout>,
cached_glue: Option<Layout>,
}
/// The context for flex layouting.
#[derive(Debug, Copy, Clone)]
pub struct FlexContext {
/// The space to layout the boxes in.
pub space: LayoutSpace,
/// The spacing between two lines of boxes.
pub flex_spacing: Size,
pub extra_space: Option<LayoutSpace>,
}
enum FlexUnit {
@ -49,7 +46,7 @@ enum FlexUnit {
}
struct FlexRun {
content: Vec<(Size2D, Layout)>,
content: Vec<(Size, Layout)>,
size: Size2D,
}
@ -60,17 +57,17 @@ impl FlexLayouter {
ctx,
units: vec![],
actions: LayoutActionList::new(),
usable: ctx.space.usable(),
dimensions: match ctx.space.alignment {
Alignment::Left => Size2D::zero(),
Alignment::Right => Size2D::with_x(ctx.space.usable().x),
stack: StackLayouter::new(StackContext {
space: ctx.space,
extra_space: ctx.extra_space,
}),
usable_width: ctx.space.usable().x,
run: FlexRun {
content: vec![],
size: Size2D::zero()
},
cursor: Size2D::new(ctx.space.padding.left, ctx.space.padding.top),
run: FlexRun::new(),
next_glue: None,
cached_glue: None,
}
}
@ -90,12 +87,14 @@ impl FlexLayouter {
}
/// 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
// ownership problems.
let units = self.units;
self.units = Vec::new();
let units = std::mem::replace(&mut self.units, vec![]);
for unit in units {
match unit {
FlexUnit::Boxed(boxed) => self.layout_box(boxed)?,
@ -104,17 +103,88 @@ impl FlexLayouter {
}
// Finish the last flex run.
self.finish_flex_run();
self.finish_run()?;
Ok(Layout {
dimensions: if self.ctx.space.shrink_to_fit {
self.dimensions.padded(self.ctx.space.padding)
} else {
self.ctx.space.dimensions
},
actions: self.actions.into_vec(),
debug_render: true,
})
self.stack.finish()
}
/// 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 {
return Err(LayoutError::NotEnoughSpace("cannot fit box into flex run"));
}
}
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.
@ -122,91 +192,7 @@ impl FlexLayouter {
self.units.is_empty()
}
fn layout_box(&mut self, boxed: Layout) -> LayoutResult<()> {
let next_glue_width = self
.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()
}
fn overflows_line(&self, line: Size) -> bool {
line > self.usable_width
}
}

View File

@ -48,6 +48,7 @@ impl Layout {
self.dimensions.x.to_pt(),
self.dimensions.y.to_pt()
)?;
writeln!(f, "{}", self.actions.len())?;
for action in &self.actions {
action.serialize(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 {
type Item = Layout;
type IntoIter = std::vec::IntoIter<Layout>;
@ -112,7 +124,7 @@ impl<'a> IntoIterator for &'a MultiLayout {
}
/// The general context for layouting.
#[derive(Copy, Clone)]
#[derive(Debug, Copy, Clone)]
pub struct LayoutContext<'a, 'p> {
pub loader: &'a SharedFontLoader<'p>,
pub style: &'a TextStyle,
@ -154,7 +166,7 @@ pub enum Alignment {
/// The error type for layouting.
pub enum LayoutError {
/// There is not enough space to add an item.
NotEnoughSpace,
NotEnoughSpace(&'static str),
/// There was no suitable font for the given character.
NoSuitableFont(char),
/// An error occured while gathering font data.
@ -167,7 +179,7 @@ pub type LayoutResult<T> = Result<T, LayoutError>;
error_type! {
err: LayoutError,
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::Font(err) => write!(f, "font error: {}", err),
},

View File

@ -5,44 +5,38 @@ use super::*;
/// The boxes are arranged vertically, each layout gettings it's own "line".
pub struct StackLayouter {
ctx: StackContext,
layouts: MultiLayout,
actions: LayoutActionList,
space: LayoutSpace,
usable: Size2D,
dimensions: Size2D,
cursor: Size2D,
in_extra_space: bool,
started: bool,
}
/// The context for stack layouting.
#[derive(Debug, Copy, Clone)]
pub struct StackContext {
/// The space to layout the boxes in.
pub space: LayoutSpace,
pub extra_space: Option<LayoutSpace>,
}
impl StackLayouter {
/// Create a new stack layouter.
pub fn new(ctx: StackContext) -> StackLayouter {
let space = ctx.space;
StackLayouter {
ctx,
layouts: MultiLayout::new(),
actions: LayoutActionList::new(),
space: ctx.space,
usable: ctx.space.usable(),
dimensions: match ctx.space.alignment {
Alignment::Left => Size2D::zero(),
Alignment::Right => Size2D::with_x(space.usable().x),
},
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,
),
dimensions: start_dimensions(ctx.space),
cursor: start_cursor(ctx.space),
in_extra_space: false,
started: true,
}
}
@ -53,19 +47,30 @@ impl StackLayouter {
/// Add a sublayout to the bottom.
pub fn add(&mut self, layout: Layout) -> LayoutResult<()> {
if !self.started {
self.start_new_space()?;
}
let new_dimensions = Size2D {
x: crate::size::max(self.dimensions.x, layout.dimensions.x),
y: self.dimensions.y + layout.dimensions.y,
};
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
// 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.
let position = match self.ctx.space.alignment {
let position = match self.space.alignment {
Alignment::Left => self.cursor,
Alignment::Right => self.cursor - Size2D::with_x(layout.dimensions.x),
};
@ -88,26 +93,74 @@ impl StackLayouter {
/// Add vertical space after the last layout.
pub fn add_space(&mut self, space: Size) -> LayoutResult<()> {
if self.overflows(self.dimensions + Size2D::with_y(space)) {
return Err(LayoutError::NotEnoughSpace);
if !self.started {
self.start_new_space()?;
}
self.cursor.y += space;
self.dimensions.y += 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.dimensions.y += space;
}
Ok(())
}
/// Finish the layouting.
pub fn finish(self) -> Layout {
Layout {
dimensions: if self.ctx.space.shrink_to_fit {
self.dimensions.padded(self.ctx.space.padding)
///
/// The layouter is not consumed by this to prevent ownership problems.
/// It should not be used further.
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 {
self.ctx.space.dimensions
self.space.dimensions
},
actions: self.actions.into_vec(),
actions: actions.into_vec(),
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.
pub fn is_empty(&self) -> bool {
self.actions.is_empty()
self.layouts.is_empty() && self.actions.is_empty()
}
fn overflows(&self, dimensions: Size2D) -> bool {
!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,
}
}

View File

@ -19,14 +19,13 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
fn new(ctx: LayoutContext<'a, 'p>) -> TreeLayouter<'a, 'p> {
TreeLayouter {
ctx,
stack: StackLayouter::new(StackContext { space: ctx.space }),
stack: StackLayouter::new(StackContext {
space: ctx.space,
extra_space: ctx.extra_space
}),
flex: FlexLayouter::new(FlexContext {
space: LayoutSpace {
dimensions: ctx.space.usable(),
padding: SizeBox::zero(),
alignment: ctx.space.alignment,
shrink_to_fit: true,
},
space: flex_space(ctx.space),
extra_space: ctx.extra_space.map(|s| flex_space(s)),
flex_spacing: flex_spacing(&ctx.style),
}),
style: Cow::Borrowed(ctx.style),
@ -48,10 +47,8 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
// Finish the current flex layouting process.
Node::Newline => {
self.layout_flex()?;
let space = paragraph_spacing(&self.style);
self.stack.add_space(space)?;
self.layout_flex(space)?;
}
// Toggle the text styles.
@ -70,12 +67,10 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
fn finish(mut self) -> LayoutResult<MultiLayout> {
// If there are remainings, add them to the layout.
if !self.flex.is_empty() {
self.layout_flex()?;
self.layout_flex(Size::zero())?;
}
Ok(MultiLayout {
layouts: vec![self.stack.finish()],
})
self.stack.finish()
}
/// 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.
fn layout_flex(&mut self) -> LayoutResult<()> {
fn layout_flex(&mut self, after_space: Size) -> LayoutResult<()> {
if self.flex.is_empty() {
return Ok(());
}
let layouts = self.flex.finish()?;
self.stack.add_many(layouts)?;
self.stack.add_space(after_space)?;
let mut ctx = self.flex.ctx();
ctx.space.dimensions = self.stack.remaining();
ctx.flex_spacing = flex_spacing(&self.style);
let next = FlexLayouter::new(ctx);
let flex = std::mem::replace(&mut self.flex, next);
let boxed = flex.finish()?;
self.flex = FlexLayouter::new(ctx);
self.stack.add(boxed)
Ok(())
}
/// Layout a function.
fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> {
let mut ctx = self.ctx;
ctx.style = &self.style;
ctx.space.dimensions = self.stack.remaining();
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)?;
@ -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 {
(style.line_spacing - 1.0) * Size::pt(style.font_size)
}

View File

@ -12,6 +12,7 @@ macro_rules! error_type {
impl std::fmt::Display for $err {
fn fmt(&self, $f: &mut std::fmt::Formatter) -> std::fmt::Result {
#[allow(unused)]
let $var = self;
$show
}
@ -22,6 +23,7 @@ macro_rules! error_type {
impl std::error::Error for $err {
// The source method is only generated if an implementation was given.
$(fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
#[allow(unused)]
let $var = self;
$source
})*

View File

@ -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,
/// both coordinate values are smaller.
#[inline]
@ -189,6 +198,11 @@ debug_display!(Size);
/// An error which can be returned when parsing a size.
pub struct ParseSizeError;
error_type! {
err: ParseSizeError,
show: f => write!(f, "failed to parse size"),
}
impl FromStr for Size {
type Err = ParseSizeError;

View File

@ -3,12 +3,16 @@ use std::io::{BufWriter, Read, Write};
use std::process::Command;
use std::time::Instant;
use regex::{Regex, Captures};
use typst::export::pdf::PdfExporter;
use typst::layout::LayoutAction;
use typst::toddle::query::FileSystemFontProvider;
use typst::size::{Size, Size2D, SizeBox};
use typst::style::PageStyle;
use typst::Typesetter;
const CACHE_DIR: &str = "test-cache";
const CACHE_DIR: &str = "tests/cache";
fn main() {
let mut perfect_match = false;
@ -31,6 +35,10 @@ fn main() {
for entry in fs::read_dir("tests/layouts/").unwrap() {
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 matches = if perfect_match {
@ -51,36 +59,47 @@ fn main() {
/// Create a _PDF_ with a name from the source code.
fn test(name: &str, src: &str) {
print!("Testing: {}", name);
println!("Testing: {}", name);
let (src, size) = preprocess(src);
let mut typesetter = Typesetter::new();
let provider = FileSystemFontProvider::from_listing("fonts/fonts.toml").unwrap();
typesetter.add_font_provider(provider.clone());
if let Some(dimensions) = size {
typesetter.set_page_style(PageStyle {
dimensions,
margins: SizeBox::zero()
});
}
let start = Instant::now();
// Layout into box layout.
let tree = typesetter.parse(src).unwrap();
let layout = typesetter.layout(&tree).unwrap();
let tree = typesetter.parse(&src).unwrap();
let layouts = typesetter.layout(&tree).unwrap();
let end = Instant::now();
let duration = end - start;
println!(" [{:?}]", duration);
println!(" => {:?}", duration);
println!();
// 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();
// Find all used fonts and their filenames.
let mut map = Vec::new();
let mut loader = typesetter.loader().borrow_mut();
let single = &layout.layouts[0];
for action in &single.actions {
if let LayoutAction::SetFont(index, _) = action {
if map.iter().find(|(i, _)| i == index).is_none() {
let (_, provider_index) = loader.get_provider_and_index(*index);
let filename = provider.get_path(provider_index).to_str().unwrap();
map.push((*index, filename));
for layout in &layouts {
for action in &layout.actions {
if let LayoutAction::SetFont(index, _) = action {
if map.iter().find(|(i, _)| i == index).is_none() {
let (_, provider_index) = loader.get_provider_and_index(*index);
let filename = provider.get_path(provider_index).to_str().unwrap();
map.push((*index, filename));
}
}
}
}
@ -91,7 +110,8 @@ fn test(name: &str, src: &str) {
for (index, path) in map {
writeln!(file, "{} {}", index, path).unwrap();
}
single.serialize(&mut file).unwrap();
layouts.serialize(&mut file).unwrap();
// Render the layout into a PNG.
Command::new("python")
@ -104,5 +124,69 @@ fn test(name: &str, src: &str) {
let path = format!("{}/pdf/{}.pdf", CACHE_DIR, name);
let file = BufWriter::new(File::create(path).unwrap());
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
}

View File

@ -0,0 +1,2 @@
{size:200pt*200pt}
{lorem:400}

View File

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

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

View File

@ -1,8 +1,5 @@
_Multiline:_
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
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.
{lorem:45}
_Emoji:_ Hello World! 🌍

View File

@ -5,36 +5,73 @@ from PIL import Image, ImageDraw, ImageFont
BASE = os.path.dirname(__file__)
CACHE_DIR = os.path.join(BASE, "../test-cache/");
CACHE_DIR = os.path.join(BASE, "cache/");
def main():
assert len(sys.argv) == 2, "usage: python render.py <name>"
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:
lines = [line[:-1] for line in file.readlines()]
fonts = {}
font_count = int(lines[0])
for i in range(font_count):
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)
renderer = MultiboxRenderer(lines)
renderer.render()
image = renderer.export()
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):
self.fonts = fonts
self.size = (pix(width), pix(height))
@ -102,8 +139,8 @@ class Renderer:
else:
raise Exception("invalid command")
def export(self, name):
self.img.save(CACHE_DIR + "rendered/" + name + ".png")
def export(self):
return self.img
def pix(points):