Layout elements and pure rust rendering 🥏
This commit is contained in:
parent
d5ff97f42e
commit
cbbc46215f
15
Cargo.toml
15
Cargo.toml
@ -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"]
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
14
src/font.rs
14
src/font.rs
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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
84
src/layout/elements.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
|
@ -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
|
||||
}
|
||||
|
12
src/lib.rs
12
src/lib.rs
@ -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
|
||||
}
|
||||
|
@ -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
BIN
tests/out/coma.pdf
Normal file
Binary file not shown.
BIN
tests/out/coma.png
Normal file
BIN
tests/out/coma.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
@ -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()
|
@ -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
248
tests/test_typeset.rs
Normal 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();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user