From 0c87c0c5a5b7379e938ef9f37673f9c9c0bff051 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 13 Mar 2019 18:42:33 +0100 Subject: [PATCH] =?UTF-8?q?Basic=20multiline=20support=20=F0=9F=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/doc.rs | 114 +++++++++++++++++++++++++++++++++++++++++++------- src/engine.rs | 93 +++++++++++++++++++++++++++++++--------- src/font.rs | 16 ++++++- src/lib.rs | 61 ++++++++++++++++++++++++--- 4 files changed, 244 insertions(+), 40 deletions(-) diff --git a/src/doc.rs b/src/doc.rs index bff4926ab..3e060f6e1 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, - /// 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 ops::Sub for Size { - type Output = Size; - - fn sub(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) + } + } + + 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 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 { + self.points.partial_cmp(&other.points) + } + } + + impl Sum for Size { + fn sum(iter: I) -> Size where I: Iterator { + iter.fold(Size::zero(), Add::add) + } } } diff --git a/src/engine.rs b/src/engine.rs index 34f987663..ef50ecb42 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, + active_font: usize, + text_commands: Vec, + 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 { - let style = Style::default(); - + pub fn typeset(mut self) -> TypeResult { // 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 305423917..c90fb983f 100644 --- a/src/font.rs +++ b/src/font.rs @@ -23,6 +23,15 @@ pub struct Font { pub widths: Vec, /// 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 { - 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 e9d0b1b4d..deb5b0667 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 { - 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. + "#); + } }