Basic multiline support 📜

This commit is contained in:
Laurenz 2019-03-13 18:42:33 +01:00
parent 107450ee5c
commit 0c87c0c5a5
4 changed files with 244 additions and 40 deletions

View File

@ -1,6 +1,5 @@
//! Abstract representation of a typesetted document.
use std::ops;
use crate::font::Font;
@ -23,8 +22,10 @@ pub struct Style {
/// A fallback list of font families to use.
pub font_families: Vec<String>,
/// The default font size.
/// The font size.
pub font_size: f32,
/// The line spacing (as a multiple of the font size).
pub line_spacing: f32,
}
impl Default for Style {
@ -39,6 +40,7 @@ impl Default for Style {
"NotoSans", "NotoSansMath"
]).iter().map(ToString::to_string).collect(),
font_size: 12.0,
line_spacing: 1.25,
}
}
}
@ -78,6 +80,10 @@ pub struct Size {
}
impl Size {
/// Create an zeroed size.
#[inline]
pub fn zero() -> Size { Size { points: 0.0 } }
/// Create a size from a number of points.
#[inline]
pub fn from_points(points: f32) -> Size { Size { points } }
@ -111,18 +117,98 @@ impl Size {
pub fn to_cm(&self) -> f32 { self.points * 0.0352778 }
}
impl ops::Add for Size {
mod size {
use super::Size;
use std::cmp::Ordering;
use std::fmt;
use std::iter::Sum;
use std::ops::*;
impl fmt::Display for Size {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}pt", self.points)
}
}
macro_rules! impl_reflexive {
($trait:ident, $func:ident, $assign_trait:ident, $assign_func:ident) => {
impl $trait for Size {
type Output = Size;
fn add(self, other: Size) -> Size {
Size { points: self.points + other.points }
#[inline]
fn $func(self, other: Size) -> Size {
Size { points: $trait::$func(self.points, other.points) }
}
}
impl ops::Sub for Size {
impl $assign_trait for Size {
#[inline]
fn $assign_func(&mut self, other: Size) {
$assign_trait::$assign_func(&mut self.points, other.points);
}
}
};
}
macro_rules! impl_num_back {
($trait:ident, $func:ident, $assign_trait:ident, $assign_func:ident, $ty:ty) => {
impl $trait<$ty> for Size {
type Output = Size;
fn sub(self, other: Size) -> Size {
Size { points: self.points - other.points }
#[inline]
fn $func(self, other: $ty) -> Size {
Size { points: $trait::$func(self.points, other as f32) }
}
}
impl $assign_trait<$ty> for Size {
#[inline]
fn $assign_func(&mut self, other: $ty) {
$assign_trait::$assign_func(&mut self.points, other as f32);
}
}
};
}
macro_rules! impl_num_both {
($trait:ident, $func:ident, $assign_trait:ident, $assign_func:ident, $ty:ty) => {
impl_num_back!($trait, $func, $assign_trait, $assign_func, $ty);
impl $trait<Size> for $ty {
type Output = Size;
#[inline]
fn $func(self, other: Size) -> Size {
Size { points: $trait::$func(self as f32, other.points) }
}
}
};
}
impl Neg for Size {
type Output = Size;
fn neg(self) -> Size {
Size { points: -self.points }
}
}
impl_reflexive!(Add, add, AddAssign, add_assign);
impl_reflexive!(Sub, sub, SubAssign, sub_assign);
impl_num_both!(Mul, mul, MulAssign, mul_assign, f32);
impl_num_both!(Mul, mul, MulAssign, mul_assign, i32);
impl_num_back!(Div, div, DivAssign, div_assign, f32);
impl_num_back!(Div, div, DivAssign, div_assign, i32);
impl PartialOrd for Size {
fn partial_cmp(&self, other: &Size) -> Option<Ordering> {
self.points.partial_cmp(&other.points)
}
}
impl Sum for Size {
fn sum<I>(iter: I) -> Size where I: Iterator<Item=Size> {
iter.fold(Size::zero(), Add::add)
}
}
}

View File

@ -3,59 +3,114 @@
use std::error;
use std::fmt;
use crate::syntax::{SyntaxTree, Node};
use crate::doc::{Document, Style, Page, Text, TextCommand};
use crate::doc::{Document, Style, Size, Page, Text, TextCommand};
use crate::font::Font;
/// The core typesetting engine, transforming an abstract syntax tree into a document.
#[derive(Debug, Clone)]
pub struct Engine<'s> {
tree: SyntaxTree<'s>,
// Immutable
tree: &'s SyntaxTree<'s>,
style: Style,
// Mutable
fonts: Vec<Font>,
active_font: usize,
text_commands: Vec<TextCommand>,
current_line: String,
current_width: Size,
}
impl<'s> Engine<'s> {
/// Create a new generator from a syntax tree.
pub fn new(tree: SyntaxTree<'s>) -> Engine<'s> {
Engine { tree }
pub fn new(tree: &'s SyntaxTree<'s>) -> Engine<'s> {
Engine {
style: Style::default(),
tree,
fonts: Vec::new(),
active_font: 0,
text_commands: Vec::new(),
current_line: String::new(),
current_width: Size::zero(),
}
}
/// Generate the abstract document.
pub fn typeset(&mut self) -> TypeResult<Document> {
let style = Style::default();
pub fn typeset(mut self) -> TypeResult<Document> {
// Load font defined by style
let font_family = style.font_families.first().unwrap();
let font_family = self.style.font_families.first().unwrap();
let program = std::fs::read(format!("../fonts/{}-Regular.ttf", font_family)).unwrap();
let font = Font::new(program).unwrap();
let mut text = String::new();
self.fonts.push(font);
self.active_font = 0;
// Move cursor to top-left position
self.text_commands.push(TextCommand::Move(
self.style.margins[0],
self.style.paper_size[1] - self.style.margins[1])
);
// Set the current font
self.text_commands.push(TextCommand::SetFont(0, self.style.font_size));
// Iterate through the documents nodes.
for node in &self.tree.nodes {
match node {
Node::Space if !text.is_empty() => text.push(' '),
Node::Space | Node::Newline => (),
Node::Word(word) => text.push_str(word),
Node::Word(word) => self.write_word(word),
Node::Space => self.write_space(),
Node::Newline => (),
Node::ToggleItalics | Node::ToggleBold | Node::ToggleMath => unimplemented!(),
Node::Func(_) => unimplemented!(),
}
}
// Create a page from the contents.
let page = Page {
size: style.paper_size,
size: self.style.paper_size,
text: vec![Text {
commands: vec![
TextCommand::Move(style.margins[0], style.paper_size[1] - style.margins[1]),
TextCommand::SetFont(0, style.font_size),
TextCommand::Text(text)
]
commands: self.text_commands,
}],
};
Ok(Document {
pages: vec![page],
fonts: vec![font],
fonts: self.fonts,
})
}
fn write_word(&mut self, word: &str) {
let max_width = self.style.paper_size[0] - 2 * self.style.margins[0];
let font = &self.fonts[self.active_font];
let width = word.chars()
.map(|c| font.widths[font.map(c) as usize] * self.style.font_size)
.sum();
if self.current_width + width > max_width {
let vertical_move = - self.style.font_size
* self.style.line_spacing
* font.metrics.ascender;
self.text_commands.push(TextCommand::Move(Size::zero(), vertical_move));
self.current_line.clear();
self.current_width = Size::zero();
}
self.text_commands.push(TextCommand::Text(word.to_owned()));
self.current_line.push_str(word);
self.current_width += width;
}
fn write_space(&mut self) {
if !self.current_line.is_empty() {
self.write_word(" ");
}
}
}
/// Result type used for parsing.

View File

@ -23,6 +23,15 @@ pub struct Font {
pub widths: Vec<Size>,
/// The fallback glyph.
pub default_glyph: u16,
/// The relevant metrics of this font.
pub metrics: FontMetrics,
}
/// Font metrics relevant to the typesetting engine.
#[derive(Debug, Clone, PartialEq)]
pub struct FontMetrics {
/// The typographics ascender relevant for line spacing.
pub ascender: Size,
}
impl Font {
@ -44,12 +53,17 @@ impl Font {
let font_name = base_font.unwrap_or_else(|| "unknown".to_owned());
let widths = hmtx.metrics.iter().map(|m| convert(m.advance_width)).collect();
let metrics = FontMetrics {
ascender: convert(os2.s_typo_ascender),
};
Ok(Font {
name: font_name,
program,
mapping: charmap.mapping,
widths,
default_glyph: os2.us_default_char.unwrap_or(0),
metrics,
})
}
@ -62,7 +76,6 @@ impl Font {
/// Encode the given text for this font (into glyph ids).
#[inline]
pub fn encode(&self, text: &str) -> Vec<u8> {
println!("encoding {} with {:?}", text, self.mapping);
let mut bytes = Vec::with_capacity(2 * text.len());
for glyph in text.chars().map(|c| self.map(c)) {
bytes.push((glyph >> 8) as u8);
@ -181,6 +194,7 @@ impl<'p> Subsetter<'p> {
mapping,
widths,
default_glyph: self.font.default_glyph,
metrics: self.font.metrics.clone(),
})
}

View File

@ -68,7 +68,8 @@ impl<'s> Compiler<'s> {
/// Return the abstract typesetted representation of the document.
#[inline]
pub fn typeset(&self) -> Result<Document, Error> {
Engine::new(self.parse()?).typeset().map_err(Into::into)
let tree = self.parse()?;
Engine::new(&tree).typeset().map_err(Into::into)
}
/// Write the document as a _PDF_, returning how many bytes were written.
@ -145,15 +146,63 @@ mod test {
}
#[test]
fn pdf() {
fn pdfs() {
test("unicode", "∑mbe∂∂ed font with Unicode!");
test("parentheses", "Text with ) and ( or (enclosed) works.");
test("composite-glyph", "Composite character‼");
test("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 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.
");
}
#[test]
fn long() {
test("wikipedia", r#"
Typesetting is the composition of text by means of arranging physical types or the
digital equivalents. Stored letters and other symbols (called sorts in mechanical
systems and glyphs in digital systems) are retrieved and ordered according to a
language's orthography for visual display. Typesetting requires one or more fonts
(which are widely but erroneously confused with and substituted for typefaces). One
significant effect of typesetting was that authorship of works could be spotted more
easily, making it difficult for copiers who have not gained permission.
During much of the letterpress era, movable type was composed by hand for each page.
Cast metal sorts were composed into words, then lines, then paragraphs, then pages of
text and tightly bound together to make up a form, with all letter faces exactly the
same "height to paper", creating an even surface of type. The form was placed in a
press, inked, and an impression made on paper.
During typesetting, individual sorts are picked from a type case with the right hand,
and set into a composing stick held in the left hand from left to right, and as viewed
by the setter upside down. As seen in the photo of the composing stick, a lower case
'q' looks like a 'd', a lower case 'b' looks like a 'p', a lower case 'p' looks like a
'b' and a lower case 'd' looks like a 'q'. This is reputed to be the origin of the
expression "mind your p's and q's". It might just as easily have been "mind your b's
and d's".
The diagram at right illustrates a cast metal sort: a face, b body or shank, c point
size, 1 shoulder, 2 nick, 3 groove, 4 foot. Wooden printing sorts were in use for
centuries in combination with metal type. Not shown, and more the concern of the
casterman, is the set, or width of each sort. Set width, like body size, is measured
in points.
In order to extend the working life of type, and to account for the finite sorts in a
case of type, copies of forms were cast when anticipating subsequent printings of a
text, freeing the costly type for other work. This was particularly prevalent in book
and newspaper work where rotary presses required type forms to wrap an impression
cylinder rather than set in the bed of a press. In this process, called stereotyping,
the entire form is pressed into a fine matrix such as plaster of Paris or papier mâché
called a flong to create a positive, from which the stereotype form was electrotyped,
cast of type metal.
Advances such as the typewriter and computer would push the state of the art even
farther ahead. Still, hand composition and letterpress printing have not fallen
completely out of use, and since the introduction of digital typesetting, it has seen a
revival as an artisanal pursuit. However, it is a very small niche within the larger
typesetting market.
"#);
}
}