Layout elements and pure rust rendering 🥏

This commit is contained in:
Laurenz 2020-08-02 21:17:42 +02:00
parent d5ff97f42e
commit cbbc46215f
18 changed files with 413 additions and 701 deletions

View File

@ -7,23 +7,28 @@ edition = "2018"
[workspace]
members = ["main"]
[profile.dev.package."*"]
opt-level = 2
[dependencies]
async-trait = "0.1"
fontdock = { path = "../fontdock", features = ["fs", "serialize"] }
serde = { version = "1", features = ["derive"], optional = true }
fontdock = { path = "../fontdock" }
tide = { path = "../tide" }
ttf-parser = "0.8.2"
unicode-xid = "0.2"
serde = { version = "1", features = ["derive"], optional = true }
[features]
serialize = []
fs = ["fontdock/fs"]
[dev-dependencies]
futures-executor = "0.3"
serde_json = "1"
raqote = { version = "0.7", default-features = false }
[[test]]
name = "typeset"
path = "tests/src/typeset.rs"
name = "test-typeset"
path = "tests/test_typeset.rs"
required-features = ["fs"]
harness = false
required-features = ["serialize"]

View File

@ -15,7 +15,8 @@ use fontdock::FaceId;
use ttf_parser::{name_id, GlyphId};
use crate::SharedFontLoader;
use crate::layout::{MultiLayout, Layout, LayoutAction};
use crate::layout::{MultiLayout, Layout};
use crate::layout::elements::LayoutElement;
use crate::length::Length;
/// Export a layouted list of boxes. The same font loader as used for
@ -144,38 +145,26 @@ impl<'a, W: Write> PdfExporter<'a, W> {
// Moves and face switches are always cached and only flushed once
// needed.
let mut text = Text::new();
let mut face_id = FaceId::MAX;
let mut font_size = 0.0;
let mut next_pos = None;
let mut face = FaceId::MAX;
let mut size = 0.0;
for action in &page.actions {
match action {
LayoutAction::MoveAbsolute(pos) => {
next_pos = Some(*pos);
},
&LayoutAction::SetFont(id, size) => {
face_id = id;
font_size = size;
text.tf(
self.to_pdf[&id] as u32 + 1,
Length::raw(font_size).as_pt() as f32
);
}
LayoutAction::WriteText(string) => {
if let Some(pos) = next_pos.take() {
let x = Length::raw(pos.x).as_pt();
let y = Length::raw(page.dimensions.y - pos.y - font_size).as_pt();
text.tm(1.0, 0.0, 0.0, 1.0, x as f32, y as f32);
for (pos, element) in &page.elements.0 {
match element {
LayoutElement::Text(shaped) => {
if shaped.face != face || shaped.size != size {
face = shaped.face;
size = shaped.size;
text.tf(
self.to_pdf[&shaped.face] as u32 + 1,
Length::raw(size).as_pt() as f32
);
}
let loader = self.loader.borrow();
let face = loader.get_loaded(face_id);
text.tj(face.encode_text(&string));
},
LayoutAction::DebugBox(_) => {}
let x = Length::raw(pos.x).as_pt();
let y = Length::raw(page.dimensions.y - pos.y - size).as_pt();
text.tm(1.0, 0.0, 0.0, 1.0, x as f32, y as f32);
text.tj(shaped.encode_glyphs());
}
}
}
@ -313,14 +302,13 @@ fn remap_fonts(layouts: &MultiLayout) -> (HashMap<FaceId, usize>, Vec<FaceId>) {
// We want to find out which fonts are used at all. To do that, look at each
// text element to find out which font is uses.
for layout in layouts {
for action in &layout.actions {
if let &LayoutAction::SetFont(id, _) = action {
to_pdf.entry(id).or_insert_with(|| {
let next_id = to_fontdock.len();
to_fontdock.push(id);
next_id
});
}
for (_, element) in &layout.elements.0 {
let LayoutElement::Text(shaped) = element;
to_pdf.entry(shaped.face).or_insert_with(|| {
let next_id = to_fontdock.len();
to_fontdock.push(shaped.face);
next_id
});
}
}

View File

@ -38,20 +38,6 @@ impl OwnedFace {
pub fn data(&self) -> &[u8] {
&self.data
}
/// Encode the text into glyph ids and encode these into a big-endian byte
/// buffer.
pub fn encode_text(&self, text: &str) -> Vec<u8> {
const BYTES_PER_GLYPH: usize = 2;
let mut bytes = Vec::with_capacity(BYTES_PER_GLYPH * text.len());
for c in text.chars() {
if let Some(glyph) = self.glyph_index(c) {
bytes.push((glyph.0 >> 8) as u8);
bytes.push((glyph.0 & 0xff) as u8);
}
}
bytes
}
}
impl ContainsChar for OwnedFace {

View File

@ -3,14 +3,10 @@
use std::fmt::{self, Debug, Formatter};
use std::ops::*;
#[cfg(feature = "serialize")]
use serde::Serialize;
use crate::layout::prelude::*;
/// A value in two dimensions.
#[derive(Default, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
pub struct Value2<T> {
/// The horizontal component.
pub x: T,
@ -180,7 +176,6 @@ impl Neg for Size {
/// A value in four dimensions.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
pub struct Value4<T> {
/// The left extent.
pub left: T,

View File

@ -1,166 +0,0 @@
//! Drawing and configuration actions composing layouts.
use std::fmt::{self, Debug, Formatter};
#[cfg(feature = "serialize")]
use serde::ser::{Serialize, Serializer, SerializeTuple};
use fontdock::FaceId;
use crate::geom::Size;
use super::Layout;
use self::LayoutAction::*;
/// A layouting action, which is the basic building block layouts are composed
/// of.
#[derive(Clone, PartialEq)]
pub enum LayoutAction {
/// Move to an absolute position.
MoveAbsolute(Size),
/// Set the font given the index from the font loader and font size.
SetFont(FaceId, f64),
/// Write text at the current position.
WriteText(String),
/// Visualize a box for debugging purposes.
DebugBox(Size),
}
#[cfg(feature = "serialize")]
impl Serialize for LayoutAction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
match self {
LayoutAction::MoveAbsolute(pos) => {
let mut tup = serializer.serialize_tuple(2)?;
tup.serialize_element(&0u8)?;
tup.serialize_element(&pos)?;
tup.end()
}
LayoutAction::SetFont(id, size) => {
let mut tup = serializer.serialize_tuple(4)?;
tup.serialize_element(&1u8)?;
tup.serialize_element(id)?;
tup.serialize_element(size)?;
tup.end()
}
LayoutAction::WriteText(text) => {
let mut tup = serializer.serialize_tuple(2)?;
tup.serialize_element(&2u8)?;
tup.serialize_element(text)?;
tup.end()
}
LayoutAction::DebugBox(size) => {
let mut tup = serializer.serialize_tuple(2)?;
tup.serialize_element(&3u8)?;
tup.serialize_element(&size)?;
tup.end()
}
}
}
}
impl Debug for LayoutAction {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use LayoutAction::*;
match self {
MoveAbsolute(s) => write!(f, "move {} {}", s.x, s.y),
SetFont(id, s) => write!(f, "font {}-{} {}", id.index, id.variant, s),
WriteText(s) => write!(f, "write {:?}", s),
DebugBox(s) => write!(f, "box {} {}", s.x, s.y),
}
}
}
/// A sequence of layouting actions.
///
/// The sequence of actions is optimized as the actions are added. For example,
/// a font changing option will only be added if the selected font is not
/// already active. All configuration actions (like moving, setting fonts, ...)
/// are only flushed when content is written.
///
/// Furthermore, the action list can translate absolute position into a
/// coordinate system with a different origin. This is realized in the
/// `add_layout` method, which allows a layout to be added at a position,
/// effectively translating all movement actions inside the layout by the
/// position.
#[derive(Debug, Clone, PartialEq)]
pub struct LayoutActions {
origin: Size,
actions: Vec<LayoutAction>,
active_font: (FaceId, f64),
next_pos: Option<Size>,
next_font: Option<(FaceId, f64)>,
}
impl LayoutActions {
/// Create a new action list.
pub fn new() -> LayoutActions {
LayoutActions {
actions: vec![],
origin: Size::ZERO,
active_font: (FaceId::MAX, 0.0),
next_pos: None,
next_font: None,
}
}
/// Add an action to the list.
pub fn add(&mut self, action: LayoutAction) {
match action {
MoveAbsolute(pos) => self.next_pos = Some(self.origin + pos),
SetFont(index, size) => {
self.next_font = Some((index, size));
}
_ => {
self.flush_position();
self.flush_font();
self.actions.push(action);
}
}
}
/// Add a series of actions.
pub fn extend<I>(&mut self, actions: I) where I: IntoIterator<Item = LayoutAction> {
for action in actions.into_iter() {
self.add(action);
}
}
/// Add a layout at a position. All move actions inside the layout are
/// translated by the position.
pub fn add_layout(&mut self, position: Size, layout: Layout) {
self.flush_position();
self.origin = position;
self.next_pos = Some(position);
self.extend(layout.actions);
}
/// Whether there are any actions in this list.
pub fn is_empty(&self) -> bool {
self.actions.is_empty()
}
/// Return the list of actions as a vector.
pub fn into_vec(self) -> Vec<LayoutAction> {
self.actions
}
/// Append a cached move action if one is cached.
fn flush_position(&mut self) {
if let Some(target) = self.next_pos.take() {
self.actions.push(MoveAbsolute(target));
}
}
/// Append a cached font-setting action if one is cached.
fn flush_font(&mut self) {
if let Some((index, size)) = self.next_font.take() {
if (index, size) != self.active_font {
self.actions.push(SetFont(index, size));
self.active_font = (index, size);
}
}
}
}

84
src/layout/elements.rs Normal file
View File

@ -0,0 +1,84 @@
//! The elements layouts are composed of.
use std::fmt::{self, Debug, Formatter};
use ttf_parser::GlyphId;
use fontdock::FaceId;
use crate::geom::Size;
/// A sequence of positioned layout elements.
#[derive(Debug, Clone, PartialEq)]
pub struct LayoutElements(pub Vec<(Size, LayoutElement)>);
impl LayoutElements {
/// Create an empty sequence.
pub fn new() -> Self {
LayoutElements(vec![])
}
/// Add an element at a position.
pub fn push(&mut self, pos: Size, element: LayoutElement) {
self.0.push((pos, element));
}
/// Add a sequence of elements offset by an `offset`.
pub fn extend_offset(&mut self, offset: Size, more: Self) {
for (subpos, element) in more.0 {
self.0.push((subpos + offset, element));
}
}
}
impl Default for LayoutElements {
fn default() -> Self {
Self::new()
}
}
/// A layouting action, which is the basic building block layouts are composed
/// of.
#[derive(Debug, Clone, PartialEq)]
pub enum LayoutElement {
/// Shaped text.
Text(Shaped),
}
/// A shaped run of text.
#[derive(Clone, PartialEq)]
pub struct Shaped {
pub text: String,
pub face: FaceId,
pub glyphs: Vec<GlyphId>,
pub offsets: Vec<f64>,
pub size: f64,
}
impl Shaped {
/// Create an empty shape run.
pub fn new(face: FaceId, size: f64) -> Shaped {
Shaped {
text: String::new(),
face,
glyphs: vec![],
offsets: vec![],
size,
}
}
/// Encode the glyph ids into a big-endian byte buffer.
pub fn encode_glyphs(&self) -> Vec<u8> {
const BYTES_PER_GLYPH: usize = 2;
let mut bytes = Vec::with_capacity(BYTES_PER_GLYPH * self.glyphs.len());
for g in &self.glyphs {
bytes.push((g.0 >> 8) as u8);
bytes.push((g.0 & 0xff) as u8);
}
bytes
}
}
impl Debug for Shaped {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Shaped({})", self.text)
}
}

View File

@ -35,9 +35,6 @@ pub struct LineContext {
pub align: LayoutAlign,
/// Whether to have repeated spaces or to use only the first and only once.
pub repeat: bool,
/// Whether to output a command which renders a debugging box showing the
/// extent of the layout.
pub debug: bool,
/// The line spacing.
pub line_spacing: f64,
}
@ -73,7 +70,6 @@ impl LineLayouter {
axes: ctx.axes,
align: ctx.align,
repeat: ctx.repeat,
debug: ctx.debug,
}),
ctx,
run: LineRun::new(),
@ -252,9 +248,9 @@ impl LineLayouter {
/// Finish the line and start a new one.
pub fn finish_line(&mut self) {
let mut actions = LayoutActions::new();
let mut elements = LayoutElements::new();
let layouts = std::mem::replace(&mut self.run.layouts, vec![]);
let layouts = std::mem::take(&mut self.run.layouts);
for (offset, layout) in layouts {
let x = match self.ctx.axes.primary.is_positive() {
true => offset,
@ -264,14 +260,14 @@ impl LineLayouter {
};
let pos = Size::with_x(x);
actions.add_layout(pos, layout);
elements.extend_offset(pos, layout.elements);
}
self.stack.add(Layout {
dimensions: self.run.size.specialized(self.ctx.axes),
align: self.run.align
.unwrap_or(LayoutAlign::new(Start, Start)),
actions: actions.into_vec(),
elements
});
self.run = LineRun::new();

View File

@ -2,17 +2,14 @@
use std::fmt::{self, Display, Formatter};
#[cfg(feature = "serialize")]
use serde::Serialize;
use fontdock::FaceId;
use crate::geom::{Size, Margins};
use self::prelude::*;
use elements::LayoutElements;
use prelude::*;
pub mod line;
pub mod stack;
pub mod text;
pub_use_mod!(actions);
pub mod elements;
pub_use_mod!(model);
/// Basic types used across the layouting engine.
@ -33,30 +30,13 @@ pub type MultiLayout = Vec<Layout>;
/// A finished box with content at fixed positions.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
pub struct Layout {
/// The size of the box.
pub dimensions: Size,
/// How to align this layout in a parent container.
#[cfg_attr(feature = "serialize", serde(skip))]
pub align: LayoutAlign,
/// The actions composing this layout.
pub actions: Vec<LayoutAction>,
}
impl Layout {
/// Returns a vector with all used font indices.
pub fn find_used_fonts(&self) -> Vec<FaceId> {
let mut fonts = Vec::new();
for action in &self.actions {
if let &LayoutAction::SetFont(id, _) = action {
if !fonts.contains(&id) {
fonts.push(id);
}
}
}
fonts
}
pub elements: LayoutElements,
}
/// A vector of layout spaces, that is stack allocated as long as it only

View File

@ -46,8 +46,6 @@ pub struct LayoutContext<'a> {
/// Whether the layout that is to be created will be nested in a parent
/// container.
pub nested: bool,
/// Whether to render debug boxs around layouts if `nested` is true.
pub debug: bool,
}
/// A sequence of layouting commands.
@ -117,7 +115,6 @@ impl<'a> ModelLayouter<'a> {
axes: ctx.axes,
align: ctx.align,
repeat: ctx.repeat,
debug: ctx.debug && ctx.nested,
line_spacing: ctx.style.text.line_spacing(),
}),
style: ctx.style.clone(),

View File

@ -48,9 +48,6 @@ pub struct StackContext {
pub align: LayoutAlign,
/// Whether to have repeated spaces or to use only the first and only once.
pub repeat: bool,
/// Whether to output a command which renders a debugging box showing the
/// extent of the layout.
pub debug: bool,
}
/// A layout space composed of subspaces which can have different axes and
@ -139,7 +136,7 @@ impl StackLayouter {
self.space.layouts.push((self.ctx.axes, Layout {
dimensions: dimensions.specialized(self.ctx.axes),
align: LayoutAlign::new(Start, Start),
actions: vec![]
elements: LayoutElements::new(),
}));
self.space.last_spacing = LastSpacing::Hard;
@ -367,13 +364,9 @@ impl StackLayouter {
// Step 4: Align each layout in its bounding box and collect everything
// into a single finished layout.
let mut actions = LayoutActions::new();
let mut elements = LayoutElements::new();
if self.ctx.debug {
actions.add(LayoutAction::DebugBox(dimensions));
}
let layouts = std::mem::replace(&mut self.space.layouts, vec![]);
let layouts = std::mem::take(&mut self.space.layouts);
for ((axes, layout), bound) in layouts.into_iter().zip(bounds) {
let size = layout.dimensions.specialized(axes);
let align = layout.align;
@ -387,13 +380,13 @@ impl StackLayouter {
let local = usable.anchor(align, axes) - size.anchor(align, axes);
let pos = Size::new(bound.left, bound.top) + local.specialized(axes);
actions.add_layout(pos, layout);
elements.extend_offset(pos, layout.elements);
}
self.layouts.push(Layout {
dimensions,
align: self.ctx.align,
actions: actions.into_vec(),
elements,
});
// ------------------------------------------------------------------ //

View File

@ -4,10 +4,12 @@
//! When the primary layouting axis horizontally inversed, the word is spelled
//! backwards. Vertical word layout is not yet supported.
use ttf_parser::GlyphId;
use fontdock::{FaceId, FaceQuery, FontStyle};
use crate::font::SharedFontLoader;
use crate::geom::Size;
use crate::style::TextStyle;
use super::elements::{LayoutElement, Shaped};
use super::*;
/// Performs the text layouting.
@ -15,9 +17,9 @@ use super::*;
struct TextLayouter<'a> {
ctx: TextContext<'a>,
text: &'a str,
actions: LayoutActions,
buffer: String,
active_font: FaceId,
shaped: Shaped,
elements: LayoutElements,
start: f64,
width: f64,
}
@ -48,9 +50,9 @@ impl<'a> TextLayouter<'a> {
TextLayouter {
ctx,
text,
actions: LayoutActions::new(),
buffer: String::new(),
active_font: FaceId::MAX,
shaped: Shaped::new(FaceId::MAX, ctx.style.font_size()),
elements: LayoutElements::new(),
start: 0.0,
width: 0.0,
}
}
@ -69,45 +71,53 @@ impl<'a> TextLayouter<'a> {
}
// Flush the last buffered parts of the word.
if !self.buffer.is_empty() {
self.actions.add(LayoutAction::WriteText(self.buffer));
if !self.shaped.text.is_empty() {
let pos = Size::new(self.start, 0.0);
self.elements.push(pos, LayoutElement::Text(self.shaped));
}
Layout {
dimensions: Size::new(self.width, self.ctx.style.font_size()),
align: self.ctx.align,
actions: self.actions.into_vec(),
elements: self.elements,
}
}
/// Layout an individual character.
async fn layout_char(&mut self, c: char) {
let (index, char_width) = match self.select_font(c).await {
let (index, glyph, char_width) = match self.select_font(c).await {
Some(selected) => selected,
// TODO: Issue warning about missing character.
None => return,
};
self.width += char_width;
// Flush the buffer and issue a font setting action if the font differs
// from the last character's one.
if self.active_font != index {
if !self.buffer.is_empty() {
let text = std::mem::replace(&mut self.buffer, String::new());
self.actions.add(LayoutAction::WriteText(text));
if self.shaped.face != index {
if !self.shaped.text.is_empty() {
let pos = Size::new(self.start, 0.0);
let shaped = std::mem::replace(
&mut self.shaped,
Shaped::new(FaceId::MAX, self.ctx.style.font_size()),
);
self.elements.push(pos, LayoutElement::Text(shaped));
self.start = self.width;
}
self.actions.add(LayoutAction::SetFont(index, self.ctx.style.font_size()));
self.active_font = index;
self.shaped.face = index;
}
self.buffer.push(c);
self.shaped.text.push(c);
self.shaped.glyphs.push(glyph);
self.shaped.offsets.push(self.width);
self.width += char_width;
}
/// Select the best font for a character and return its index along with
/// the width of the char in the font.
async fn select_font(&mut self, c: char) -> Option<(FaceId, f64)> {
async fn select_font(&mut self, c: char) -> Option<(FaceId, GlyphId, f64)> {
let mut loader = self.ctx.loader.borrow_mut();
let mut variant = self.ctx.style.variant;
@ -140,7 +150,7 @@ impl<'a> TextLayouter<'a> {
let glyph_width = face.glyph_hor_advance(glyph)?;
let char_width = to_raw(glyph_width) * self.ctx.style.font_size();
Some((id, char_width))
Some((id, glyph, char_width))
} else {
None
}

View File

@ -13,8 +13,7 @@
//! - **Exporting:** The finished layout can then be exported into a supported
//! format. Submodules for these formats are located in the
//! [export](crate::export) module. Currently, the only supported output
//! format is [_PDF_](crate::export::pdf). Alternatively, the layout can be
//! serialized to pass it to a suitable renderer.
//! format is [_PDF_](crate::export::pdf).
use std::fmt::Debug;
@ -62,8 +61,6 @@ pub struct Typesetter {
style: LayoutStyle,
/// The base parser state.
parse_state: ParseState,
/// Whether to render debug boxes.
debug: bool,
}
impl Typesetter {
@ -73,7 +70,6 @@ impl Typesetter {
loader,
style: LayoutStyle::default(),
parse_state: ParseState { scope: Scope::with_std() },
debug: false,
}
}
@ -87,11 +83,6 @@ impl Typesetter {
self.style.page = style;
}
/// Set whether to render debug boxes.
pub fn set_debug(&mut self, debug: bool) {
self.debug = debug;
}
/// Parse source code into a syntax tree.
pub fn parse(&self, src: &str) -> Pass<SyntaxModel> {
parse(src, Pos::ZERO, &self.parse_state)
@ -117,7 +108,6 @@ impl Typesetter {
axes: LayoutAxes::new(LTT, TTB),
align: LayoutAlign::new(Start, Start),
nested: false,
debug: self.debug,
},
).await
}

View File

@ -8,7 +8,6 @@ function! {
body: SyntaxModel,
width: Option<ScaleLength>,
height: Option<ScaleLength>,
debug: Option<bool>,
}
parse(header, body, ctx, f) {
@ -16,7 +15,6 @@ function! {
body: body!(opt: body, ctx, f).unwrap_or(SyntaxModel::new()),
width: header.args.key.get::<ScaleLength>("width", f),
height: header.args.key.get::<ScaleLength>("height", f),
debug: header.args.key.get::<bool>("debug", f),
}
}
@ -24,10 +22,6 @@ function! {
ctx.repeat = false;
ctx.spaces.truncate(1);
if let Some(debug) = self.debug {
ctx.debug = debug;
}
self.width.with(|v| {
let length = v.raw_scaled(ctx.base.x);
ctx.base.x = length;

BIN
tests/out/coma.pdf Normal file

Binary file not shown.

BIN
tests/out/coma.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@ -1,182 +0,0 @@
import sys
import os
import math
import numpy
import json
from PIL import Image, ImageDraw, ImageFont
BASE = os.path.dirname(__file__)
CACHE = 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, f'{name}.serde.json')
with open(filename, encoding='utf-8') as file:
data = json.load(file)
renderer = MultiboxRenderer(data)
renderer.render()
image = renderer.export()
image.save(os.path.join(CACHE, f'{name}.png'))
class MultiboxRenderer:
def __init__(self, data):
self.combined = None
self.faces = {}
for entry in data["faces"]:
face_id = int(entry[0]["index"]), int(entry[0]["variant"])
self.faces[face_id] = os.path.join(BASE, '../../', entry[1])
self.layouts = data["layouts"]
def render(self):
images = []
horizontal = math.floor(math.sqrt(len(self.layouts)))
start = 1
for layout in self.layouts:
size = layout["dimensions"]
renderer = BoxRenderer(self.faces, size["x"], size["y"])
for action in layout["actions"]:
renderer.execute(action)
images.append(renderer.export())
i = 0
x = 10
y = 10
width = 10
row_height = 0
positions = []
for image in images:
positions.append((x, y))
x += 10 + image.width
row_height = max(row_height, image.height)
i += 1
if i >= horizontal:
width = max(width, x)
x = 10
y += 10 + row_height
i = 0
row_height = 0
height = y
if i != 0:
height += 10 + row_height
self.combined = Image.new('RGBA', (width, height))
for (position, image) in zip(positions, images):
self.combined.paste(image, position)
def export(self):
return self.combined
class BoxRenderer:
def __init__(self, faces, width, height, grid=False):
self.faces = faces
self.size = (pix(width), pix(height))
img = Image.new('RGBA', self.size, (255, 255, 255, 255))
pixels = numpy.array(img)
if grid:
for i in range(0, int(height)):
for j in range(0, int(width)):
if ((i // 2) % 2 == 0) == ((j // 2) % 2 == 0):
pixels[4*i:4*(i+1), 4*j:4*(j+1)] = (225, 225, 225, 255)
self.img = Image.fromarray(pixels, 'RGBA')
self.draw = ImageDraw.Draw(self.img)
self.cursor = (0, 0)
self.colors = [
(176, 264, 158),
(274, 173, 207),
(158, 252, 264),
(285, 275, 187),
(132, 217, 136),
(236, 177, 246),
(174, 232, 279),
(285, 234, 158)
]
self.rects = []
self.color_index = 0
def execute(self, command):
cmd = command[0]
args = command[1:]
if cmd == 0:
self.cursor = [pix(args[0]["x"]), pix(args[0]["y"])]
elif cmd == 1:
face_id = int(args[0]["index"]), int(args[0]["variant"])
size = pix(args[1])
self.font = ImageFont.truetype(self.faces[face_id], size)
elif cmd == 2:
text = args[0]
width = self.draw.textsize(text, font=self.font)[0]
self.draw.text(self.cursor, text, (0, 0, 0, 255), font=self.font)
self.cursor[0] += width
elif cmd == 3:
x, y = self.cursor
w, h = pix(args[0]["x"]), pix(args[0]["y"])
rect = [x, y, x+w-1, y+h-1]
forbidden_colors = set()
for other_rect, other_color in self.rects:
if rect == other_rect:
return
if overlap(rect, other_rect) or overlap(other_rect, rect):
forbidden_colors.add(other_color)
for color in self.colors[self.color_index:] + self.colors[:self.color_index]:
self.color_index = (self.color_index + 1) % len(self.colors)
if color not in forbidden_colors:
break
overlay = Image.new('RGBA', self.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
draw.rectangle(rect, fill=color + (255,))
self.img = Image.alpha_composite(self.img, overlay)
self.draw = ImageDraw.Draw(self.img)
self.rects.append((rect, color))
else:
raise Exception('invalid command')
def export(self):
return self.img
# the number of pixels per raw unit
def pix(raw):
return int(4 * raw)
def overlap(a, b):
return (a[0] < b[2] and b[0] < a[2]) and (a[1] < b[3] and b[1] < a[3])
if __name__ == '__main__':
main()

View File

@ -1,206 +0,0 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::error::Error;
use std::ffi::OsStr;
use std::fs::{File, create_dir_all, read_dir, read_to_string};
use std::io::BufWriter;
use std::panic;
use std::process::Command;
use std::rc::Rc;
use std::time::{Instant, Duration};
use serde::Serialize;
use futures_executor::block_on;
use typstc::Typesetter;
use typstc::font::DynProvider;
use typstc::geom::{Size, Value4};
use typstc::layout::MultiLayout;
use typstc::length::Length;
use typstc::style::PageStyle;
use typstc::paper::PaperClass;
use typstc::export::pdf;
use fontdock::{FaceId, FontLoader};
use fontdock::fs::{FsIndex, FsProvider};
type DynResult<T> = Result<T, Box<dyn Error>>;
fn main() -> DynResult<()> {
let opts = Options::parse();
create_dir_all("tests/cache")?;
let tests: Vec<_> = read_dir("tests/")?.collect();
let mut filtered = Vec::new();
for entry in tests {
let path = entry?.path();
if path.extension() != Some(OsStr::new("typ")) {
continue;
}
let name = path
.file_stem().ok_or("expected file stem")?
.to_string_lossy()
.to_string();
if opts.matches(&name) {
let src = read_to_string(&path)?;
filtered.push((name, src));
}
}
let len = filtered.len();
if len == 0 {
return Ok(());
} else if len == 1 {
println!("Running test ...");
} else {
println!("Running {} tests", len);
}
let mut index = FsIndex::new();
index.search_dir("fonts");
for (name, src) in filtered {
panic::catch_unwind(|| {
if let Err(e) = test(&name, &src, &index) {
println!("error: {:?}", e);
}
}).ok();
}
Ok(())
}
/// Create a _PDF_ and render with a name from the source code.
fn test(name: &str, src: &str, index: &FsIndex) -> DynResult<()> {
println!("Testing: {}.", name);
let (descriptors, files) = index.clone().into_vecs();
let provider = FsProvider::new(files.clone());
let dynamic = Box::new(provider) as Box<DynProvider>;
let loader = FontLoader::new(dynamic, descriptors);
let loader = Rc::new(RefCell::new(loader));
let mut typesetter = Typesetter::new(loader.clone());
typesetter.set_page_style(PageStyle {
class: PaperClass::Custom,
dimensions: Size::with_all(Length::pt(250.0).as_raw()),
margins: Value4::with_all(None),
});
let layouts = compile(&typesetter, src);
// Write the PDF file.
let path = format!("tests/cache/{}.pdf", name);
let file = BufWriter::new(File::create(path)?);
pdf::export(&layouts, &loader, file)?;
// Compute the font's paths.
let mut faces = HashMap::new();
for layout in &layouts {
for id in layout.find_used_fonts() {
faces.entry(id).or_insert_with(|| {
files[id.index][id.variant].0.to_str().unwrap()
});
}
}
#[derive(Serialize)]
struct Document<'a> {
faces: Vec<(FaceId, &'a str)>,
layouts: MultiLayout,
}
let document = Document { faces: faces.into_iter().collect(), layouts };
// Serialize the document into JSON.
let path = format!("tests/cache/{}.serde.json", name);
let file = BufWriter::new(File::create(&path)?);
serde_json::to_writer(file, &document)?;
// Render the layout into a PNG.
Command::new("python")
.arg("tests/src/render.py")
.arg(name)
.spawn()
.expect("failed to run python renderer")
.wait()
.expect("command did not run");
std::fs::remove_file(path)?;
Ok(())
}
/// Compile the source code with the typesetter.
fn compile(typesetter: &Typesetter, src: &str) -> MultiLayout {
if cfg!(debug_assertions) {
let typeset = block_on(typesetter.typeset(src));
let diagnostics = typeset.feedback.diagnostics;
if !diagnostics.is_empty() {
for diagnostic in diagnostics {
println!(" {:?} {:?}: {}",
diagnostic.v.level,
diagnostic.span,
diagnostic.v.message
);
}
}
typeset.output
} else {
fn measure<T>(f: impl FnOnce() -> T) -> (T, Duration) {
let start = Instant::now();
let output = f();
let duration = Instant::now() - start;
(output, duration)
};
let (_, cold) = measure(|| block_on(typesetter.typeset(src)));
let (model, parse) = measure(|| typesetter.parse(src).output);
let (layouts, layout) = measure(|| block_on(typesetter.layout(&model)).output);
println!(" - cold start: {:?}", cold);
println!(" - warmed up: {:?}", parse + layout);
println!(" - parsing: {:?}", parse);
println!(" - layouting: {:?}", layout);
layouts
}
}
/// Command line options.
struct Options {
filter: Vec<String>,
perfect: bool,
}
impl Options {
/// Parse the options from the environment arguments.
fn parse() -> Options {
let mut perfect = false;
let mut filter = Vec::new();
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--nocapture" => {},
"=" => perfect = true,
_ => filter.push(arg),
}
}
Options { filter, perfect }
}
/// Whether a given test should be executed.
fn matches(&self, name: &str) -> bool {
match self.perfect {
true => self.filter.iter().any(|p| name == p),
false => self.filter.is_empty()
|| self.filter.iter().any(|p| name.contains(p))
}
}
}

248
tests/test_typeset.rs Normal file
View File

@ -0,0 +1,248 @@
use std::cell::RefCell;
use std::env;
use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::BufWriter;
use std::rc::Rc;
use futures_executor::block_on;
use raqote::{DrawTarget, Source, SolidSource, PathBuilder, Vector, Transform};
use ttf_parser::OutlineBuilder;
use typstc::Typesetter;
use typstc::font::{DynProvider, SharedFontLoader};
use typstc::geom::{Size, Value4};
use typstc::layout::MultiLayout;
use typstc::layout::elements::{LayoutElement, Shaped};
use typstc::length::Length;
use typstc::style::PageStyle;
use typstc::paper::PaperClass;
use typstc::export::pdf;
use fontdock::FontLoader;
use fontdock::fs::{FsIndex, FsProvider};
const TEST_DIR: &str = "tests";
const OUT_DIR: &str = "tests/out";
const FONT_DIR: &str = "fonts";
const BLACK: SolidSource = SolidSource { r: 0, g: 0, b: 0, a: 255 };
const WHITE: SolidSource = SolidSource { r: 255, g: 255, b: 255, a: 255 };
fn main() {
let filter = TestFilter::new(env::args().skip(1));
let mut filtered = Vec::new();
for entry in fs::read_dir(TEST_DIR).unwrap() {
let path = entry.unwrap().path();
if path.extension() != Some(OsStr::new("typ")) {
continue;
}
let name = path
.file_stem()
.unwrap()
.to_string_lossy()
.to_string();
if filter.matches(&name) {
let src = fs::read_to_string(&path).unwrap();
filtered.push((name, src));
}
}
let len = filtered.len();
if len == 0 {
return;
} else if len == 1 {
println!("Running test ...");
} else {
println!("Running {} tests", len);
}
fs::create_dir_all(OUT_DIR).unwrap();
let mut index = FsIndex::new();
index.search_dir(FONT_DIR);
let (descriptors, files) = index.clone().into_vecs();
let provider = FsProvider::new(files.clone());
let dynamic = Box::new(provider) as Box<DynProvider>;
let loader = FontLoader::new(dynamic, descriptors);
let loader = Rc::new(RefCell::new(loader));
let mut typesetter = Typesetter::new(loader.clone());
typesetter.set_page_style(PageStyle {
class: PaperClass::Custom,
dimensions: Size::with_all(Length::pt(250.0).as_raw()),
margins: Value4::with_all(None),
});
for (name, src) in filtered {
test(&name, &src, &mut typesetter, &loader)
}
}
fn test(
name: &str,
src: &str,
typesetter: &mut Typesetter,
loader: &SharedFontLoader,
) {
println!("Testing {}.", name);
let typeset = block_on(typesetter.typeset(src));
let layouts = typeset.output;
for diagnostic in typeset.feedback.diagnostics {
println!(" {:?} {:?}: {}",
diagnostic.v.level,
diagnostic.span,
diagnostic.v.message
);
}
// Render the PNG file.
let png_path = format!("{}/{}.png", OUT_DIR, name);
render(&layouts, &loader, 3.0).write_png(png_path).unwrap();
// Write the PDF file.
let pdf_path = format!("{}/{}.pdf", OUT_DIR, name);
let file = BufWriter::new(File::create(pdf_path).unwrap());
pdf::export(&layouts, &loader, file).unwrap();
}
struct TestFilter {
filter: Vec<String>,
perfect: bool,
}
impl TestFilter {
fn new(args: impl Iterator<Item=String>) -> TestFilter {
let mut filter = Vec::new();
let mut perfect = false;
for arg in args {
match arg.as_str() {
"--nocapture" => {},
"=" => perfect = true,
_ => filter.push(arg),
}
}
TestFilter { filter, perfect }
}
fn matches(&self, name: &str) -> bool {
if self.perfect {
self.filter.iter().any(|p| name == p)
} else {
self.filter.is_empty()
|| self.filter.iter().any(|p| name.contains(p))
}
}
}
fn render(
layouts: &MultiLayout,
loader: &SharedFontLoader,
scale: f64,
) -> DrawTarget {
let pad = scale * 10.0;
let width = 2.0 * pad + layouts.iter()
.map(|l| scale * l.dimensions.x)
.max_by(|a, b| a.partial_cmp(&b).unwrap())
.unwrap()
.round();
let height = pad + layouts.iter()
.map(|l| scale * l.dimensions.y + pad)
.sum::<f64>()
.round();
let mut surface = DrawTarget::new(width as i32, height as i32);
surface.clear(BLACK);
let mut offset = Size::new(pad, pad);
for layout in layouts {
surface.fill_rect(
offset.x as f32,
offset.y as f32,
(scale * layout.dimensions.x) as f32,
(scale * layout.dimensions.y) as f32,
&Source::Solid(WHITE),
&Default::default(),
);
for &(pos, ref element) in &layout.elements.0 {
match element {
LayoutElement::Text(shaped) => {
render_shaped(
&mut surface,
loader,
shaped,
scale * pos + offset,
scale,
);
},
}
}
offset.y += scale * layout.dimensions.y + pad;
}
surface
}
fn render_shaped(
surface: &mut DrawTarget,
loader: &SharedFontLoader,
shaped: &Shaped,
pos: Size,
scale: f64,
) {
let loader = loader.borrow();
let face = loader.get_loaded(shaped.face);
for (&glyph, &offset) in shaped.glyphs.iter().zip(&shaped.offsets) {
let mut builder = WrappedPathBuilder(PathBuilder::new());
face.outline_glyph(glyph, &mut builder);
let path = builder.0.finish();
let units_per_em = face.units_per_em().unwrap_or(1000);
let s = scale * (shaped.size / units_per_em as f64);
let x = pos.x + scale * offset;
let y = pos.y + scale * shaped.size;
let t = Transform::create_scale(s as f32, -s as f32)
.post_translate(Vector::new(x as f32, y as f32));
surface.fill(
&path.transform(&t),
&Source::Solid(SolidSource { r: 0, g: 0, b: 0, a: 255 }),
&Default::default(),
)
}
}
struct WrappedPathBuilder(PathBuilder);
impl OutlineBuilder for WrappedPathBuilder {
fn move_to(&mut self, x: f32, y: f32) {
self.0.move_to(x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
self.0.line_to(x, y);
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.0.quad_to(x1, y1, x, y);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.0.cubic_to(x1, y1, x2, y2, x, y);
}
fn close(&mut self) {
self.0.close();
}
}