summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2019-03-13 18:42:33 +0100
committerLaurenz <laurmaedje@gmail.com>2019-03-13 18:42:33 +0100
commit0c87c0c5a5b7379e938ef9f37673f9c9c0bff051 (patch)
tree6310996dd92bcf50999ae2f3ce7641d08ef36986
parent107450ee5ca8e7e0bc03cf6ce9f59268aa20e9f6 (diff)
Basic multiline support 📜
-rw-r--r--src/doc.rs108
-rw-r--r--src/engine.rs93
-rw-r--r--src/font.rs16
-rw-r--r--src/lib.rs61
4 files changed, 241 insertions, 37 deletions
diff --git a/src/doc.rs b/src/doc.rs
index bff4926a..3e060f6e 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -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 {
- type Output = Size;
+mod size {
+ use super::Size;
+ use std::cmp::Ordering;
+ use std::fmt;
+ use std::iter::Sum;
+ use std::ops::*;
- fn add(self, other: Size) -> Size {
- Size { points: self.points + other.points }
+ impl fmt::Display for Size {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}pt", self.points)
+ }
}
-}
-impl ops::Sub for Size {
- type Output = Size;
+ macro_rules! impl_reflexive {
+ ($trait:ident, $func:ident, $assign_trait:ident, $assign_func:ident) => {
+ impl $trait for Size {
+ type Output = Size;
+
+ #[inline]
+ fn $func(self, other: Size) -> Size {
+ Size { points: $trait::$func(self.points, other.points) }
+ }
+ }
+
+ 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;
+
+ #[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 }
+ }
+ }
- fn sub(self, other: Size) -> Size {
- Size { points: self.points - other.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)
+ }
}
}
diff --git a/src/engine.rs b/src/engine.rs
index 34f98766..ef50ecb4 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -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.
diff --git a/src/font.rs b/src/font.rs
index 30542391..c90fb983 100644
--- a/src/font.rs
+++ b/src/font.rs
@@ -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(),
})
}
diff --git a/src/lib.rs b/src/lib.rs
index e9d0b1b4..deb5b066 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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.
+ "#);
+ }
}