Extract into separate repository 🧱

This commit is contained in:
Laurenz 2019-10-09 19:45:40 +02:00
parent b96a7e0cf3
commit f22a307000
30 changed files with 374 additions and 1449 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target
**/*.rs.bk
Cargo.lock
things

View File

@ -1,28 +1,16 @@
[package]
name = "typeset"
name = "typst"
version = "0.1.0"
authors = ["Laurenz Mädje <laurmaedje@gmail.com>"]
edition = "2018"
[dependencies]
pdf = { path = "../pdf" }
opentype = { path = "../opentype" }
tide = { path = "../tide" }
toddle = { path = "../toddle" }
byteorder = "1"
smallvec = "0.6.10"
unicode-xid = "0.1.0"
toml = "0.5"
[dev-dependencies]
bencher = "0.1"
[[bin]]
name = "typst"
name = "typstc"
path = "src/bin/main.rs"
[[bench]]
name = "font"
harness = false
[[bench]]
name = "complete"
harness = false

View File

@ -1,48 +0,0 @@
use bencher::Bencher;
use typeset::Typesetter;
use typeset::font::FileSystemFontProvider;
use typeset::export::pdf::PdfExporter;
fn prepare<'p>() -> (Typesetter<'p>, &'static str) {
let src = include_str!("../test/shakespeare.tps");
let mut typesetter = Typesetter::new();
let provider = FileSystemFontProvider::from_listing("../fonts/fonts.toml").unwrap();
typesetter.add_font_provider(provider);
(typesetter, src)
}
/// Benchmarks only the parsing step.
fn parsing(b: &mut Bencher) {
let (typesetter, src) = prepare();
b.iter(|| { typesetter.parse(src).unwrap(); });
}
/// Benchmarks only the layouting step.
fn layouting(b: &mut Bencher) {
let (typesetter, src) = prepare();
let tree = typesetter.parse(src).unwrap();
b.iter(|| { typesetter.layout(&tree).unwrap(); });
}
/// Benchmarks the full typesetting step.
fn typesetting(b: &mut Bencher) {
let (typesetter, src) = prepare();
b.iter(|| { typesetter.typeset(src).unwrap(); });
}
/// Benchmarks only the exporting step.
fn exporting(b: &mut Bencher) {
let (typesetter, src) = prepare();
let doc = typesetter.typeset(src).unwrap();
let exporter = PdfExporter::new();
b.iter(|| {
let mut buf = Vec::new();
exporter.export(&doc, &typesetter.loader(), &mut buf).unwrap();
});
}
bencher::benchmark_group!(benches, parsing, layouting, typesetting, exporting);
bencher::benchmark_main!(benches);

View File

@ -1,46 +0,0 @@
use bencher::Bencher;
use typeset::font::{*, FontClass::*};
use typeset::style::TextStyle;
/// Benchmarks just the char-by-char font loading.
fn font_loading(b: &mut Bencher) {
let provider = FileSystemFontProvider::from_listing("../fonts/fonts.toml").unwrap();
let mut font_loader = FontLoader::new();
font_loader.add_font_provider(provider);
let text = include_str!("../test/shakespeare.tps");
let mut style = TextStyle {
classes: vec![Regular],
fallback: vec![
Family("Helvetica".to_string()),
Family("Computer Modern".to_string()),
Serif,
Monospace,
],
font_size: 12.0,
line_spacing: 1.0,
paragraph_spacing: 1.0,
};
b.iter(|| {
for character in text.chars() {
match character {
'_' => style.toggle_class(Italic),
'*' => style.toggle_class(Bold),
'\n' | '[' | ']' => {},
_ => {
let _font = font_loader.get(FontQuery {
character,
classes: style.classes.clone(),
fallback: style.fallback.clone(),
}).unwrap();
},
}
}
});
}
bencher::benchmark_group!(benches, font_loading);
bencher::benchmark_main!(benches);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
fonts/CMU-Serif-Bold.ttf Normal file

Binary file not shown.

BIN
fonts/CMU-Serif-Italic.ttf Normal file

Binary file not shown.

BIN
fonts/CMU-Serif-Regular.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

103
fonts/License-CMU.txt Normal file
View File

@ -0,0 +1,103 @@
Copyright (C) Authors of original metafont fonts:
Donald Ervin Knuth (cm, concrete fonts)
1995, 1996, 1997 J"org Knappen, 1990, 1992 Norbert Schwarz (ec fonts)
1992-2006 A.Khodulev, O.Lapko, A.Berdnikov, V.Volovich (lh fonts)
1997-2005 Claudio Beccari (cb greek fonts)
2002 FUKUI Rei (tipa fonts)
2003-2005 Han The Thanh (Vietnamese fonts)
1996-2005 Walter Schmidt (cmbright fonts)
Copyright (C) 2003-2009, Andrey V. Panov (panov@canopus.iacp.dvo.ru),
with Reserved Font Family Name "Computer Modern Unicode fonts".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

94
fonts/License-Noto.txt Normal file
View File

@ -0,0 +1,94 @@
Copyright 2018 The Noto Project Authors (github.com/googlei18n/noto-fonts)
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

BIN
fonts/NotoEmoji-Regular.ttf Normal file

Binary file not shown.

13
fonts/fonts.toml Normal file
View File

@ -0,0 +1,13 @@
"CMU-SansSerif-Regular.ttf" = ["Computer Modern", "Regular", "SansSerif"]
"CMU-SansSerif-Italic.ttf" = ["Computer Modern", "Italic", "SansSerif"]
"CMU-SansSerif-Bold.ttf" = ["Computer Modern", "Bold", "SansSerif"]
"CMU-SansSerif-Bold-Italic.ttf" = ["Computer Modern", "Bold", "Italic", "SansSerif"]
"CMU-Serif-Regular.ttf" = ["Computer Modern", "Regular", "Serif"]
"CMU-Serif-Italic.ttf" = ["Computer Modern", "Italic", "Serif"]
"CMU-Serif-Bold.ttf" = ["Computer Modern", "Bold", "Serif"]
"CMU-Serif-Bold-Italic.ttf" = ["Computer Modern", "Bold", "Italic", "Serif"]
"CMU-Typewriter-Regular.ttf" = ["Computer Modern", "Regular", "Serif", "SansSerif", "Monospace"]
"CMU-Typewriter-Italic.ttf" = ["Computer Modern", "Italic", "Serif", "SansSerif", "Monospace"]
"CMU-Typewriter-Bold.ttf" = ["Computer Modern", "Bold", "Serif", "SansSerif", "Monospace"]
"CMU-Typewriter-Bold-Italic.ttf" = ["Computer Modern", "Bold", "Italic", "Serif", "SansSerif", "Monospace"]
"NotoEmoji-Regular.ttf" = ["Noto", "Noto Emoji", "Regular", "SansSerif", "Serif", "Monospace"]

View File

@ -5,9 +5,9 @@ use std::io::{Read, BufWriter};
use std::path::{Path, PathBuf};
use std::process;
use typeset::Typesetter;
use typeset::font::FileSystemFontProvider;
use typeset::export::pdf::PdfExporter;
use typst::Typesetter;
use typst::export::pdf::PdfExporter;
use typst::toddle::query::FileSystemFontProvider;
fn main() {

View File

@ -3,13 +3,18 @@
use std::collections::{HashMap, HashSet};
use std::io::{self, Write};
use pdf::{PdfWriter, Ref, Rect, Version, Trailer, Content};
use pdf::doc::{Catalog, PageTree, Page, Resource, Text};
use pdf::font::{Type0Font, CIDFont, CIDFontType, CIDSystemInfo, FontDescriptor, FontFlags};
use pdf::font::{GlyphUnit, CMap, CMapEncoding, WidthRecord, FontStream};
use tide::{PdfWriter, Ref, Rect, Version, Trailer};
use tide::content::Content;
use tide::doc::{Catalog, PageTree, Page, Resource, Text};
use tide::font::{Type0Font, CIDFont, CIDFontType, CIDSystemInfo, FontDescriptor, FontFlags};
use tide::font::{GlyphUnit, CMap, CMapEncoding, WidthRecord, FontStream};
use toddle::tables::{Header, Post, OS2, HorizontalMetrics, CharMap, Name, NameEntry, MacStyleFlags};
use toddle::font::OwnedFont;
use toddle::query::SharedFontLoader;
use toddle::Error as FontError;
use crate::doc::{Document, Page as DocPage, LayoutAction};
use crate::font::{Font, FontLoader, FontError};
use crate::size::{Size, Size2D};
@ -26,7 +31,7 @@ impl PdfExporter {
/// Export a typesetted document into a writer. Returns how many bytes were written.
#[inline]
pub fn export<W: Write>(&self, document: &Document, loader: &FontLoader, target: W)
pub fn export<W: Write>(&self, document: &Document, loader: &SharedFontLoader, target: W)
-> PdfResult<usize> {
let mut engine = PdfEngine::new(document, loader, target)?;
engine.write()
@ -34,13 +39,12 @@ impl PdfExporter {
}
/// Writes documents in the _PDF_ format.
#[derive(Debug)]
struct PdfEngine<'d, W: Write> {
writer: PdfWriter<W>,
doc: &'d Document,
offsets: Offsets,
font_remap: HashMap<usize, usize>,
fonts: Vec<PdfFont>,
fonts: Vec<OwnedFont>,
}
/// Offsets for the various groups of ids.
@ -55,7 +59,7 @@ struct Offsets {
impl<'d, W: Write> PdfEngine<'d, W> {
/// Create a new _PDF_ engine.
fn new(doc: &'d Document, loader: &FontLoader, target: W) -> PdfResult<PdfEngine<'d, W>> {
fn new(doc: &'d Document, loader: &SharedFontLoader, target: W) -> PdfResult<PdfEngine<'d, W>> {
// Create a subsetted PDF font for each font in the document.
let mut font_remap = HashMap::new();
let fonts = {
@ -82,11 +86,22 @@ impl<'d, W: Write> PdfEngine<'d, W> {
}
// Collect the fonts into a vector in the order of the values in the remapping.
let mut loader = loader.borrow_mut();
let mut order = font_remap.iter().map(|(&old, &new)| (old, new)).collect::<Vec<_>>();
order.sort_by_key(|&(_, new)| new);
order.into_iter()
.map(|(old, _)| PdfFont::new(&loader.get_with_index(old), &chars[&old]))
.collect::<PdfResult<Vec<_>>>()?
let mut fonts = vec![];
for (index, _) in order {
let font = loader.get_with_index(index);
let subsetted = font.subsetted(
chars[&index].iter().cloned(),
&["name", "OS/2", "post", "head", "hhea", "hmtx", "maxp",
"cmap", "cvt ", "fpgm", "prep", "loca", "glyf"][..]
)?;
fonts.push(OwnedFont::from_bytes(subsetted)?);
}
fonts
};
// Calculate a unique id for all objects that will be written.
@ -108,12 +123,12 @@ impl<'d, W: Write> PdfEngine<'d, W> {
/// Write the complete document.
fn write(&mut self) -> PdfResult<usize> {
self.writer.write_header(&Version::new(1, 7))?;
self.writer.write_header(Version::new(1, 7))?;
self.write_page_tree()?;
self.write_pages()?;
self.write_fonts()?;
self.writer.write_xref_table()?;
self.writer.write_trailer(&Trailer::new(self.offsets.catalog))?;
self.writer.write_trailer(Trailer::new(self.offsets.catalog))?;
Ok(self.writer.written())
}
@ -185,7 +200,7 @@ impl<'d, W: Write> PdfEngine<'d, W> {
}
// Write the text.
text.tj(self.fonts[active_font.0].encode_text(&string));
text.tj(self.fonts[active_font.0].encode_text(&string)?);
},
}
}
@ -199,8 +214,11 @@ impl<'d, W: Write> PdfEngine<'d, W> {
fn write_fonts(&mut self) -> PdfResult<()> {
let mut id = self.offsets.fonts.0;
for font in &self.fonts {
let base_font = format!("ABCDEF+{}", font.name);
for font in &mut self.fonts {
let name = font.read_table::<Name>()?
.get_decoded(NameEntry::PostScriptName)
.unwrap_or_else(|| "unknown".to_string());
let base_font = format!("ABCDEF+{}", name);
// Write the base font object referencing the CID font.
self.writer.write_obj(id,
@ -211,39 +229,77 @@ impl<'d, W: Write> PdfEngine<'d, W> {
).to_unicode(id + 3)
)?;
let system_info = CIDSystemInfo::new("Adobe", "Identity", 0);
// Extract information from the head table.
let head = font.read_table::<Header>()?;
let font_unit_ratio = 1.0 / (head.units_per_em as f32);
let font_unit_to_size = |x| Size::pt(font_unit_ratio * x);
let font_unit_to_glyph_unit = |fu| {
let size = font_unit_to_size(fu);
(1000.0 * size.to_pt()).round() as GlyphUnit
};
let italic = head.mac_style.contains(MacStyleFlags::ITALIC);
let bounding_box = Rect::new(
font_unit_to_glyph_unit(head.x_min as f32),
font_unit_to_glyph_unit(head.y_min as f32),
font_unit_to_glyph_unit(head.x_max as f32),
font_unit_to_glyph_unit(head.y_max as f32),
);
// Transform the width into PDF units.
let widths: Vec<_> = font
.read_table::<HorizontalMetrics>()?
.metrics.iter()
.map(|m| font_unit_to_glyph_unit(m.advance_width as f32))
.collect();
// Write the CID font referencing the font descriptor.
let system_info = CIDSystemInfo::new("Adobe", "Identity", 0);
self.writer.write_obj(id + 1,
CIDFont::new(
CIDFontType::Type2,
base_font.clone(),
system_info.clone(),
id + 2,
).widths(vec![WidthRecord::start(0, font.widths.clone())])
).widths(vec![WidthRecord::start(0, widths)])
)?;
// Extract information from the post table.
let post = font.read_table::<Post>()?;
let fixed_pitch = post.is_fixed_pitch;
let italic_angle = post.italic_angle.to_f32();
// Build the flag set.
let mut flags = FontFlags::empty();
flags.set(FontFlags::SERIF, name.contains("Serif"));
flags.set(FontFlags::FIXED_PITCH, fixed_pitch);
flags.set(FontFlags::ITALIC, italic);
flags.insert(FontFlags::SYMBOLIC);
flags.insert(FontFlags::SMALL_CAP);
// Extract information from the OS/2 table.
let os2 = font.read_table::<OS2>()?;
// Write the font descriptor (contains the global information about the font).
self.writer.write_obj(id + 2,
FontDescriptor::new(
base_font,
font.flags,
font.italic_angle,
)
.font_bbox(font.bounding_box)
.ascent(font.ascender)
.descent(font.descender)
.cap_height(font.cap_height)
.stem_v(font.stem_v)
.font_file_2(id + 4)
FontDescriptor::new(base_font, flags, italic_angle)
.font_bbox(bounding_box)
.ascent(font_unit_to_glyph_unit(os2.s_typo_ascender as f32))
.descent(font_unit_to_glyph_unit(os2.s_typo_descender as f32))
.cap_height(font_unit_to_glyph_unit(os2.s_cap_height.unwrap_or(os2.s_typo_ascender) as f32))
.stem_v((10.0 + 0.244 * (os2.us_weight_class as f32 - 50.0)) as GlyphUnit)
.font_file_2(id + 4)
)?;
// Write the CMap, which maps glyphs to unicode codepoints.
let mapping = font.font.mapping.iter().map(|(&c, &cid)| (cid, c));
let mapping = font.read_table::<CharMap>()?
.mapping.iter()
.map(|(&c, &cid)| (cid, c));
self.writer.write_obj(id + 3, &CMap::new("Custom", system_info, mapping))?;
// Finally write the subsetted font program.
self.writer.write_obj(id + 4, &FontStream::new(&font.program))?;
self.writer.write_obj(id + 4, &FontStream::new(font.data().get_ref()))?;
id += 5;
}
@ -257,78 +313,6 @@ fn ids((start, end): (Ref, Ref)) -> impl Iterator<Item=Ref> {
start ..= end
}
/// The data we need from the font.
#[derive(Debug, Clone)]
struct PdfFont {
font: Font,
widths: Vec<GlyphUnit>,
flags: FontFlags,
italic_angle: f32,
bounding_box: Rect<GlyphUnit>,
ascender: GlyphUnit,
descender: GlyphUnit,
cap_height: GlyphUnit,
stem_v: GlyphUnit,
}
impl PdfFont {
/// Create a subetted version of the font and calculate some information
/// needed for creating the _PDF_.
fn new(font: &Font, chars: &HashSet<char>) -> PdfResult<PdfFont> {
/// Convert a size into a _PDF_ glyph unit.
fn size_to_glyph_unit(size: Size) -> GlyphUnit {
(1000.0 * size.to_pt()).round() as GlyphUnit
}
let subset_result = font.subsetted(
chars.iter().cloned(),
&["head", "hhea", "hmtx", "maxp", "cmap", "cvt ", "fpgm", "prep", "loca", "glyf"][..]
);
// Check if the subsetting was successful and if it could not handle this
// font we just copy it plainly.
let subsetted = match subset_result {
Ok(font) => font,
Err(FontError::UnsupportedFont(_)) => font.clone(),
Err(err) => return Err(err.into()),
};
let mut flags = FontFlags::empty();
flags.set(FontFlags::FIXED_PITCH, font.metrics.monospace);
flags.set(FontFlags::SERIF, font.name.contains("Serif"));
flags.insert(FontFlags::SYMBOLIC);
flags.set(FontFlags::ITALIC, font.metrics.italic);
flags.insert(FontFlags::SMALL_CAP);
let widths = subsetted.widths.iter().map(|&x| size_to_glyph_unit(x)).collect();
Ok(PdfFont {
font: subsetted,
widths,
flags,
italic_angle: font.metrics.italic_angle,
bounding_box: Rect::new(
size_to_glyph_unit(font.metrics.bounding_box[0]),
size_to_glyph_unit(font.metrics.bounding_box[1]),
size_to_glyph_unit(font.metrics.bounding_box[2]),
size_to_glyph_unit(font.metrics.bounding_box[3]),
),
ascender: size_to_glyph_unit(font.metrics.ascender),
descender: size_to_glyph_unit(font.metrics.descender),
cap_height: size_to_glyph_unit(font.metrics.cap_height),
stem_v: (10.0 + 0.244 * (font.metrics.weight_class as f32 - 50.0)) as GlyphUnit,
})
}
}
impl std::ops::Deref for PdfFont {
type Target = Font;
fn deref(&self) -> &Font {
&self.font
}
}
/// The error type for _PDF_ creation.
pub enum PdfExportError {
/// An error occured while subsetting the font for the _PDF_.

View File

@ -1,165 +0,0 @@
//! Loading of fonts matching queries.
use std::cell::{RefCell, Ref};
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use super::{Font, FontInfo, FontClass, FontProvider};
/// Serves fonts matching queries.
pub struct FontLoader<'p> {
/// The font providers.
providers: Vec<Box<dyn FontProvider + 'p>>,
/// The internal state. Uses interior mutability because the loader works behind
/// an immutable reference to ease usage.
state: RefCell<FontLoaderState>,
}
/// Internal state of the font loader (seperated to wrap it in a `RefCell`).
struct FontLoaderState {
/// The loaded fonts alongside their external indices. Some fonts may not
/// have external indices because they were loaded but did not contain the
/// required character. However, these are still stored because they may
/// be needed later. The index is just set to `None` then.
fonts: Vec<(Option<usize>, Font)>,
/// Allows to retrieve a font (index) quickly if a query was submitted before.
query_cache: HashMap<FontQuery, usize>,
/// Allows to re-retrieve loaded fonts by their info instead of loading them again.
info_cache: HashMap<FontInfo, usize>,
/// Indexed by external indices (the ones inside the tuples in the `fonts` vector)
/// and maps to internal indices (the actual indices into the vector).
inner_index: Vec<usize>,
}
impl<'p> FontLoader<'p> {
/// Create a new font loader.
pub fn new() -> FontLoader<'p> {
FontLoader {
providers: vec![],
state: RefCell::new(FontLoaderState {
query_cache: HashMap::new(),
info_cache: HashMap::new(),
inner_index: vec![],
fonts: vec![],
}),
}
}
/// Add a font provider to this loader.
pub fn add_font_provider<P: FontProvider + 'p>(&mut self, provider: P) {
self.providers.push(Box::new(provider));
}
/// Returns the font (and its index) best matching the query, if there is any.
pub fn get(&self, query: FontQuery) -> Option<(usize, Ref<Font>)> {
// Load results from the cache, if we had the exact same query before.
let state = self.state.borrow();
if let Some(&index) = state.query_cache.get(&query) {
// The font must have an external index already because it is in the query cache.
// It has been served before.
let extern_index = state.fonts[index].0.unwrap();
let font = Ref::map(state, |s| &s.fonts[index].1);
return Some((extern_index, font));
}
drop(state);
// The outermost loop goes over the fallbacks because we want to serve the
// font that matches the first possible class.
for class in &query.fallback {
// For each class now go over all fonts from all font providers.
for provider in &self.providers {
for info in provider.available().iter() {
let viable = info.classes.contains(class);
let matches = viable && query.classes.iter()
.all(|class| info.classes.contains(class));
if matches {
let mut state = self.state.borrow_mut();
// Check if we have already loaded this font before, otherwise,
// we will load it from the provider.
let index = if let Some(&index) = state.info_cache.get(info) {
index
} else if let Some(mut source) = provider.get(info) {
let mut program = Vec::new();
source.read_to_end(&mut program).ok()?;
let font = Font::new(program).ok()?;
// Insert it into the storage and cache it by its info.
let index = state.fonts.len();
state.info_cache.insert(info.clone(), index);
state.fonts.push((None, font));
index
} else {
// Strangely, this provider lied and cannot give us the promised font.
continue;
};
// Proceed if this font has the character we need.
let has_char = state.fonts[index].1.mapping.contains_key(&query.character);
if has_char {
// This font is suitable, thus we cache the query result.
state.query_cache.insert(query, index);
// Now we have to find out the external index of it or assign
// a new one if it has none.
let external_index = state.fonts[index].0.unwrap_or_else(|| {
// We have to assign an external index before serving.
let new_index = state.inner_index.len();
state.inner_index.push(index);
state.fonts[index].0 = Some(new_index);
new_index
});
// Release the mutable borrow to be allowed to borrow immutably.
drop(state);
// Finally, get a reference to the actual font.
let font = Ref::map(self.state.borrow(), |s| &s.fonts[index].1);
return Some((external_index, font));
}
}
}
}
}
// Not a single match!
None
}
/// Return the font previously loaded at this index.
/// Panics if the index is not assigned.
#[inline]
pub fn get_with_index(&self, index: usize) -> Ref<Font> {
let state = self.state.borrow();
let internal = state.inner_index[index];
Ref::map(state, |s| &s.fonts[internal].1)
}
}
impl Debug for FontLoader<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let state = self.state.borrow();
f.debug_struct("FontLoader")
.field("providers", &self.providers.len())
.field("fonts", &state.fonts)
.field("query_cache", &state.query_cache)
.field("info_cache", &state.info_cache)
.field("inner_index", &state.inner_index)
.finish()
}
}
/// A query for a font with specific properties.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct FontQuery {
/// Which character is needed.
pub character: char,
/// Which classes the font has to be part of.
pub classes: Vec<FontClass>,
/// The font matching the leftmost class in this sequence should be returned.
pub fallback: Vec<FontClass>,
}

View File

@ -1,448 +0,0 @@
//! Font loading and subsetting.
//!
//! # Font handling
//! To do the typesetting, the engine needs font data. However, to be highly portable the engine
//! itself assumes nothing about the environment. To still work with fonts, the consumer of this
//! library has to add _font providers_ to their typesetting instance. These can be queried for font
//! data given flexible font filters specifying required font families and styles. A font provider
//! is a type implementing the [`FontProvider`](crate::font::FontProvider) trait.
//!
//! There is one [included font provider](crate::font::FileSystemFontProvider) that serves fonts
//! from a folder on the file system.
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{self, Cursor, Read, Seek, BufReader};
use std::path::{Path, PathBuf};
use opentype::{Error as OpentypeError, OpenTypeReader};
use opentype::tables::{Header, Name, CharMap, HorizontalMetrics, Post, OS2};
use opentype::types::{MacStyleFlags, NameEntry};
use toml::map::Map as TomlMap;
use toml::value::Value as TomlValue;
use self::subset::Subsetter;
use crate::size::Size;
mod loader;
mod subset;
pub use loader::{FontLoader, FontQuery};
/// A parsed _OpenType_ font program.
#[derive(Debug, Clone)]
pub struct Font {
/// The name of the font.
pub name: String,
/// The complete, raw bytes of the font program.
pub program: Vec<u8>,
/// The mapping from character codes to glyph ids.
pub mapping: HashMap<char, u16>,
/// The widths of the glyphs indexed by glyph id.
pub widths: Vec<Size>,
/// The id of the fallback glyph.
pub default_glyph: u16,
/// The typesetting or exporting-relevant metrics of this font.
pub metrics: FontMetrics,
}
/// Font metrics relevant to the typesetting or exporting processes.
#[derive(Debug, Copy, Clone)]
pub struct FontMetrics {
/// Whether the font is italic.
pub italic: bool,
/// Whether font is monospace.
pub monospace: bool,
/// The angle of text in italics (in counter-clockwise degrees from vertical).
pub italic_angle: f32,
/// The extremal values [x_min, y_min, x_max, y_max] for all glyph bounding boxes.
pub bounding_box: [Size; 4],
/// The typographic ascender.
pub ascender: Size,
/// The typographic descender.
pub descender: Size,
/// The approximate height of capital letters.
pub cap_height: Size,
/// The weight class of the font (from 100 for thin to 900 for heavy).
pub weight_class: u16,
}
impl Font {
/// Create a `Font` from a raw font program.
pub fn new(program: Vec<u8>) -> FontResult<Font> {
let cursor = Cursor::new(&program);
let mut reader = OpenTypeReader::new(cursor);
// All of these tables are required by the OpenType specification,
// so we do not really have to handle the case that they are missing.
let head = reader.read_table::<Header>()?;
let name = reader.read_table::<Name>()?;
let os2 = reader.read_table::<OS2>()?;
let cmap = reader.read_table::<CharMap>()?;
let hmtx = reader.read_table::<HorizontalMetrics>()?;
let post = reader.read_table::<Post>()?;
// Create a conversion function between font units and sizes.
let font_unit_ratio = 1.0 / (head.units_per_em as f32);
let font_unit_to_size = |x| Size::pt(font_unit_ratio * x);
let font_name = name
.get_decoded(NameEntry::PostScriptName)
.unwrap_or_else(|| "unknown".to_owned());
let widths = hmtx.metrics.iter()
.map(|m| font_unit_to_size(m.advance_width as f32)).collect();
let metrics = FontMetrics {
italic: head.mac_style.contains(MacStyleFlags::ITALIC),
monospace: post.is_fixed_pitch,
italic_angle: post.italic_angle.to_f32(),
bounding_box: [
font_unit_to_size(head.x_min as f32),
font_unit_to_size(head.y_min as f32),
font_unit_to_size(head.x_max as f32),
font_unit_to_size(head.y_max as f32),
],
ascender: font_unit_to_size(os2.s_typo_ascender as f32),
descender: font_unit_to_size(os2.s_typo_descender as f32),
cap_height: font_unit_to_size(os2.s_cap_height.unwrap_or(os2.s_typo_ascender) as f32),
weight_class: os2.us_weight_class,
};
Ok(Font {
name: font_name,
program,
mapping: cmap.mapping,
widths,
default_glyph: os2.us_default_char.unwrap_or(0),
metrics,
})
}
/// Encode a character into it's glyph id.
#[inline]
pub fn encode(&self, character: char) -> u16 {
self.mapping.get(&character).map(|&g| g).unwrap_or(self.default_glyph)
}
/// Encode the given text into a vector of glyph ids.
#[inline]
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() {
let glyph = self.encode(c);
bytes.push((glyph >> 8) as u8);
bytes.push((glyph & 0xff) as u8);
}
bytes
}
/// Generate a subsetted version of this font.
///
/// This version includes only the given `chars` and _OpenType_ `tables`.
#[inline]
pub fn subsetted<C, I, S>(&self, chars: C, tables: I) -> Result<Font, FontError>
where
C: IntoIterator<Item=char>,
I: IntoIterator<Item=S>,
S: AsRef<str>
{
Subsetter::subset(self, chars, tables)
}
}
/// A type that provides fonts.
pub trait FontProvider {
/// Returns a font with the given info if this provider has one.
fn get(&self, info: &FontInfo) -> Option<Box<dyn FontData>>;
/// The available fonts this provider can serve. While these should generally
/// be retrievable through the `get` method, this is not guaranteed.
fn available<'p>(&'p self) -> &'p [FontInfo];
}
/// A wrapper trait around `Read + Seek`.
///
/// This type is needed because currently you can't make a trait object with two traits, like
/// `Box<dyn Read + Seek>`. Automatically implemented for all types that are [`Read`] and [`Seek`].
pub trait FontData: Read + Seek {}
impl<T: Read + Seek> FontData for T {}
/// Classifies a font by listing the font classes it is part of.
///
/// All fonts with the same [`FontInfo`] are part of the same intersection
/// of [font classes](FontClass).
///
/// This structure can be constructed conveniently through the [`font`] macro.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct FontInfo {
/// The font classes this font is part of.
pub classes: Vec<FontClass>,
}
impl FontInfo {
/// Create a new font info from a collection of classes.
#[inline]
pub fn new<I>(classes: I) -> FontInfo where I: IntoIterator<Item=FontClass> {
FontInfo {
classes: classes.into_iter().collect()
}
}
}
/// A class of fonts.
///
/// The set of all fonts can be classified into subsets of font classes like
/// _serif_ or _bold_. This enum lists such subclasses.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum FontClass {
Serif,
SansSerif,
Monospace,
Regular,
Bold,
Italic,
/// A custom family like _Arial_ or _Times_.
Family(String),
}
/// A macro to create [FontInfos](crate::font::FontInfo) easily.
///
/// Accepts an ordered list of font classes. Strings expressions are parsed
/// into custom `Family`-variants and others can be named directly.
///
/// # Examples
/// ```
/// # use typeset::font;
/// // Noto Sans in regular typeface.
/// font!["NotoSans", "Noto", Regular, SansSerif];
///
/// // Noto Serif in italics and boldface.
/// font!["NotoSerif", "Noto", Bold, Italic, Serif];
///
/// // Arial in italics.
/// font!["Arial", Italic, SansSerif];
///
/// // Noto Emoji, which works in sans-serif and serif contexts.
/// font!["NotoEmoji", "Noto", Regular, SansSerif, Serif, Monospace];
/// ```
#[macro_export]
macro_rules! font {
// Parse class list one by one.
(@__cls $v:expr) => {};
(@__cls $v:expr, $c:ident) => { $v.push($crate::font::FontClass::$c); };
(@__cls $v:expr, $c:ident, $($tts:tt)*) => {
font!(@__cls $v, $c);
font!(@__cls $v, $($tts)*)
};
(@__cls $v:expr, $f:expr) => { $v.push( $crate::font::FontClass::Family($f.to_string())); };
(@__cls $v:expr, $f:expr, $($tts:tt)*) => {
font!(@__cls $v, $f);
font!(@__cls $v, $($tts)*)
};
// Entry point
($($tts:tt)*) => {{
let mut classes = Vec::new();
font!(@__cls classes, $($tts)*);
$crate::font::FontInfo { classes }
}};
}
/// A font provider serving fonts from a folder on the local file system.
#[derive(Debug)]
pub struct FileSystemFontProvider {
/// The base folder all other paths are relative to.
base: PathBuf,
/// Paths of the fonts relative to the `base` path.
paths: Vec<PathBuf>,
/// The info for the font with the same index in `paths`.
infos: Vec<FontInfo>,
}
impl FileSystemFontProvider {
/// Create a new provider serving fonts from a base path. The `fonts` iterator
/// should contain paths of fonts relative to the base alongside matching
/// infos for these fonts.
///
/// # Example
/// Serve the two fonts `NotoSans-Regular` and `NotoSans-Italic` from the local folder
/// `../fonts`.
/// ```
/// # use typeset::{font::FileSystemFontProvider, font};
/// FileSystemFontProvider::new("../fonts", vec![
/// ("NotoSans-Regular.ttf", font!["NotoSans", Regular, SansSerif]),
/// ("NotoSans-Italic.ttf", font!["NotoSans", Italic, SansSerif]),
/// ]);
/// ```
pub fn new<B, I, P>(base: B, fonts: I) -> FileSystemFontProvider
where
B: Into<PathBuf>,
I: IntoIterator<Item = (P, FontInfo)>,
P: Into<PathBuf>,
{
let iter = fonts.into_iter();
// Find out how long the iterator is at least, to reserve the correct
// capacity for the vectors.
let min = iter.size_hint().0;
let mut paths = Vec::with_capacity(min);
let mut infos = Vec::with_capacity(min);
for (path, info) in iter {
paths.push(path.into());
infos.push(info);
}
FileSystemFontProvider {
base: base.into(),
paths,
infos,
}
}
/// Create a new provider from a font listing file.
pub fn from_listing<P: AsRef<Path>>(file: P) -> FontResult<FileSystemFontProvider> {
fn inv<S: ToString>(message: S) -> FontError {
FontError::InvalidListing(message.to_string())
}
let file = file.as_ref();
let base = file.parent()
.ok_or_else(|| inv("expected listings file"))?;
let bytes = fs::read(file)?;
let map: TomlMap<String, toml::Value> = toml::de::from_slice(&bytes)
.map_err(|err| inv(err))?;
let mut paths = Vec::new();
let mut infos = Vec::new();
for value in map.values() {
if let TomlValue::Table(table) = value {
// Parse the string file key.
paths.push(match table.get("file") {
Some(TomlValue::String(s)) => PathBuf::from(s),
_ => return Err(inv("expected file name")),
});
// Parse the array<string> classes key.
infos.push(if let Some(TomlValue::Array(array)) = table.get("classes") {
let mut classes = Vec::with_capacity(array.len());
for class in array {
classes.push(match class {
TomlValue::String(class) => match class.as_str() {
"Serif" => FontClass::Serif,
"SansSerif" => FontClass::SansSerif,
"Monospace" => FontClass::Monospace,
"Regular" => FontClass::Regular,
"Bold" => FontClass::Bold,
"Italic" => FontClass::Italic,
_ => FontClass::Family(class.to_string()),
},
_ => return Err(inv("expect font class string")),
})
}
FontInfo { classes }
} else {
return Err(inv("expected font classes"));
});
} else {
return Err(inv("expected file/classes table"));
}
}
Ok(FileSystemFontProvider {
base: base.to_owned(),
paths,
infos,
})
}
}
impl FontProvider for FileSystemFontProvider {
#[inline]
fn get(&self, info: &FontInfo) -> Option<Box<dyn FontData>> {
let index = self.infos.iter().position(|c| c == info)?;
let path = &self.paths[index];
let full_path = self.base.join(path);
let file = File::open(full_path).ok()?;
Some(Box::new(BufReader::new(file)) as Box<dyn FontData>)
}
#[inline]
fn available<'p>(&'p self) -> &'p [FontInfo] {
&self.infos
}
}
/// The error type for font operations.
pub enum FontError {
/// The font file is incorrect.
InvalidFont(String),
/// The font listing is incorrect.
InvalidListing(String),
/// A character requested for subsetting was not present in the source font.
MissingCharacter(char),
/// A requested or required table was not present.
MissingTable(String),
/// The table is unknown to the subsetting engine.
UnsupportedTable(String),
/// The font is not supported by the subsetting engine.
UnsupportedFont(String),
/// An I/O Error occured while reading the font program.
Io(io::Error),
}
error_type! {
err: FontError,
res: FontResult,
show: f => match err {
FontError::InvalidFont(message) => write!(f, "invalid font: {}", message),
FontError::InvalidListing(message) => write!(f, "invalid font listing: {}", message),
FontError::MissingCharacter(c) => write!(f, "missing character: '{}'", c),
FontError::MissingTable(table) => write!(f, "missing table: '{}'", table),
FontError::UnsupportedTable(table) => write!(f, "unsupported table: {}", table),
FontError::UnsupportedFont(message) => write!(f, "unsupported font: {}", message),
FontError::Io(err) => write!(f, "io error: {}", err),
},
source: match err {
FontError::Io(err) => Some(err),
_ => None,
},
from: (io::Error, FontError::Io(err)),
from: (OpentypeError, match err {
OpentypeError::InvalidFont(message) => FontError::InvalidFont(message),
OpentypeError::MissingTable(tag) => FontError::MissingTable(tag.to_string()),
OpentypeError::Io(err) => FontError::Io(err),
}),
}
#[cfg(test)]
mod tests {
use super::*;
/// Tests the font info macro.
#[test]
fn font_macro() {
use FontClass::*;
assert_eq!(font!["NotoSans", "Noto", Regular, SansSerif], FontInfo {
classes: vec![
Family("NotoSans".to_owned()), Family("Noto".to_owned()),
Regular, SansSerif
]
});
assert_eq!(font!["NotoSerif", Serif, Italic, "Noto"], FontInfo {
classes: vec![
Family("NotoSerif".to_owned()), Serif, Italic,
Family("Noto".to_owned())
],
});
}
}

View File

@ -1,562 +0,0 @@
//! Subsetting of opentype fonts.
use std::collections::HashMap;
use std::io::{Cursor, Seek, SeekFrom};
use byteorder::{BE, ReadBytesExt, WriteBytesExt};
use opentype::{OpenTypeReader, Outlines, Table, TableRecord, Tag};
use opentype::tables::{Header, CharMap, Locations, HorizontalMetrics, Glyphs};
use crate::size::Size;
use super::{Font, FontError, FontResult};
/// Subsets a font.
#[derive(Debug)]
pub struct Subsetter<'a> {
// The original font
font: &'a Font,
reader: OpenTypeReader<Cursor<&'a [u8]>>,
outlines: Outlines,
tables: Vec<TableRecord>,
glyphs: Vec<u16>,
// The subsetted font
chars: Vec<char>,
records: Vec<TableRecord>,
body: Vec<u8>,
}
impl<'a> Subsetter<'a> {
/// Subset a font. See [`Font::subetted`] for more details.
pub fn subset<C, I, S>(font: &Font, chars: C, tables: I) -> Result<Font, FontError>
where
C: IntoIterator<Item=char>,
I: IntoIterator<Item=S>,
S: AsRef<str>
{
let mut reader = OpenTypeReader::from_slice(&font.program);
let outlines = reader.outlines()?;
let table_records = reader.tables()?.to_vec();
let chars: Vec<_> = chars.into_iter().collect();
let subsetter = Subsetter {
font,
reader,
outlines,
tables: table_records,
glyphs: Vec::with_capacity(1 + chars.len()),
chars,
records: vec![],
body: vec![],
};
subsetter.run(tables)
}
/// Do the subsetting.
fn run<I, S>(mut self, tables: I) -> FontResult<Font>
where I: IntoIterator<Item=S>, S: AsRef<str> {
if self.outlines == Outlines::CFF {
return Err(FontError::UnsupportedFont("CFF outlines".to_string()));
}
// Find out which glyphs to include based on which characters we want and
// which glyphs are additionally used by composite glyphs.
self.find_glyphs()?;
// Copy/subset all the tables the caller wants.
for table in tables.into_iter() {
let tag = table.as_ref().parse()
.map_err(|_| FontError::UnsupportedTable(table.as_ref().to_string()))?;
if self.contains_table(tag) {
self.subset_table(tag)?;
}
}
// Preprend the new header to the body. We have to do this last, because
// we only have the necessary information now.
self.write_header()?;
Ok(Font {
name: self.font.name.clone(),
mapping: self.compute_mapping(),
widths: self.compute_widths()?,
program: self.body,
default_glyph: self.font.default_glyph,
metrics: self.font.metrics,
})
}
/// Store all glyphs the subset shall contain into `self.glyphs`.
fn find_glyphs(&mut self) -> FontResult<()> {
if self.outlines == Outlines::TrueType {
let char_map = self.read_table::<CharMap>()?;
let glyf = self.read_table::<Glyphs>()?;
// The default glyph should always be at index 0.
self.glyphs.push(self.font.default_glyph);
for &c in &self.chars {
let glyph = char_map.get(c).ok_or_else(|| FontError::MissingCharacter(c))?;
self.glyphs.push(glyph);
}
// Collect the glyphs not used mapping from characters but used in
// composite glyphs, too.
let mut i = 0;
while i < self.glyphs.len() as u16 {
let glyph_id = self.glyphs[i as usize];
let glyph = glyf.get(glyph_id).take_invalid("missing glyf entry")?;
for &composite in &glyph.composites {
if self.glyphs.iter().rev().all(|&x| x != composite) {
self.glyphs.push(composite);
}
}
i += 1;
}
} else {
unimplemented!()
}
Ok(())
}
/// Prepend the new header to the constructed body.
fn write_header(&mut self) -> FontResult<()> {
// Create an output buffer
const BASE_HEADER_LEN: usize = 12;
const TABLE_RECORD_LEN: usize = 16;
let header_len = BASE_HEADER_LEN + self.records.len() * TABLE_RECORD_LEN;
let mut header = Vec::with_capacity(header_len);
let num_tables = self.records.len() as u16;
let mut max_power = 1u16;
while max_power * 2 <= num_tables {
max_power *= 2;
}
max_power = std::cmp::min(max_power, num_tables);
let search_range = max_power * 16;
let entry_selector = (max_power as f32).log2() as u16;
let range_shift = num_tables * 16 - search_range;
// Write the base OpenType header
header.write_u32::<BE>(match self.outlines {
Outlines::TrueType => 0x00010000,
Outlines::CFF => 0x4f54544f,
})?;
header.write_u16::<BE>(num_tables)?;
header.write_u16::<BE>(search_range)?;
header.write_u16::<BE>(entry_selector)?;
header.write_u16::<BE>(range_shift)?;
// Write the table records
for record in &self.records {
header.extend(record.tag.value());
header.write_u32::<BE>(record.check_sum)?;
header.write_u32::<BE>(header_len as u32 + record.offset)?;
header.write_u32::<BE>(record.length)?;
}
// Prepend the fresh header to the body.
header.append(&mut self.body);
self.body = header;
Ok(())
}
/// Compute the new subsetted widths vector.
fn compute_widths(&self) -> FontResult<Vec<Size>> {
let mut widths = Vec::with_capacity(self.glyphs.len());
for &glyph in &self.glyphs {
let &width = self.font.widths.get(glyph as usize)
.take_invalid("missing glyph width")?;
widths.push(width);
}
Ok(widths)
}
/// Compute the new character to glyph id mapping.
fn compute_mapping(&self) -> HashMap<char, u16> {
// The mapping is basically just the index into the char vector, but we add one
// to each index here because we added the default glyph to the front.
self.chars.iter().enumerate()
.map(|(i, &c)| (c, 1 + i as u16))
.collect::<HashMap<char, u16>>()
}
/// Subset and write the table with the given tag to the output.
fn subset_table(&mut self, tag: Tag) -> FontResult<()> {
match tag.value() {
// These tables can just be copied.
b"head" | b"name" | b"OS/2" |
b"cvt " | b"fpgm" | b"prep" | b"gasp" => self.copy_table(tag),
// These tables have more complex subsetting routines.
b"hhea" => self.subset_hhea(),
b"hmtx" => self.subset_hmtx(),
b"maxp" => self.subset_maxp(),
b"post" => self.subset_post(),
b"cmap" => self.subset_cmap(),
b"glyf" => self.subset_glyf(),
b"loca" => self.subset_loca(),
_ => Err(FontError::UnsupportedTable(tag.to_string()))
}
}
/// Copy the table body without modification.
fn copy_table(&mut self, tag: Tag) -> FontResult<()> {
self.write_table_body(tag, |this| {
let table = this.read_table_data(tag)?;
Ok(this.body.extend(table))
})
}
/// Subset the `hhea` table by changing the number of horizontal metrics in it.
fn subset_hhea(&mut self) -> FontResult<()> {
let tag = "hhea".parse().unwrap();
let hhea = self.read_table_data(tag)?;
let glyph_count = self.glyphs.len() as u16;
self.write_table_body(tag, |this| {
this.body.extend(&hhea[..hhea.len() - 2]);
this.body.write_u16::<BE>(glyph_count)?;
Ok(())
})
}
/// Subset the `hmtx` table by changing the included metrics.
fn subset_hmtx(&mut self) -> FontResult<()> {
let tag = "hmtx".parse().unwrap();
let hmtx = self.read_table::<HorizontalMetrics>()?;
self.write_table_body(tag, |this| {
for &glyph in &this.glyphs {
let metrics = hmtx.get(glyph).take_invalid("missing glyph metrics")?;
this.body.write_u16::<BE>(metrics.advance_width)?;
this.body.write_i16::<BE>(metrics.left_side_bearing)?;
}
Ok(())
})
}
/// Subset the `maxp` table by changing the glyph count in it.
fn subset_maxp(&mut self) -> FontResult<()> {
let tag = "maxp".parse().unwrap();
let maxp = self.read_table_data(tag)?;
let glyph_count = self.glyphs.len() as u16;
self.write_table_body(tag, |this| {
this.body.extend(&maxp[..4]);
this.body.write_u16::<BE>(glyph_count)?;
Ok(this.body.extend(&maxp[6..]))
})
}
/// Subset the `post` table by removing all name information.
fn subset_post(&mut self) -> FontResult<()> {
let tag = "post".parse().unwrap();
let post = self.read_table_data(tag)?;
self.write_table_body(tag, |this| {
this.body.write_u32::<BE>(0x00030000)?;
Ok(this.body.extend(&post[4..32]))
})
}
/// Subset the `cmap` table by only including the selected characters.
/// Always uses format 12 for simplicity.
fn subset_cmap(&mut self) -> FontResult<()> {
let tag = "cmap".parse().unwrap();
self.write_table_body(tag, |this| {
let mut groups = Vec::new();
// Find out which chars are in consecutive groups.
let mut end = 0;
let len = this.chars.len();
while end < len {
// Compute the end of the consecutive group.
let start = end;
while end + 1 < len && this.chars[end+1] as u32 == this.chars[end] as u32 + 1 {
end += 1;
}
// Add one to the start because we inserted the default glyph in front.
let glyph_id = 1 + start;
groups.push((this.chars[start], this.chars[end], glyph_id));
end += 1;
}
// Write the table header.
this.body.write_u16::<BE>(0)?;
this.body.write_u16::<BE>(1)?;
this.body.write_u16::<BE>(3)?;
this.body.write_u16::<BE>(10)?;
this.body.write_u32::<BE>(12)?;
// Write the subtable header.
this.body.write_u16::<BE>(12)?;
this.body.write_u16::<BE>(0)?;
this.body.write_u32::<BE>((16 + 12 * groups.len()) as u32)?;
this.body.write_u32::<BE>(0)?;
this.body.write_u32::<BE>(groups.len() as u32)?;
// Write the subtable body.
for group in &groups {
this.body.write_u32::<BE>(group.0 as u32)?;
this.body.write_u32::<BE>(group.1 as u32)?;
this.body.write_u32::<BE>(group.2 as u32)?;
}
Ok(())
})
}
/// Subset the `glyf` table by changing the indices of composite glyphs.
fn subset_glyf(&mut self) -> FontResult<()> {
let tag = "glyf".parse().unwrap();
let loca = self.read_table::<Locations>()?;
let glyf = self.read_table_data(tag)?;
self.write_table_body(tag, |this| {
for &glyph in &this.glyphs {
// Find out the location of the glyph in the glyf table.
let start = loca.offset(glyph).take_invalid("missing loca entry")?;
let end = loca.offset(glyph + 1).take_invalid("missing loca entry")?;
// If this glyph has no contours, skip it.
if end == start {
continue;
}
let mut glyph_data = glyf.get(start as usize .. end as usize)
.take_invalid("missing glyph data")?.to_vec();
let mut cursor = Cursor::new(&mut glyph_data);
// This is a composite glyph
let num_contours = cursor.read_i16::<BE>()?;
if num_contours < 0 {
cursor.seek(SeekFrom::Current(8))?;
loop {
let flags = cursor.read_u16::<BE>()?;
let old_glyph_index = cursor.read_u16::<BE>()?;
// Compute the new glyph index by searching for it's index
// in the glyph vector.
let new_glyph_index = this.glyphs.iter()
.position(|&g| g == old_glyph_index)
.take_invalid("invalid composite glyph")? as u16;
// Overwrite the old index with the new one.
cursor.seek(SeekFrom::Current(-2))?;
cursor.write_u16::<BE>(new_glyph_index)?;
// This was the last component
if flags & 0x0020 == 0 {
break;
}
// Skip additional arguments.
let skip = if flags & 1 != 0 { 4 } else { 2 }
+ if flags & 8 != 0 { 2 }
else if flags & 64 != 0 { 4 }
else if flags & 128 != 0 { 8 }
else { 0 };
cursor.seek(SeekFrom::Current(skip))?;
}
}
this.body.extend(glyph_data);
}
Ok(())
})
}
/// Subset the `loca` table by changing to the new offsets.
fn subset_loca(&mut self) -> FontResult<()> {
let format = self.read_table::<Header>()?.index_to_loc_format;
let tag = "loca".parse().unwrap();
let loca = self.read_table::<Locations>()?;
self.write_table_body(tag, |this| {
let mut offset = 0;
for &glyph in &this.glyphs {
if format == 0 {
this.body.write_u16::<BE>((offset / 2) as u16)?;
} else {
this.body.write_u32::<BE>(offset)?;
}
let len = loca.length(glyph).take_invalid("missing loca entry")?;
offset += len;
}
// Write the final offset (so that it is known how long the last glyph is).
if format == 0 {
this.body.write_u16::<BE>((offset / 2) as u16)?;
} else {
this.body.write_u32::<BE>(offset)?;
}
Ok(())
})
}
/// Let a writer write the table body and then store the relevant metadata.
fn write_table_body<F>(&mut self, tag: Tag, writer: F) -> FontResult<()>
where F: FnOnce(&mut Self) -> FontResult<()> {
// Run the writer and capture the length.
let start = self.body.len();
writer(self)?;
let end = self.body.len();
// Pad with zeros.
while (self.body.len() - start) % 4 != 0 {
self.body.push(0);
}
Ok(self.records.push(TableRecord {
tag,
check_sum: calculate_check_sum(&self.body[start..]),
offset: start as u32,
length: (end - start) as u32,
}))
}
/// Whether this font contains a given table.
fn contains_table(&self, tag: Tag) -> bool {
self.tables.binary_search_by_key(&tag, |r| r.tag).is_ok()
}
/// Read a table with the opentype reader.
fn read_table<T: Table>(&mut self) -> FontResult<T> {
self.reader.read_table::<T>().map_err(Into::into)
}
/// Read the raw table data of a table.
fn read_table_data(&self, tag: Tag) -> FontResult<&'a [u8]> {
let record = match self.tables.binary_search_by_key(&tag, |r| r.tag) {
Ok(index) => &self.tables[index],
Err(_) => return Err(FontError::MissingTable(tag.to_string())),
};
self.font.program
.get(record.offset as usize .. (record.offset + record.length) as usize)
.take_invalid("missing table data")
}
}
/// Calculate a checksum over the sliced data as sum of u32's. The data
/// length has to be a multiple of four.
fn calculate_check_sum(data: &[u8]) -> u32 {
let mut sum = 0u32;
data.chunks_exact(4).for_each(|c| {
sum = sum.wrapping_add(
((c[0] as u32) << 24)
+ ((c[1] as u32) << 16)
+ ((c[2] as u32) << 8)
+ (c[3] as u32)
);
});
sum
}
/// Helper trait to create subsetting errors more easily.
trait TakeInvalid<T>: Sized {
/// Pull the type out of self, returning an invalid font
/// error if self was not valid.
fn take_invalid<S: Into<String>>(self, message: S) -> FontResult<T>;
}
impl<T> TakeInvalid<T> for Option<T> {
fn take_invalid<S: Into<String>>(self, message: S) -> FontResult<T> {
self.ok_or(FontError::InvalidFont(message.into()))
}
}
#[cfg(test)]
mod tests {
use std::fs;
use crate::font::Font;
use opentype::{OpenTypeReader, TableRecord};
use opentype::tables::{CharMap, Locations};
const ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz";
/// Stores some tables for inspections.
struct Tables<'a> {
cmap: CharMap,
loca: Locations,
glyf_data: &'a [u8],
}
impl<'a> Tables<'a> {
/// Load the tables from the font.
fn new(font: &'a Font) -> Tables<'a> {
let mut reader = OpenTypeReader::from_slice(&font.program);
let cmap = reader.read_table::<CharMap>().unwrap();
let loca = reader.read_table::<Locations>().unwrap();
let &TableRecord { offset, length, .. } = reader.get_table_record("glyf").unwrap();
let glyf_data = &font.program[offset as usize .. (offset + length) as usize];
Tables { cmap, loca, glyf_data }
}
/// Return the glyph data for the given character.
fn glyph_data(&self, character: char) -> Option<&'a [u8]> {
let glyph = self.cmap.get(character)?;
let start = self.loca.offset(glyph)?;
let end = self.loca.offset(glyph + 1)?;
Some(&self.glyf_data[start as usize .. end as usize])
}
}
/// Return the original and subsetted version of a font with the characters
/// included that are given as the chars of the string.
fn subset(font: &str, chars: &str) -> (Font, Font) {
let program = fs::read(format!("../fonts/{}", font)).unwrap();
let font = Font::new(program).unwrap();
let subsetted = font.subsetted(
chars.chars(),
&["name", "OS/2", "post", "head", "hhea", "hmtx", "maxp", "cmap",
"cvt ", "fpgm", "prep", "gasp", "loca", "glyf"][..]
).unwrap();
(font, subsetted)
}
/// A test that creates a subsetted fonts in the `target` directory
/// for manual inspection.
#[test]
fn manual_files() {
let subsetted = subset("SourceSansPro-Regular.ttf", ALPHABET).1;
fs::write("../target/SourceSansPro-Subsetted.ttf", &subsetted.program).unwrap();
let subsetted = subset("NotoSans-Regular.ttf", ALPHABET).1;
fs::write("../target/NotoSans-Subsetted.ttf", &subsetted.program).unwrap();
}
/// Tests whether the glyph data for specific glyphs match in the original
/// and subsetted version.
#[test]
fn glyph_data() {
let (font, subsetted) = subset("SourceSansPro-Regular.ttf", ALPHABET);
let font_tables = Tables::new(&font);
let subset_tables = Tables::new(&subsetted);
// Go through all characters but skip the composite glyphs.
for c in ALPHABET.chars().filter(|&x| x != 'i' && x != 'j') {
assert_eq!(font_tables.glyph_data(c), subset_tables.glyph_data(c));
}
}
}

View File

@ -3,8 +3,8 @@
use std::any::Any;
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use toddle::query::FontClass;
use crate::font::FontClass;
use crate::layout::{layout, Layout, LayoutContext, LayoutResult};
use crate::layout::flex::FlexLayout;
use crate::parsing::{parse, ParseContext, ParseError, ParseResult};

View File

@ -3,8 +3,10 @@
use std::borrow::Cow;
use std::mem;
use toddle::query::{SharedFontLoader, FontClass};
use toddle::Error as FontError;
use crate::doc::LayoutAction;
use crate::font::{FontLoader, FontClass, FontError};
use crate::size::{Size, Size2D, SizeBox};
use crate::syntax::{SyntaxTree, Node, FuncCall};
use crate::style::TextStyle;
@ -33,10 +35,10 @@ pub fn layout(tree: &SyntaxTree, ctx: LayoutContext) -> LayoutResult<BoxLayout>
}
/// The context for layouting.
#[derive(Debug, Copy, Clone)]
#[derive(Copy, Clone)]
pub struct LayoutContext<'a, 'p> {
/// Loads fonts matching queries.
pub loader: &'a FontLoader<'p>,
pub loader: &'a SharedFontLoader<'p>,
/// Base style to set text with.
pub style: &'a TextStyle,
/// The space to layout in.
@ -66,12 +68,11 @@ impl LayoutSpace {
}
/// Transforms a syntax tree into a box layout.
#[derive(Debug)]
struct Layouter<'a, 'p> {
tree: &'a SyntaxTree,
box_layouter: BoxLayouter,
flex_layout: FlexLayout,
loader: &'a FontLoader<'p>,
loader: &'a SharedFontLoader<'p>,
style: Cow<'a, TextStyle>,
}

View File

@ -1,22 +1,26 @@
//! Layouting of text into boxes.
use toddle::query::{FontQuery, SharedFontLoader};
use toddle::tables::{Header, CharMap, HorizontalMetrics};
use crate::doc::LayoutAction;
use crate::font::FontQuery;
use crate::size::{Size, Size2D};
use super::*;
/// The context for text layouting.
#[derive(Debug, Copy, Clone)]
#[derive(Copy, Clone)]
pub struct TextContext<'a, 'p> {
/// Loads fonts matching queries.
pub loader: &'a FontLoader<'p>,
pub loader: &'a SharedFontLoader<'p>,
/// Base style to set text with.
pub style: &'a TextStyle,
}
/// Layout one piece of text without any breaks as one continous box.
pub fn layout(text: &str, ctx: TextContext) -> LayoutResult<BoxLayout> {
let mut loader = ctx.loader.borrow_mut();
let mut actions = Vec::new();
let mut active_font = std::usize::MAX;
let mut buffer = String::new();
@ -25,14 +29,45 @@ pub fn layout(text: &str, ctx: TextContext) -> LayoutResult<BoxLayout> {
// Walk the characters.
for character in text.chars() {
// Retrieve the best font for this character.
let (index, font) = ctx.loader.get(FontQuery {
classes: ctx.style.classes.clone(),
fallback: ctx.style.fallback.clone(),
character,
}).ok_or_else(|| LayoutError::NoSuitableFont(character))?;
let mut font = None;
let mut classes = ctx.style.classes.clone();
for class in &ctx.style.fallback {
classes.push(class.clone());
font = loader.get(FontQuery {
chars: &[character],
classes: &classes,
});
if font.is_some() {
break;
}
classes.pop();
}
let (font, index) = match font {
Some(f) => f,
None => return Err(LayoutError::NoSuitableFont(character)),
};
// Create a conversion function between font units and sizes.
let font_unit_ratio = 1.0 / (font.read_table::<Header>()?.units_per_em as f32);
let font_unit_to_size = |x| Size::pt(font_unit_ratio * x);
// Add the char width to the total box width.
let char_width = font.widths[font.encode(character) as usize] * ctx.style.font_size;
let glyph = font.read_table::<CharMap>()?
.get(character)
.expect("layout text: font should have char");
let glyph_width = font_unit_to_size(
font.read_table::<HorizontalMetrics>()?
.get(glyph)
.expect("layout text: font should have glyph")
.advance_width as f32
);
let char_width = glyph_width * ctx.style.font_size;
width += char_width;
// Change the font if necessary.

View File

@ -1,6 +1,6 @@
//! The compiler for the _Typeset_ typesetting language 📜.
//! The compiler for the _Typst_ typesetting language.
//!
//! # Compilation
//! # Steps
//! - **Parsing:** The parsing step first transforms a plain string into an [iterator of
//! tokens](crate::parsing::Tokens). Then the [parser](crate::parsing::Parser) operates on that to
//! construct a syntax tree. The structures describing the tree can be found in the [syntax]
@ -11,38 +11,13 @@
//! - **Exporting:** The finished document can then be exported into supported formats. Submodules
//! for the supported formats are located in the [export] module. Currently the only supported
//! format is _PDF_.
//!
//! # Example
//! ```
//! use std::fs::File;
//! use typeset::Typesetter;
//! use typeset::{font::FileSystemFontProvider, font};
//! use typeset::export::pdf::PdfExporter;
//!
//! // Simple example source code.
//! let src = "Hello World from _Typeset_! 🌍";
//!
//! // Create a typesetter with a font provider that provides three fonts
//! // (two sans-serif fonts and a fallback for the emoji).
//! let mut typesetter = Typesetter::new();
//! typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
//! ("CMU-Serif-Regular.ttf", font!["Computer Modern", Regular, Serif]),
//! ("CMU-Serif-Italic.ttf", font!["Computer Modern", Italic, Serif]),
//! ("NotoEmoji-Regular.ttf", font!["Noto", Regular, Serif, SansSerif, Monospace]),
//! ]));
//!
//! // Typeset the document and export it into a PDF file.
//! let document = typesetter.typeset(src).unwrap();
//! # /*
//! let file = File::create("hello-typeset.pdf").unwrap();
//! # */
//! # let file = File::create("../target/typeset-doc-hello.pdf").unwrap();
//! let exporter = PdfExporter::new();
//! exporter.export(&document, typesetter.loader(), file).unwrap();
//! ```
pub extern crate toddle;
use std::cell::RefCell;
use toddle::query::{FontLoader, SharedFontLoader, FontProvider};
use crate::doc::Document;
use crate::font::{FontLoader, FontProvider};
use crate::func::Scope;
use crate::parsing::{parse, ParseContext, ParseResult, ParseError};
use crate::layout::{layout, LayoutContext, LayoutSpace, LayoutError, LayoutResult};
@ -54,8 +29,6 @@ use crate::syntax::SyntaxTree;
mod error;
pub mod doc;
pub mod export;
#[macro_use]
pub mod font;
pub mod func;
pub mod layout;
pub mod parsing;
@ -67,10 +40,9 @@ pub mod syntax;
/// Transforms source code into typesetted documents.
///
/// Can be configured through various methods.
#[derive(Debug)]
pub struct Typesetter<'p> {
/// The font loader shared by all typesetting processes.
loader: FontLoader<'p>,
loader: SharedFontLoader<'p>,
/// The default text style.
text_style: TextStyle,
/// The default page style.
@ -82,7 +54,7 @@ impl<'p> Typesetter<'p> {
#[inline]
pub fn new() -> Typesetter<'p> {
Typesetter {
loader: FontLoader::new(),
loader: RefCell::new(FontLoader::new()),
text_style: TextStyle::default(),
page_style: PageStyle::default(),
}
@ -103,7 +75,7 @@ impl<'p> Typesetter<'p> {
/// Add a font provider to the context of this typesetter.
#[inline]
pub fn add_font_provider<P: 'p>(&mut self, provider: P) where P: FontProvider {
self.loader.add_font_provider(provider);
self.loader.get_mut().add_provider(provider);
}
/// Parse source code into a syntax tree.
@ -135,7 +107,7 @@ impl<'p> Typesetter<'p> {
}
/// A reference to the backing font loader.
pub fn loader(&self) -> &FontLoader<'p> {
pub fn loader(&self) -> &SharedFontLoader<'p> {
&self.loader
}
}
@ -170,12 +142,12 @@ mod test {
use std::io::BufWriter;
use crate::Typesetter;
use crate::export::pdf::PdfExporter;
use crate::font::{FileSystemFontProvider};
use toddle::query::FileSystemFontProvider;
/// Create a _PDF_ with a name from the source code.
fn test(name: &str, src: &str) {
let mut typesetter = Typesetter::new();
let provider = FileSystemFontProvider::from_listing("../fonts/fonts.toml").unwrap();
let provider = FileSystemFontProvider::from_listing("fonts/fonts.toml").unwrap();
typesetter.add_font_provider(provider);
// Typeset into document.

View File

@ -1,6 +1,6 @@
//! Styles for layouting.
use crate::font::FontClass;
use toddle::query::FontClass;
use crate::size::{Size, Size2D, SizeBox};