Extract into separate repository 🧱
This commit is contained in:
parent
b96a7e0cf3
commit
f22a307000
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
things
|
20
Cargo.toml
20
Cargo.toml
@ -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
|
||||
|
@ -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);
|
@ -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);
|
BIN
fonts/CMU-SansSerif-Bold-Italic.ttf
Normal file
BIN
fonts/CMU-SansSerif-Bold-Italic.ttf
Normal file
Binary file not shown.
BIN
fonts/CMU-SansSerif-Bold.ttf
Normal file
BIN
fonts/CMU-SansSerif-Bold.ttf
Normal file
Binary file not shown.
BIN
fonts/CMU-SansSerif-Italic.ttf
Normal file
BIN
fonts/CMU-SansSerif-Italic.ttf
Normal file
Binary file not shown.
BIN
fonts/CMU-SansSerif-Regular.ttf
Normal file
BIN
fonts/CMU-SansSerif-Regular.ttf
Normal file
Binary file not shown.
BIN
fonts/CMU-Serif-Bold-Italic.ttf
Normal file
BIN
fonts/CMU-Serif-Bold-Italic.ttf
Normal file
Binary file not shown.
BIN
fonts/CMU-Serif-Bold.ttf
Normal file
BIN
fonts/CMU-Serif-Bold.ttf
Normal file
Binary file not shown.
BIN
fonts/CMU-Serif-Italic.ttf
Normal file
BIN
fonts/CMU-Serif-Italic.ttf
Normal file
Binary file not shown.
BIN
fonts/CMU-Serif-Regular.ttf
Normal file
BIN
fonts/CMU-Serif-Regular.ttf
Normal file
Binary file not shown.
BIN
fonts/CMU-Typewriter-Bold-Italic.ttf
Normal file
BIN
fonts/CMU-Typewriter-Bold-Italic.ttf
Normal file
Binary file not shown.
BIN
fonts/CMU-Typewriter-Bold.ttf
Normal file
BIN
fonts/CMU-Typewriter-Bold.ttf
Normal file
Binary file not shown.
BIN
fonts/CMU-Typewriter-Italic.ttf
Normal file
BIN
fonts/CMU-Typewriter-Italic.ttf
Normal file
Binary file not shown.
BIN
fonts/CMU-Typewriter-Regular.ttf
Normal file
BIN
fonts/CMU-Typewriter-Regular.ttf
Normal file
Binary file not shown.
103
fonts/License-CMU.txt
Normal file
103
fonts/License-CMU.txt
Normal 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
94
fonts/License-Noto.txt
Normal 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
BIN
fonts/NotoEmoji-Regular.ttf
Normal file
Binary file not shown.
13
fonts/fonts.toml
Normal file
13
fonts/fonts.toml
Normal 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"]
|
@ -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() {
|
||||
|
@ -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_.
|
||||
|
@ -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>,
|
||||
}
|
448
src/font/mod.rs
448
src/font/mod.rs
@ -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())
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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};
|
||||
|
@ -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>,
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
54
src/lib.rs
54
src/lib.rs
@ -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.
|
||||
|
@ -1,6 +1,6 @@
|
||||
//! Styles for layouting.
|
||||
|
||||
use crate::font::FontClass;
|
||||
use toddle::query::FontClass;
|
||||
use crate::size::{Size, Size2D, SizeBox};
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user