mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Redesign document representation 🧱
This commit is contained in:
parent
5942c3ba2a
commit
67281c4f46
223
src/doc.rs
223
src/doc.rs
@ -1,34 +1,60 @@
|
|||||||
//! Generation of abstract documents from syntax trees.
|
//! Abstract representation of a typesetted document.
|
||||||
|
|
||||||
#![allow(dead_code)]
|
use std::ops;
|
||||||
|
use crate::font::Font;
|
||||||
use std::fmt;
|
|
||||||
use crate::parsing::{SyntaxTree, Node};
|
|
||||||
|
|
||||||
|
|
||||||
/// Abstract representation of a complete typesetted document.
|
|
||||||
///
|
|
||||||
/// This abstract thing can then be serialized into a specific format like _PDF_.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Document {
|
pub struct Document {
|
||||||
/// The pages of the document.
|
|
||||||
pub pages: Vec<Page>,
|
pub pages: Vec<Page>,
|
||||||
/// The font the document is written in.
|
pub fonts: Vec<Font>,
|
||||||
pub font: String,
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Style {
|
||||||
|
// Paper
|
||||||
|
pub paper_size: [Size; 2],
|
||||||
|
pub margins: [Size; 4],
|
||||||
|
|
||||||
|
// Font handling
|
||||||
|
pub font_families: Vec<String>,
|
||||||
|
pub font_size: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Style {
|
||||||
|
fn default() -> Style {
|
||||||
|
Style {
|
||||||
|
// A4 paper with 1.5 cm margins in all directions
|
||||||
|
paper_size: [Size::from_mm(210.0), Size::from_mm(297.0)],
|
||||||
|
margins: [Size::from_cm(2.5); 4],
|
||||||
|
|
||||||
|
// Default font family
|
||||||
|
font_families: (&[
|
||||||
|
"NotoSans", "NotoSansMath"
|
||||||
|
]).iter().map(ToString::to_string).collect(),
|
||||||
|
font_size: 12.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A page of a document.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
/// The width and height of the page.
|
pub width: Size,
|
||||||
pub size: [Size; 2],
|
pub height: Size,
|
||||||
/// The contents of the page.
|
pub text: Vec<Text>,
|
||||||
pub contents: Vec<Text>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Plain text.
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
pub struct Text {
|
||||||
pub struct Text(pub String);
|
pub commands: Vec<TextCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum TextCommand {
|
||||||
|
Text(String),
|
||||||
|
Move(Size, Size),
|
||||||
|
SetFont(usize, f32),
|
||||||
|
}
|
||||||
|
|
||||||
/// A general distance type that can convert between units.
|
/// A general distance type that can convert between units.
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
@ -40,170 +66,49 @@ pub struct Size {
|
|||||||
impl Size {
|
impl Size {
|
||||||
/// Create a size from a number of points.
|
/// Create a size from a number of points.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_points(points: f32) -> Size {
|
pub fn from_points(points: f32) -> Size { Size { points } }
|
||||||
Size { points }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a size from a number of inches.
|
/// Create a size from a number of inches.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_inches(inches: f32) -> Size {
|
pub fn from_inches(inches: f32) -> Size { Size { points: 72.0 * inches } }
|
||||||
Size { points: 72.0 * inches }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a size from a number of millimeters.
|
/// Create a size from a number of millimeters.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_mm(mm: f32) -> Size {
|
pub fn from_mm(mm: f32) -> Size { Size { points: 2.83465 * mm } }
|
||||||
Size { points: 2.83465 * mm }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a size from a number of centimeters.
|
/// Create a size from a number of centimeters.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_cm(cm: f32) -> Size {
|
pub fn from_cm(cm: f32) -> Size { Size { points: 28.3465 * cm } }
|
||||||
Size { points: 28.3465 * cm }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a size from a number of points.
|
/// Create a size from a number of points.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn to_points(&self) -> f32 {
|
pub fn to_points(&self) -> f32 { self.points }
|
||||||
self.points
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a size from a number of inches.
|
/// Create a size from a number of inches.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn to_inches(&self) -> f32 {
|
pub fn to_inches(&self) -> f32 { self.points * 0.0138889 }
|
||||||
self.points * 0.0138889
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a size from a number of millimeters.
|
/// Create a size from a number of millimeters.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn to_mm(&self) -> f32 {
|
pub fn to_mm(&self) -> f32 { self.points * 0.352778 }
|
||||||
self.points * 0.352778
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a size from a number of centimeters.
|
/// Create a size from a number of centimeters.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn to_cm(&self) -> f32 {
|
pub fn to_cm(&self) -> f32 { self.points * 0.0352778 }
|
||||||
self.points * 0.0352778
|
}
|
||||||
|
|
||||||
|
impl ops::Add for Size {
|
||||||
|
type Output = Size;
|
||||||
|
|
||||||
|
fn add(self, other: Size) -> Size {
|
||||||
|
Size { points: self.points + other.points }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ops::Sub for Size {
|
||||||
|
type Output = Size;
|
||||||
|
|
||||||
/// A type that can be generated into a document.
|
fn sub(self, other: Size) -> Size {
|
||||||
pub trait Generate {
|
Size { points: self.points - other.points }
|
||||||
/// Generate a document from self.
|
|
||||||
fn generate(self) -> GenResult<Document>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Generate for SyntaxTree<'_> {
|
|
||||||
fn generate(self) -> GenResult<Document> {
|
|
||||||
Generator::new(self).generate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Result type used for parsing.
|
|
||||||
type GenResult<T> = std::result::Result<T, GenerationError>;
|
|
||||||
|
|
||||||
/// A failure when generating.
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
||||||
pub struct GenerationError {
|
|
||||||
/// A message describing the error.
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for GenerationError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "generation error: {}", self.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Transforms an abstract syntax tree into a document.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct Generator<'s> {
|
|
||||||
tree: SyntaxTree<'s>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'s> Generator<'s> {
|
|
||||||
/// Create a new generator from a syntax tree.
|
|
||||||
fn new(tree: SyntaxTree<'s>) -> Generator<'s> {
|
|
||||||
Generator { tree }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate the abstract document.
|
|
||||||
fn generate(&mut self) -> GenResult<Document> {
|
|
||||||
let mut text = String::new();
|
|
||||||
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::ToggleItalics | Node::ToggleBold | Node::ToggleMath => unimplemented!(),
|
|
||||||
Node::Func(_) => unimplemented!(),
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let page = Page {
|
|
||||||
size: [Size::from_mm(210.0), Size::from_mm(297.0)],
|
|
||||||
contents: vec![ Text(text) ],
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Document {
|
|
||||||
pages: vec![page],
|
|
||||||
font: "NotoSans-Regular".to_owned(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gives a generation error with a message.
|
|
||||||
#[inline]
|
|
||||||
fn err<R, S: Into<String>>(&self, message: S) -> GenResult<R> {
|
|
||||||
Err(GenerationError { message: message.into() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod generator_tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::parsing::ParseTree;
|
|
||||||
|
|
||||||
/// Test if the source gets generated into the document.
|
|
||||||
fn test(src: &str, doc: Document) {
|
|
||||||
assert_eq!(src.parse_tree().unwrap().generate(), Ok(doc));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test if generation gives this error for the source code.
|
|
||||||
fn test_err(src: &str, err: GenerationError) {
|
|
||||||
assert_eq!(src.parse_tree().unwrap().generate(), Err(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn generator_simple() {
|
|
||||||
test("This is an example of a sentence.", Document {
|
|
||||||
pages: vec![
|
|
||||||
Page {
|
|
||||||
size: [Size::from_mm(210.0), Size::from_mm(297.0)],
|
|
||||||
contents: vec![
|
|
||||||
Text("This is an example of a sentence.".to_owned()),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
font: "NotoSans-Regular".to_owned(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn generate_emoji() {
|
|
||||||
use crate::write::WritePdf;
|
|
||||||
let doc = Document {
|
|
||||||
pages: vec![Page {
|
|
||||||
size: [Size::from_mm(210.0), Size::from_mm(297.0)],
|
|
||||||
contents: vec![Text("🌍".to_owned())]
|
|
||||||
}],
|
|
||||||
font: "NotoEmoji-Regular".to_owned(),
|
|
||||||
};
|
|
||||||
let mut file = std::fs::File::create("../target/typeset-doc-emoji.pdf").unwrap();
|
|
||||||
file.write_pdf(&doc).unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
87
src/engine.rs
Normal file
87
src/engine.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
//! Core typesetting engine.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use crate::parsing::{SyntaxTree, Node};
|
||||||
|
use crate::doc::{Document, Style, Page, Text, TextCommand};
|
||||||
|
use crate::font::Font;
|
||||||
|
|
||||||
|
|
||||||
|
/// A type that can be typesetted into a document.
|
||||||
|
pub trait Typeset {
|
||||||
|
/// Generate a document from self.
|
||||||
|
fn typeset(self) -> TypeResult<Document>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Typeset for SyntaxTree<'_> {
|
||||||
|
fn typeset(self) -> TypeResult<Document> {
|
||||||
|
Engine::new(self).typeset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result type used for parsing.
|
||||||
|
type TypeResult<T> = std::result::Result<T, TypesetError>;
|
||||||
|
|
||||||
|
/// Errors occuring in the process of typesetting.
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct TypesetError {
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TypesetError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.write_str(&self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Transforms an abstract syntax tree into a document.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Engine<'s> {
|
||||||
|
tree: SyntaxTree<'s>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> Engine<'s> {
|
||||||
|
/// Create a new generator from a syntax tree.
|
||||||
|
fn new(tree: SyntaxTree<'s>) -> Engine<'s> {
|
||||||
|
Engine { tree }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the abstract document.
|
||||||
|
fn typeset(&mut self) -> TypeResult<Document> {
|
||||||
|
let style = Style::default();
|
||||||
|
|
||||||
|
// Load font defined by style
|
||||||
|
let font_family = 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();
|
||||||
|
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::ToggleItalics | Node::ToggleBold | Node::ToggleMath => unimplemented!(),
|
||||||
|
Node::Func(_) => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let page = Page {
|
||||||
|
width: style.paper_size[0],
|
||||||
|
height: style.paper_size[1],
|
||||||
|
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)
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Document {
|
||||||
|
pages: vec![page],
|
||||||
|
fonts: vec![font],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
77
src/font.rs
77
src/font.rs
@ -6,18 +6,62 @@ use std::io::{self, Cursor, Seek, SeekFrom};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use byteorder::{BE, ReadBytesExt, WriteBytesExt};
|
use byteorder::{BE, ReadBytesExt, WriteBytesExt};
|
||||||
use opentype::{OpenTypeReader, Outlines, TableRecord, Tag};
|
use opentype::{OpenTypeReader, Outlines, TableRecord, Tag};
|
||||||
use opentype::tables::{Header, CharMap, MaximumProfile, HorizontalMetrics};
|
use opentype::tables::{Header, Name, NameEntry, CharMap, MaximumProfile, HorizontalMetrics, OS2};
|
||||||
|
use crate::doc::Size;
|
||||||
|
|
||||||
|
|
||||||
/// An font wrapper which allows to subset a font.
|
/// An font wrapper which allows to subset a font.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Font {
|
pub struct Font {
|
||||||
program: Vec<u8>,
|
pub name: String,
|
||||||
|
pub program: Vec<u8>,
|
||||||
|
pub mapping: HashMap<char, u16>,
|
||||||
|
pub widths: Vec<Size>,
|
||||||
|
pub default_glyph: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Font {
|
impl Font {
|
||||||
/// Create a new font from a font program.
|
/// Create a new font from a font program.
|
||||||
pub fn new(program: Vec<u8>) -> Font {
|
pub fn new(program: Vec<u8>) -> Result<Font, opentype::Error> {
|
||||||
Font { program }
|
let mut readable = Cursor::new(&program);
|
||||||
|
let mut reader = OpenTypeReader::new(&mut readable);
|
||||||
|
|
||||||
|
let head = reader.read_table::<Header>()?;
|
||||||
|
let name = reader.read_table::<Name>()?;
|
||||||
|
let os2 = reader.read_table::<OS2>()?;
|
||||||
|
let charmap = reader.read_table::<CharMap>()?;
|
||||||
|
let hmtx = reader.read_table::<HorizontalMetrics>()?;
|
||||||
|
|
||||||
|
let unit_ratio = 1.0 / (head.units_per_em as f32);
|
||||||
|
let convert = |x| Size::from_points(unit_ratio * x as f32);
|
||||||
|
|
||||||
|
let base_font = name.get_decoded(NameEntry::PostScriptName);
|
||||||
|
let font_name = base_font.unwrap_or_else(|| "unknown".to_owned());
|
||||||
|
let widths = hmtx.metrics.iter().map(|m| convert(m.advance_width)).collect();
|
||||||
|
|
||||||
|
Ok(Font {
|
||||||
|
name: font_name,
|
||||||
|
program,
|
||||||
|
mapping: charmap.mapping,
|
||||||
|
widths,
|
||||||
|
default_glyph: os2.us_default_char.unwrap_or(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a character to it's glyph index.
|
||||||
|
pub fn map(&self, c: char) -> u16 {
|
||||||
|
self.mapping.get(&c).map(|&g| g).unwrap_or(self.default_glyph)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode the given text for our font (into glyph ids).
|
||||||
|
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);
|
||||||
|
bytes.push((glyph & 0xff) as u8);
|
||||||
|
}
|
||||||
|
bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a subsetted version of this font including only the chars listed in
|
/// Generate a subsetted version of this font including only the chars listed in
|
||||||
@ -33,7 +77,7 @@ impl Font {
|
|||||||
chars: C,
|
chars: C,
|
||||||
needed_tables: I1,
|
needed_tables: I1,
|
||||||
optional_tables: I2
|
optional_tables: I2
|
||||||
) -> Result<(Vec<u8>, HashMap<char, u16>), SubsettingError>
|
) -> Result<Font, SubsettingError>
|
||||||
where
|
where
|
||||||
C: IntoIterator<Item=char>,
|
C: IntoIterator<Item=char>,
|
||||||
I1: IntoIterator<Item=S1>, S1: AsRef<str>,
|
I1: IntoIterator<Item=S1>, S1: AsRef<str>,
|
||||||
@ -48,7 +92,7 @@ impl Font {
|
|||||||
tables.sort_by_key(|r| r.tag);
|
tables.sort_by_key(|r| r.tag);
|
||||||
|
|
||||||
Subsetter {
|
Subsetter {
|
||||||
program: &self.program,
|
font: &self,
|
||||||
reader,
|
reader,
|
||||||
outlines,
|
outlines,
|
||||||
tables,
|
tables,
|
||||||
@ -65,7 +109,7 @@ impl Font {
|
|||||||
|
|
||||||
struct Subsetter<'p> {
|
struct Subsetter<'p> {
|
||||||
// Original font
|
// Original font
|
||||||
program: &'p [u8],
|
font: &'p Font,
|
||||||
reader: OpenTypeReader<'p, Cursor<&'p Vec<u8>>>,
|
reader: OpenTypeReader<'p, Cursor<&'p Vec<u8>>>,
|
||||||
outlines: Outlines,
|
outlines: Outlines,
|
||||||
tables: Vec<TableRecord>,
|
tables: Vec<TableRecord>,
|
||||||
@ -82,7 +126,7 @@ struct Subsetter<'p> {
|
|||||||
|
|
||||||
impl<'p> Subsetter<'p> {
|
impl<'p> Subsetter<'p> {
|
||||||
fn subset<I1, S1, I2, S2>(mut self, needed_tables: I1, optional_tables: I2)
|
fn subset<I1, S1, I2, S2>(mut self, needed_tables: I1, optional_tables: I2)
|
||||||
-> SubsettingResult<(Vec<u8>, HashMap<char, u16>)>
|
-> SubsettingResult<Font>
|
||||||
where
|
where
|
||||||
I1: IntoIterator<Item=S1>, S1: AsRef<str>,
|
I1: IntoIterator<Item=S1>, S1: AsRef<str>,
|
||||||
I2: IntoIterator<Item=S2>, S2: AsRef<str>
|
I2: IntoIterator<Item=S2>, S2: AsRef<str>
|
||||||
@ -117,10 +161,21 @@ impl<'p> Subsetter<'p> {
|
|||||||
|
|
||||||
self.write_header()?;
|
self.write_header()?;
|
||||||
|
|
||||||
|
let widths = self.glyphs.iter()
|
||||||
|
.map(|&glyph| self.font.widths.get(glyph as usize).map(|&w| w)
|
||||||
|
.take_invalid("missing glyph metrics"))
|
||||||
|
.collect::<SubsettingResult<Vec<_>>>()?;
|
||||||
|
|
||||||
let mapping = self.chars.into_iter().enumerate().map(|(i, c)| (c, i as u16))
|
let mapping = self.chars.into_iter().enumerate().map(|(i, c)| (c, i as u16))
|
||||||
.collect::<HashMap<char, u16>>();
|
.collect::<HashMap<char, u16>>();
|
||||||
|
|
||||||
Ok((self.body, mapping))
|
Ok(Font {
|
||||||
|
name: self.font.name.clone(),
|
||||||
|
program: self.body,
|
||||||
|
mapping,
|
||||||
|
widths,
|
||||||
|
default_glyph: self.font.default_glyph,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_glyphs(&mut self) -> SubsettingResult<()> {
|
fn build_glyphs(&mut self) -> SubsettingResult<()> {
|
||||||
@ -131,6 +186,8 @@ impl<'p> Subsetter<'p> {
|
|||||||
self.glyphs.push(cmap.get(c).ok_or_else(|| SubsettingError::MissingCharacter(c))?)
|
self.glyphs.push(cmap.get(c).ok_or_else(|| SubsettingError::MissingCharacter(c))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.glyphs.push(self.font.default_glyph);
|
||||||
|
|
||||||
// Composite glyphs may need additional glyphs we have not yet in our list.
|
// Composite glyphs may need additional glyphs we have not yet in our list.
|
||||||
// So now we have a look at the glyf table to check that and add glyphs
|
// So now we have a look at the glyf table to check that and add glyphs
|
||||||
// we need additionally.
|
// we need additionally.
|
||||||
@ -400,7 +457,7 @@ impl<'p> Subsetter<'p> {
|
|||||||
Err(_) => return Err(SubsettingError::MissingTable(tag.to_string())),
|
Err(_) => return Err(SubsettingError::MissingTable(tag.to_string())),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.program.get(record.offset as usize .. (record.offset + record.length) as usize)
|
self.font.program.get(record.offset as usize .. (record.offset + record.length) as usize)
|
||||||
.take_bytes()
|
.take_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
//! # Example
|
//! # Example
|
||||||
//! This is an example of compiling a really simple document into _PDF_.
|
//! This is an example of compiling a really simple document into _PDF_.
|
||||||
//! ```
|
//! ```
|
||||||
//! use typeset::{parsing::ParseTree, doc::Generate, write::WritePdf};
|
//! use typeset::{parsing::ParseTree, engine::Typeset, write::WritePdf};
|
||||||
//!
|
//!
|
||||||
//! // Create an output file.
|
//! // Create an output file.
|
||||||
//! # /*
|
//! # /*
|
||||||
@ -15,7 +15,7 @@
|
|||||||
//!
|
//!
|
||||||
//! // Parse the source and then generate the document.
|
//! // Parse the source and then generate the document.
|
||||||
//! let src = "Hello World from Typeset‼";
|
//! let src = "Hello World from Typeset‼";
|
||||||
//! let doc = src.parse_tree().unwrap().generate().unwrap();
|
//! let doc = src.parse_tree().unwrap().typeset().unwrap();
|
||||||
//!
|
//!
|
||||||
//! // Write the document into file as PDF.
|
//! // Write the document into file as PDF.
|
||||||
//! file.write_pdf(&doc).unwrap();
|
//! file.write_pdf(&doc).unwrap();
|
||||||
@ -23,9 +23,10 @@
|
|||||||
|
|
||||||
mod pdf;
|
mod pdf;
|
||||||
mod utility;
|
mod utility;
|
||||||
pub mod font;
|
|
||||||
pub mod parsing;
|
pub mod parsing;
|
||||||
pub mod doc;
|
pub mod doc;
|
||||||
|
pub mod engine;
|
||||||
|
pub mod font;
|
||||||
|
|
||||||
/// Writing of documents into supported formats.
|
/// Writing of documents into supported formats.
|
||||||
pub mod write {
|
pub mod write {
|
||||||
|
226
src/pdf.rs
226
src/pdf.rs
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io::{self, Write, Cursor};
|
use std::io::{self, Write, Cursor};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashSet;
|
||||||
use pdf::{PdfWriter, Id, Rect, Version, Trailer};
|
use pdf::{PdfWriter, Id, Rect, Version, Trailer};
|
||||||
use pdf::doc::{Catalog, PageTree, Page, Resource, Content};
|
use pdf::doc::{Catalog, PageTree, Page, Resource, Content};
|
||||||
use pdf::text::Text;
|
use pdf::text::Text;
|
||||||
@ -10,8 +10,8 @@ use pdf::font::{
|
|||||||
Type0Font, CMapEncoding, CIDFont, CIDFontType, CIDSystemInfo,
|
Type0Font, CMapEncoding, CIDFont, CIDFontType, CIDSystemInfo,
|
||||||
WidthRecord, FontDescriptor, FontFlags, EmbeddedFont, GlyphUnit
|
WidthRecord, FontDescriptor, FontFlags, EmbeddedFont, GlyphUnit
|
||||||
};
|
};
|
||||||
use opentype::{OpenTypeReader, tables::{self, NameEntry, MacStyleFlags}};
|
use opentype::{OpenTypeReader, tables::{self, MacStyleFlags}};
|
||||||
use crate::doc::Document;
|
use crate::doc::{self, Document, TextCommand};
|
||||||
use crate::font::Font;
|
use crate::font::Font;
|
||||||
|
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ struct PdfCreator<'a, W: Write> {
|
|||||||
writer: PdfWriter<'a, W>,
|
writer: PdfWriter<'a, W>,
|
||||||
doc: &'a Document,
|
doc: &'a Document,
|
||||||
offsets: Offsets,
|
offsets: Offsets,
|
||||||
font: PdfFont,
|
fonts: Vec<PdfFont>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Offsets for the various groups of ids.
|
/// Offsets for the various groups of ids.
|
||||||
@ -87,33 +87,50 @@ impl<'a, W: Write> PdfCreator<'a, W> {
|
|||||||
let catalog = 1;
|
let catalog = 1;
|
||||||
let page_tree = catalog + 1;
|
let page_tree = catalog + 1;
|
||||||
let pages = (page_tree + 1, page_tree + doc.pages.len() as Id);
|
let pages = (page_tree + 1, page_tree + doc.pages.len() as Id);
|
||||||
let content_count = doc.pages.iter().flat_map(|p| p.contents.iter()).count() as Id;
|
let content_count = doc.pages.iter().flat_map(|p| p.text.iter()).count() as Id;
|
||||||
let contents = (pages.1 + 1, pages.1 + content_count);
|
let contents = (pages.1 + 1, pages.1 + content_count);
|
||||||
let fonts = (contents.1 + 1, contents.1 + 4);
|
let fonts = (contents.1 + 1, contents.1 + 4 * doc.fonts.len() as Id);
|
||||||
|
|
||||||
|
let offsets = Offsets {
|
||||||
|
catalog,
|
||||||
|
page_tree,
|
||||||
|
pages,
|
||||||
|
contents,
|
||||||
|
fonts,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(doc.fonts.len() > 0);
|
||||||
|
|
||||||
// Find out which chars are used in this document.
|
// Find out which chars are used in this document.
|
||||||
let mut chars = HashSet::new();
|
let mut char_sets = vec![HashSet::new(); doc.fonts.len()];
|
||||||
|
let mut current_font: usize = 0;
|
||||||
for page in &doc.pages {
|
for page in &doc.pages {
|
||||||
for content in &page.contents {
|
for text in &page.text {
|
||||||
chars.extend(content.0.chars());
|
for command in &text.commands {
|
||||||
|
match command {
|
||||||
|
TextCommand::Text(string) => {
|
||||||
|
char_sets[current_font].extend(string.chars());
|
||||||
|
},
|
||||||
|
TextCommand::SetFont(id, _) => {
|
||||||
|
assert!(*id < doc.fonts.len());
|
||||||
|
current_font = *id;
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a subsetted pdf font.
|
// Create a subsetted pdf font.
|
||||||
let data = std::fs::read(format!("../fonts/{}.ttf", doc.font))?;
|
let fonts = doc.fonts.iter().enumerate().map(|(i, font)| {
|
||||||
let font = PdfFont::new(&doc.font, data, chars)?;
|
PdfFont::new(font, &char_sets[i])
|
||||||
|
}).collect::<PdfResult<Vec<_>>>()?;
|
||||||
|
|
||||||
Ok(PdfCreator {
|
Ok(PdfCreator {
|
||||||
writer: PdfWriter::new(target),
|
writer: PdfWriter::new(target),
|
||||||
doc,
|
doc,
|
||||||
offsets: Offsets {
|
offsets,
|
||||||
catalog,
|
fonts,
|
||||||
page_tree,
|
|
||||||
pages,
|
|
||||||
contents,
|
|
||||||
fonts,
|
|
||||||
},
|
|
||||||
font,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,8 +171,8 @@ impl<'a, W: Write> PdfCreator<'a, W> {
|
|||||||
// The page objects
|
// The page objects
|
||||||
let mut id = self.offsets.pages.0;
|
let mut id = self.offsets.pages.0;
|
||||||
for page in &self.doc.pages {
|
for page in &self.doc.pages {
|
||||||
let width = page.size[0].to_points();
|
let width = page.width.to_points();
|
||||||
let height = page.size[1].to_points();
|
let height = page.height.to_points();
|
||||||
|
|
||||||
self.writer.write_obj(id, Page::new(self.offsets.page_tree)
|
self.writer.write_obj(id, Page::new(self.offsets.page_tree)
|
||||||
.media_box(Rect::new(0.0, 0.0, width, height))
|
.media_box(Rect::new(0.0, 0.0, width, height))
|
||||||
@ -172,77 +189,92 @@ impl<'a, W: Write> PdfCreator<'a, W> {
|
|||||||
fn write_contents(&mut self) -> PdfResult<()> {
|
fn write_contents(&mut self) -> PdfResult<()> {
|
||||||
let mut id = self.offsets.contents.0;
|
let mut id = self.offsets.contents.0;
|
||||||
for page in &self.doc.pages {
|
for page in &self.doc.pages {
|
||||||
for content in &page.contents {
|
for text in &page.text {
|
||||||
self.writer.write_obj(id, &Text::new()
|
self.write_text(id, text)?;
|
||||||
.set_font(1, 13.0)
|
|
||||||
.move_line(108.0, 734.0)
|
|
||||||
.write_text(&self.encode(&content.0))
|
|
||||||
.to_stream()
|
|
||||||
)?;
|
|
||||||
id += 1;
|
id += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_text(&mut self, id: u32, text: &doc::Text) -> PdfResult<()> {
|
||||||
|
let mut current_font = 0;
|
||||||
|
let encoded = text.commands.iter().filter_map(|cmd| match cmd {
|
||||||
|
TextCommand::Text(string) => Some(self.fonts[current_font].encode(&string)),
|
||||||
|
TextCommand::SetFont(id, _) => { current_font = *id; None },
|
||||||
|
_ => None,
|
||||||
|
}).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut object = Text::new();
|
||||||
|
let mut nr = 0;
|
||||||
|
|
||||||
|
for command in &text.commands {
|
||||||
|
match command {
|
||||||
|
TextCommand::Text(_) => {
|
||||||
|
object.write_text(&encoded[nr]);
|
||||||
|
nr += 1;
|
||||||
|
},
|
||||||
|
TextCommand::SetFont(id, size) => {
|
||||||
|
object.set_font(*id as u32 + 1, *size);
|
||||||
|
},
|
||||||
|
TextCommand::Move(x, y) => {
|
||||||
|
object.move_line(x.to_points(), y.to_points());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.writer.write_obj(id, &object.to_stream())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the fonts.
|
/// Write the fonts.
|
||||||
fn write_fonts(&mut self) -> PdfResult<()> {
|
fn write_fonts(&mut self) -> PdfResult<()> {
|
||||||
let id = self.offsets.fonts.0;
|
let mut id = self.offsets.fonts.0;
|
||||||
|
|
||||||
self.writer.write_obj(id, &Type0Font::new(
|
for font in &self.fonts {
|
||||||
self.font.name.clone(),
|
self.writer.write_obj(id, &Type0Font::new(
|
||||||
CMapEncoding::Predefined("Identity-H".to_owned()),
|
font.name.clone(),
|
||||||
id + 1
|
CMapEncoding::Predefined("Identity-H".to_owned()),
|
||||||
)).unwrap();
|
id + 1
|
||||||
|
))?;
|
||||||
|
|
||||||
self.writer.write_obj(id + 1,
|
self.writer.write_obj(id + 1,
|
||||||
CIDFont::new(
|
CIDFont::new(
|
||||||
CIDFontType::Type2,
|
CIDFontType::Type2,
|
||||||
self.font.name.clone(),
|
font.name.clone(),
|
||||||
CIDSystemInfo::new("(Adobe)", "(Identity)", 0),
|
CIDSystemInfo::new("(Adobe)", "(Identity)", 0),
|
||||||
id + 2,
|
id + 2,
|
||||||
).widths(vec![WidthRecord::start(0, self.font.widths.clone())])
|
).widths(vec![WidthRecord::start(0, font.widths.clone())])
|
||||||
).unwrap();
|
)?;
|
||||||
|
|
||||||
self.writer.write_obj(id + 2,
|
self.writer.write_obj(id + 2,
|
||||||
FontDescriptor::new(
|
FontDescriptor::new(
|
||||||
self.font.name.clone(),
|
font.name.clone(),
|
||||||
self.font.flags,
|
font.flags,
|
||||||
self.font.italic_angle,
|
font.italic_angle,
|
||||||
)
|
)
|
||||||
.font_bbox(self.font.bounding_box)
|
.font_bbox(font.bounding_box)
|
||||||
.ascent(self.font.ascender)
|
.ascent(font.ascender)
|
||||||
.descent(self.font.descender)
|
.descent(font.descender)
|
||||||
.cap_height(self.font.cap_height)
|
.cap_height(font.cap_height)
|
||||||
.stem_v(self.font.stem_v)
|
.stem_v(font.stem_v)
|
||||||
.font_file_3(id + 3)
|
.font_file_3(id + 3)
|
||||||
).unwrap();
|
)?;
|
||||||
|
|
||||||
|
self.writer.write_obj(id + 3, &EmbeddedFont::OpenType(&font.program))?;
|
||||||
|
|
||||||
self.writer.write_obj(id + 3, &EmbeddedFont::OpenType(&self.font.data)).unwrap();
|
id += 4;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode the given text for our font.
|
|
||||||
fn encode(&self, text: &str) -> Vec<u8> {
|
|
||||||
let mut bytes = Vec::with_capacity(2 * text.len());
|
|
||||||
for glyph in text.chars().map(|c| self.font.map(c)) {
|
|
||||||
bytes.push((glyph >> 8) as u8);
|
|
||||||
bytes.push((glyph & 0xff) as u8);
|
|
||||||
}
|
|
||||||
bytes
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// The data we need from the font.
|
/// The data we need from the font.
|
||||||
struct PdfFont {
|
struct PdfFont {
|
||||||
data: Vec<u8>,
|
font: Font,
|
||||||
mapping: HashMap<char, u16>,
|
|
||||||
default_glyph: u16,
|
|
||||||
name: String,
|
|
||||||
widths: Vec<GlyphUnit>,
|
widths: Vec<GlyphUnit>,
|
||||||
flags: FontFlags,
|
flags: FontFlags,
|
||||||
italic_angle: f32,
|
italic_angle: f32,
|
||||||
@ -256,47 +288,36 @@ struct PdfFont {
|
|||||||
impl PdfFont {
|
impl PdfFont {
|
||||||
/// Create a subetted version of the font and calculate some information
|
/// Create a subetted version of the font and calculate some information
|
||||||
/// needed for creating the _PDF_.
|
/// needed for creating the _PDF_.
|
||||||
pub fn new(font_name: &str, data: Vec<u8>, chars: HashSet<char>) -> PdfResult<PdfFont> {
|
pub fn new(font: &Font, chars: &HashSet<char>) -> PdfResult<PdfFont> {
|
||||||
let mut readable = Cursor::new(&data);
|
let mut readable = Cursor::new(&font.program);
|
||||||
let mut reader = OpenTypeReader::new(&mut readable);
|
let mut reader = OpenTypeReader::new(&mut readable);
|
||||||
|
|
||||||
let head = reader.read_table::<tables::Header>()?;
|
let head = reader.read_table::<tables::Header>()?;
|
||||||
let name = reader.read_table::<tables::Name>()?;
|
|
||||||
let post = reader.read_table::<tables::Post>()?;
|
let post = reader.read_table::<tables::Post>()?;
|
||||||
let os2 = reader.read_table::<tables::OS2>()?;
|
let os2 = reader.read_table::<tables::OS2>()?;
|
||||||
|
|
||||||
let font = Font::new(data);
|
let subsetted = font.subsetted(
|
||||||
let (subsetted, mapping) = font.subsetted(
|
chars.iter().cloned(),
|
||||||
chars,
|
|
||||||
&["head", "hhea", "maxp", "hmtx", "loca", "glyf"],
|
&["head", "hhea", "maxp", "hmtx", "loca", "glyf"],
|
||||||
&["cvt ", "prep", "fpgm", "OS/2", "cmap", "name", "post"],
|
&["cvt ", "prep", "fpgm", "OS/2", "cmap", "name", "post"],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let unit_ratio = 1000.0 / (head.units_per_em as f32);
|
|
||||||
let convert = |x| (unit_ratio * x as f32).round() as GlyphUnit;
|
|
||||||
|
|
||||||
let base_font = name.get_decoded(NameEntry::PostScriptName);
|
|
||||||
let font_name = base_font.unwrap_or_else(|| font_name.to_owned());
|
|
||||||
|
|
||||||
|
|
||||||
let mut flags = FontFlags::empty();
|
let mut flags = FontFlags::empty();
|
||||||
flags.set(FontFlags::FIXED_PITCH, post.is_fixed_pitch);
|
flags.set(FontFlags::FIXED_PITCH, post.is_fixed_pitch);
|
||||||
flags.set(FontFlags::SERIF, font_name.contains("Serif"));
|
flags.set(FontFlags::SERIF, font.name.contains("Serif"));
|
||||||
flags.insert(FontFlags::SYMBOLIC);
|
flags.insert(FontFlags::SYMBOLIC);
|
||||||
flags.set(FontFlags::ITALIC, head.mac_style.contains(MacStyleFlags::ITALIC));
|
flags.set(FontFlags::ITALIC, head.mac_style.contains(MacStyleFlags::ITALIC));
|
||||||
flags.insert(FontFlags::SMALL_CAP);
|
flags.insert(FontFlags::SMALL_CAP);
|
||||||
|
|
||||||
let mut readable = Cursor::new(&subsetted);
|
let widths = subsetted.widths.iter()
|
||||||
let mut reader = OpenTypeReader::new(&mut readable);
|
.map(|w| (1000.0 * w.to_points()).round() as GlyphUnit)
|
||||||
let hmtx = reader.read_table::<tables::HorizontalMetrics>()?;
|
.collect();
|
||||||
let widths = hmtx.metrics.iter().map(|m| convert(m.advance_width)).collect();
|
|
||||||
|
|
||||||
|
let unit_ratio = 1.0 / (head.units_per_em as f32);
|
||||||
|
let convert = |x| (unit_ratio * x as f32).round() as GlyphUnit;
|
||||||
|
|
||||||
Ok(PdfFont {
|
Ok(PdfFont {
|
||||||
data: subsetted,
|
font: subsetted,
|
||||||
mapping,
|
|
||||||
default_glyph: os2.us_default_char.unwrap_or(0),
|
|
||||||
name: font_name,
|
|
||||||
widths,
|
widths,
|
||||||
flags,
|
flags,
|
||||||
italic_angle: post.italic_angle.to_f32(),
|
italic_angle: post.italic_angle.to_f32(),
|
||||||
@ -312,10 +333,13 @@ impl PdfFont {
|
|||||||
stem_v: (10.0 + 220.0 * (os2.us_weight_class as f32 - 50.0) / 900.0) as GlyphUnit,
|
stem_v: (10.0 + 220.0 * (os2.us_weight_class as f32 - 50.0) / 900.0) as GlyphUnit,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Map a character to it's glyph index.
|
impl std::ops::Deref for PdfFont {
|
||||||
fn map(&self, c: char) -> u16 {
|
type Target = Font;
|
||||||
self.mapping.get(&c).map(|&g| g).unwrap_or(self.default_glyph)
|
|
||||||
|
fn deref(&self) -> &Font {
|
||||||
|
&self.font
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,20 +348,21 @@ impl PdfFont {
|
|||||||
mod pdf_tests {
|
mod pdf_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::parsing::ParseTree;
|
use crate::parsing::ParseTree;
|
||||||
use crate::doc::Generate;
|
use crate::engine::Typeset;
|
||||||
|
|
||||||
/// Create a pdf with a name from the source code.
|
/// Create a pdf with a name from the source code.
|
||||||
fn test(name: &str, src: &str) {
|
fn test(name: &str, src: &str) {
|
||||||
let doc = src.parse_tree().unwrap().generate().unwrap();
|
let doc = src.parse_tree().unwrap().typeset().unwrap();
|
||||||
let path = format!("../target/typeset-pdf-{}.pdf", name);
|
let path = format!("../target/typeset-pdf-{}.pdf", name);
|
||||||
let mut file = std::fs::File::create(path).unwrap();
|
let mut file = std::fs::File::create(path).unwrap();
|
||||||
file.write_pdf(&doc).unwrap();
|
file.write_pdf(&doc).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pdf_simple() {
|
fn pdf() {
|
||||||
test("unicode", "∑mbe∂∂ed font with Unicode!");
|
test("unicode", "∑mbe∂∂ed font with Unicode!");
|
||||||
test("parentheses", "Text with ) and ( or (enclosed) works.");
|
test("parentheses", "Text with ) and ( or (enclosed) works.");
|
||||||
|
test("composite-glyph", "Composite character‼");
|
||||||
test("multiline","
|
test("multiline","
|
||||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed
|
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed
|
||||||
diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed
|
diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed
|
||||||
@ -345,9 +370,4 @@ mod pdf_tests {
|
|||||||
Stet clita kasd gubergren, no sea takimata sanctus est.
|
Stet clita kasd gubergren, no sea takimata sanctus est.
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn pdf_composite_glyph() {
|
|
||||||
test("composite-glyph", "Composite character‼");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user