Reengineer font and resource loading 🏞

This commit is contained in:
Laurenz 2021-04-23 17:46:14 +02:00
parent 72478946c2
commit 6292d25afb
18 changed files with 972 additions and 336 deletions

View File

@ -22,13 +22,6 @@ jobs:
with:
path: typst
- name: Checkout fontdock
uses: actions/checkout@v2
with:
repository: typst/fontdock
token: ${{ secrets.TYPSTC_ACTION_TOKEN }}
path: fontdock
- name: Checkout pdf-writer
uses: actions/checkout@v2
with:

View File

@ -5,9 +5,9 @@ authors = ["The Typst Project Developers"]
edition = "2018"
[features]
default = ["fs"]
fs = ["fontdock/fs"]
cli = ["fs", "anyhow"]
default = ["cli", "fs"]
cli = ["anyhow", "fs"]
fs = ["dirs", "memmap2", "walkdir"]
[workspace]
members = ["bench"]
@ -21,17 +21,19 @@ debug = 0
opt-level = 2
[dependencies]
fontdock = { path = "../fontdock", features = ["serde"], default-features = false }
image = { version = "0.23", default-features = false, features = ["jpeg", "png"] }
miniz_oxide = "0.3"
pdf-writer = { path = "../pdf-writer" }
rustybuzz = { git = "https://github.com/laurmaedje/rustybuzz" }
serde = { version = "1", features = ["derive"] }
ttf-parser = "0.12"
unicode-bidi = "0.3.5"
unicode-xid = "0.2"
xi-unicode = "0.3"
anyhow = { version = "1", optional = true }
serde = { version = "1", features = ["derive"] }
dirs = { version = "3", optional = true }
memmap2 = { version = "0.2", optional = true }
walkdir = { version = "2", optional = true }
[dev-dependencies]
walkdir = "2"

View File

@ -7,7 +7,6 @@ publish = false
[dev-dependencies]
criterion = "0.3"
fontdock = { path = "../../fontdock" }
typst = { path = ".." }
[[bench]]

View File

@ -1,9 +1,8 @@
use std::path::Path;
use criterion::{criterion_group, criterion_main, Criterion};
use fontdock::FsIndex;
use typst::env::{Env, FsIndexExt, ResourceLoader};
use typst::env::{Env, FsLoader};
use typst::eval::eval;
use typst::exec::{exec, State};
use typst::layout::layout;
@ -17,13 +16,10 @@ const TYP_DIR: &str = "../tests/typ";
const CASES: &[&str] = &["full/coma.typ", "text/basic.typ"];
fn benchmarks(c: &mut Criterion) {
let mut index = FsIndex::new();
index.search_dir(FONT_DIR);
let mut loader = FsLoader::new();
loader.search_dir(FONT_DIR);
let mut env = Env {
fonts: index.into_dynamic_loader(),
resources: ResourceLoader::new(),
};
let mut env = Env::new(loader);
let scope = library::_new();
let state = State::default();

View File

@ -1,153 +0,0 @@
//! Environment interactions.
use std::any::Any;
use std::collections::{hash_map::Entry, HashMap};
use std::fmt::{self, Debug, Formatter};
use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use fontdock::{FaceId, FontSource};
use image::io::Reader as ImageReader;
use image::{DynamicImage, GenericImageView, ImageFormat};
use serde::{Deserialize, Serialize};
#[cfg(feature = "fs")]
use fontdock::{FsIndex, FsSource};
use crate::font::FaceBuf;
/// Encapsulates all environment dependencies (fonts, resources).
#[derive(Debug)]
pub struct Env {
/// Loads fonts from a dynamic font source.
pub fonts: FontLoader,
/// Loads resource from the file system.
pub resources: ResourceLoader,
}
impl Env {
/// Create an empty environment for testing purposes.
pub fn blank() -> Self {
struct BlankSource;
impl FontSource for BlankSource {
type Face = FaceBuf;
fn load(&self, _: FaceId) -> Option<Self::Face> {
None
}
}
Self {
fonts: FontLoader::new(Box::new(BlankSource), vec![]),
resources: ResourceLoader::new(),
}
}
}
/// A font loader that is backed by a dynamic source.
pub type FontLoader = fontdock::FontLoader<Box<dyn FontSource<Face = FaceBuf>>>;
/// Simplify font loader construction from an [`FsIndex`].
#[cfg(feature = "fs")]
pub trait FsIndexExt {
/// Create a font loader backed by a boxed [`FsSource`] which serves all
/// indexed font faces.
fn into_dynamic_loader(self) -> FontLoader;
}
#[cfg(feature = "fs")]
impl FsIndexExt for FsIndex {
fn into_dynamic_loader(self) -> FontLoader {
let (files, descriptors) = self.into_vecs();
FontLoader::new(Box::new(FsSource::new(files)), descriptors)
}
}
/// Loads resource from the file system.
pub struct ResourceLoader {
paths: HashMap<PathBuf, ResourceId>,
entries: Vec<Box<dyn Any>>,
}
/// A unique identifier for a resource.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Serialize, Deserialize)]
pub struct ResourceId(usize);
impl ResourceLoader {
/// Create a new resource loader.
pub fn new() -> Self {
Self { paths: HashMap::new(), entries: vec![] }
}
/// Load a resource from a path and parse it.
pub fn load<P, F, R>(&mut self, path: P, parse: F) -> Option<(ResourceId, &R)>
where
P: AsRef<Path>,
F: FnOnce(Vec<u8>) -> Option<R>,
R: 'static,
{
let path = path.as_ref();
let id = match self.paths.entry(path.to_owned()) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let data = fs::read(path).ok()?;
let resource = parse(data)?;
let len = self.entries.len();
self.entries.push(Box::new(resource));
*entry.insert(ResourceId(len))
}
};
Some((id, self.loaded(id)))
}
/// Retrieve a previously loaded resource by its id.
///
/// # Panics
/// This panics if no resource with this id was loaded.
#[track_caller]
pub fn loaded<R: 'static>(&self, id: ResourceId) -> &R {
self.entries[id.0].downcast_ref().expect("bad resource type")
}
}
impl Debug for ResourceLoader {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_set().entries(self.paths.keys()).finish()
}
}
/// A loaded image resource.
pub struct ImageResource {
/// The original format the image was encoded in.
pub format: ImageFormat,
/// The decoded image.
pub buf: DynamicImage,
}
impl ImageResource {
/// Parse an image resource from raw data in a supported format.
///
/// The image format is determined automatically.
pub fn parse(data: Vec<u8>) -> Option<Self> {
let reader = ImageReader::new(Cursor::new(data)).with_guessed_format().ok()?;
let format = reader.format()?;
let buf = reader.decode().ok()?;
Some(Self { format, buf })
}
}
impl Debug for ImageResource {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let (width, height) = self.buf.dimensions();
f.debug_struct("ImageResource")
.field("format", &self.format)
.field("color", &self.buf.color())
.field("width", &width)
.field("height", &height)
.finish()
}
}

208
src/env/fs.rs vendored Normal file
View File

@ -0,0 +1,208 @@
use std::collections::{hash_map::Entry, HashMap};
use std::fs::File;
use std::io;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use memmap2::Mmap;
use ttf_parser::{name_id, Face};
use walkdir::WalkDir;
use super::{Buffer, Loader};
use crate::font::{FaceInfo, FontStretch, FontStyle, FontVariant, FontWeight};
/// Loads fonts and resources from the local file system.
///
/// _This is only available when the `fs` feature is enabled._
#[derive(Default, Debug, Clone)]
pub struct FsLoader {
faces: Vec<FaceInfo>,
paths: Vec<PathBuf>,
cache: FileCache,
}
/// Maps from paths to loaded file buffers. When the buffer is `None` the file
/// does not exist or couldn't be read.
type FileCache = HashMap<PathBuf, Option<Buffer>>;
impl FsLoader {
/// Create a new loader without any fonts.
pub fn new() -> Self {
Self {
faces: vec![],
paths: vec![],
cache: HashMap::new(),
}
}
/// Search for fonts in the operating system's font directories.
#[cfg(all(unix, not(target_os = "macos")))]
pub fn search_system(&mut self) {
self.search_dir("/usr/share/fonts");
self.search_dir("/usr/local/share/fonts");
if let Some(dir) = dirs::font_dir() {
self.search_dir(dir);
}
}
/// Search for fonts in the operating system's font directories.
#[cfg(target_os = "macos")]
pub fn search_system(&mut self) {
self.search_dir("/Library/Fonts");
self.search_dir("/Network/Library/Fonts");
self.search_dir("/System/Library/Fonts");
if let Some(dir) = dirs::font_dir() {
self.search_dir(dir);
}
}
/// Search for fonts in the operating system's font directories.
#[cfg(windows)]
pub fn search_system(&mut self) {
let windir =
std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string());
self.search_dir(Path::new(&windir).join("Fonts"));
if let Some(roaming) = dirs::config_dir() {
self.search_dir(roaming.join("Microsoft\\Windows\\Fonts"));
}
if let Some(local) = dirs::cache_dir() {
self.search_dir(local.join("Microsoft\\Windows\\Fonts"));
}
}
/// Search for all fonts in a directory.
pub fn search_dir(&mut self, dir: impl AsRef<Path>) {
let walk = WalkDir::new(dir)
.follow_links(true)
.sort_by(|a, b| a.file_name().cmp(b.file_name()))
.into_iter()
.filter_map(|e| e.ok());
for entry in walk {
let path = entry.path();
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
match ext {
#[rustfmt::skip]
"ttf" | "otf" | "TTF" | "OTF" |
"ttc" | "otc" | "TTC" | "OTC" => {
self.search_file(path).ok();
}
_ => {}
}
}
}
}
/// Index the font faces in the file at the given path.
///
/// The file may form a font collection and contain multiple font faces,
/// which will then all be indexed.
pub fn search_file(&mut self, path: impl AsRef<Path>) -> io::Result<()> {
let path = path.as_ref();
let file = File::open(path)?;
let mmap = unsafe { Mmap::map(&file)? };
for i in 0 .. ttf_parser::fonts_in_collection(&mmap).unwrap_or(1) {
let face = Face::from_slice(&mmap, i)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
self.parse_face(path, &face, i)?;
}
Ok(())
}
/// Parse a single face and insert it into the `families`. This either
/// merges with an existing family entry if they have the same trimmed
/// family name, or creates a new one.
fn parse_face(&mut self, path: &Path, face: &Face<'_>, index: u32) -> io::Result<()> {
fn find_name(face: &Face, name_id: u16) -> Option<String> {
face.names().find_map(|entry| {
if entry.name_id() == name_id {
entry.to_string()
} else {
None
}
})
}
let family = find_name(face, name_id::TYPOGRAPHIC_FAMILY)
.or_else(|| find_name(face, name_id::FAMILY))
.ok_or("unknown font family")
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
let variant = FontVariant {
style: match (face.is_italic(), face.is_oblique()) {
(false, false) => FontStyle::Normal,
(true, _) => FontStyle::Italic,
(_, true) => FontStyle::Oblique,
},
weight: FontWeight::from_number(face.weight().to_number()),
stretch: FontStretch::from_number(face.width().to_number()),
};
// Merge with an existing entry for the same family name.
self.faces.push(FaceInfo { family, variant, index });
self.paths.push(path.to_owned());
Ok(())
}
}
impl Loader for FsLoader {
fn faces(&self) -> &[FaceInfo] {
&self.faces
}
fn load_face(&mut self, idx: usize) -> Option<Buffer> {
load(&mut self.cache, &self.paths[idx])
}
fn load_file(&mut self, url: &str) -> Option<Buffer> {
load(&mut self.cache, Path::new(url))
}
}
fn load(cache: &mut FileCache, path: &Path) -> Option<Buffer> {
match cache.entry(path.to_owned()) {
Entry::Occupied(entry) => entry.get().clone(),
Entry::Vacant(entry) => {
let buffer = std::fs::read(path).ok().map(Rc::new);
entry.insert(buffer).clone()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_index_font_dir() {
let mut loader = FsLoader::new();
loader.search_dir("fonts");
loader.paths.sort();
assert_eq!(loader.paths, &[
Path::new("fonts/EBGaramond-Bold.ttf"),
Path::new("fonts/EBGaramond-BoldItalic.ttf"),
Path::new("fonts/EBGaramond-Italic.ttf"),
Path::new("fonts/EBGaramond-Regular.ttf"),
Path::new("fonts/Inconsolata-Bold.ttf"),
Path::new("fonts/Inconsolata-Regular.ttf"),
Path::new("fonts/LatinModernMath.otf"),
Path::new("fonts/NotoSansArabic-Regular.ttf"),
Path::new("fonts/NotoSerifCJKsc-Regular.otf"),
Path::new("fonts/NotoSerifHebrew-Bold.ttf"),
Path::new("fonts/NotoSerifHebrew-Regular.ttf"),
Path::new("fonts/PTSans-Regular.ttf"),
Path::new("fonts/TwitterColorEmoji.ttf"),
]);
}
}

40
src/env/image.rs vendored Normal file
View File

@ -0,0 +1,40 @@
use std::fmt::{self, Debug, Formatter};
use std::io::Cursor;
use image::io::Reader as ImageReader;
use image::{DynamicImage, GenericImageView, ImageFormat};
use super::Buffer;
/// A loaded image resource.
pub struct ImageResource {
/// The original format the image was encoded in.
pub format: ImageFormat,
/// The decoded image.
pub buf: DynamicImage,
}
impl ImageResource {
/// Parse an image resource from raw data in a supported format.
///
/// The image format is determined automatically.
pub fn parse(data: Buffer) -> Option<Self> {
let cursor = Cursor::new(data.as_ref());
let reader = ImageReader::new(cursor).with_guessed_format().ok()?;
let format = reader.format()?;
let buf = reader.decode().ok()?;
Some(Self { format, buf })
}
}
impl Debug for ImageResource {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let (width, height) = self.buf.dimensions();
f.debug_struct("ImageResource")
.field("format", &self.format)
.field("color", &self.buf.color())
.field("width", &width)
.field("height", &height)
.finish()
}
}

202
src/env/mod.rs vendored Normal file
View File

@ -0,0 +1,202 @@
//! Font and resource loading.
#[cfg(feature = "fs")]
mod fs;
mod image;
pub use self::image::*;
#[cfg(feature = "fs")]
pub use fs::*;
use std::any::Any;
use std::collections::{hash_map::Entry, HashMap};
use std::rc::Rc;
use serde::{Deserialize, Serialize};
use crate::font::{Face, FaceInfo, FontVariant};
/// Handles font and resource loading.
pub struct Env {
/// The loader that serves the font face and file buffers.
loader: Box<dyn Loader>,
/// Loaded resources indexed by [`ResourceId`].
resources: Vec<Box<dyn Any>>,
/// Maps from URL to loaded resource.
urls: HashMap<String, ResourceId>,
/// Faces indexed by [`FaceId`]. `None` if not yet loaded.
faces: Vec<Option<Face>>,
/// Maps a family name to the ids of all faces that are part of the family.
families: HashMap<String, Vec<FaceId>>,
}
impl Env {
/// Create an environment from a `loader`.
pub fn new(loader: impl Loader + 'static) -> Self {
let infos = loader.faces();
let mut faces = vec![];
let mut families = HashMap::<String, Vec<FaceId>>::new();
for (i, info) in infos.iter().enumerate() {
let id = FaceId(i as u32);
faces.push(None);
families
.entry(info.family.to_lowercase())
.and_modify(|vec| vec.push(id))
.or_insert_with(|| vec![id]);
}
Self {
loader: Box::new(loader),
resources: vec![],
urls: HashMap::new(),
faces,
families,
}
}
/// Create an empty environment for testing purposes.
pub fn blank() -> Self {
struct BlankLoader;
impl Loader for BlankLoader {
fn faces(&self) -> &[FaceInfo] {
&[]
}
fn load_face(&mut self, _: usize) -> Option<Buffer> {
None
}
fn load_file(&mut self, _: &str) -> Option<Buffer> {
None
}
}
Self::new(BlankLoader)
}
/// Query for and load the font face from the given `family` that most
/// closely matches the given `variant`.
pub fn query_face(&mut self, family: &str, variant: FontVariant) -> Option<FaceId> {
// Check whether a family with this name exists.
let ids = self.families.get(family)?;
let infos = self.loader.faces();
let mut best = None;
let mut best_key = None;
// Find the best matching variant of this font.
for &id in ids {
let current = infos[id.0 as usize].variant;
// This is a perfect match, no need to search further.
if current == variant {
best = Some(id);
break;
}
// If this is not a perfect match, we compute a key that we want to
// minimize among all variants. This key prioritizes style, then
// stretch distance and then weight distance.
let key = (
current.style != variant.style,
current.stretch.distance(variant.stretch),
current.weight.distance(variant.weight),
);
if best_key.map_or(true, |b| key < b) {
best = Some(id);
best_key = Some(key);
}
}
// Load the face if it's not already loaded.
let idx = best?.0 as usize;
let slot = &mut self.faces[idx];
if slot.is_none() {
let index = infos[idx].index;
let buffer = self.loader.load_face(idx)?;
let face = Face::new(buffer, index)?;
*slot = Some(face);
}
best
}
/// Load a file from a local or remote URL, parse it into a cached resource
/// and return a unique identifier that allows to retrieve the parsed
/// resource through [`resource()`](Self::resource).
pub fn load_resource<F, R>(&mut self, url: &str, parse: F) -> Option<ResourceId>
where
F: FnOnce(Buffer) -> Option<R>,
R: 'static,
{
Some(match self.urls.entry(url.to_string()) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let buffer = self.loader.load_file(url)?;
let resource = parse(buffer)?;
let len = self.resources.len();
self.resources.push(Box::new(resource));
*entry.insert(ResourceId(len as u32))
}
})
}
/// Get a reference to a queried face.
///
/// # Panics
/// This panics if no face with this id was loaded. This function should
/// only be called with ids returned by [`query_face()`](Self::query_face).
#[track_caller]
pub fn face(&self, id: FaceId) -> &Face {
self.faces[id.0 as usize].as_ref().expect("font face was not loaded")
}
/// Get a reference to a loaded resource.
///
/// This panics if no resource with this id was loaded. This function should
/// only be called with ids returned by
/// [`load_resource()`](Self::load_resource).
#[track_caller]
pub fn resource<R: 'static>(&self, id: ResourceId) -> &R {
self.resources[id.0 as usize]
.downcast_ref()
.expect("bad resource type")
}
}
/// Loads fonts and resources from a remote or local source.
pub trait Loader {
/// Descriptions of all font faces this loader serves.
fn faces(&self) -> &[FaceInfo];
/// Load the font face with the given index in [`faces()`](Self::faces).
fn load_face(&mut self, idx: usize) -> Option<Buffer>;
/// Load a file from a URL.
fn load_file(&mut self, url: &str) -> Option<Buffer>;
}
/// A shared byte buffer.
pub type Buffer = Rc<Vec<u8>>;
/// A unique identifier for a loaded font face.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct FaceId(u32);
impl FaceId {
/// A blank initialization value.
pub const MAX: Self = Self(u32::MAX);
}
/// A unique identifier for a loaded resource.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct ResourceId(u32);
impl ResourceId {
/// A blank initialization value.
pub const MAX: Self = Self(u32::MAX);
}

View File

@ -1,10 +1,8 @@
use std::fmt::{self, Display, Formatter};
use std::rc::Rc;
use fontdock::{FontStretch, FontStyle, FontVariant, FontWeight};
use crate::color::{Color, RgbaColor};
use crate::font::VerticalFontMetric;
use crate::font::{FontStretch, FontStyle, FontVariant, FontWeight, VerticalFontMetric};
use crate::geom::*;
use crate::layout::Fill;
use crate::paper::{Paper, PaperClass, PAPER_A4};
@ -181,7 +179,7 @@ impl Default for FontState {
variant: FontVariant {
style: FontStyle::Normal,
weight: FontWeight::REGULAR,
stretch: FontStretch::Normal,
stretch: FontStretch::NORMAL,
},
size: Length::pt(11.0),
top_edge: VerticalFontMetric::CapHeight,

View File

@ -1,27 +1,60 @@
//! Font handling.
use std::fmt::{self, Display, Formatter};
use std::fmt::{self, Debug, Display, Formatter};
use fontdock::FaceFromVec;
use serde::{Deserialize, Serialize};
use crate::env::Buffer;
use crate::geom::Length;
/// An owned font face.
pub struct FaceBuf {
data: Box<[u8]>,
/// A font face.
pub struct Face {
buffer: Buffer,
index: u32,
inner: rustybuzz::Face<'static>,
ttf: rustybuzz::Face<'static>,
units_per_em: f64,
ascender: f64,
cap_height: f64,
x_height: f64,
descender: f64,
ascender: Em,
cap_height: Em,
x_height: Em,
descender: Em,
}
impl FaceBuf {
/// The raw face data.
pub fn data(&self) -> &[u8] {
&self.data
impl Face {
/// Parse a font face from a buffer and collection index.
pub fn new(buffer: Buffer, index: u32) -> Option<Self> {
// SAFETY:
// - The slices's location is stable in memory:
// - We don't move the underlying vector
// - Nobody else can move it since we haved a strong ref to the `Rc`.
// - The internal static lifetime is not leaked because its rewritten
// to the self-lifetime in `ttf()`.
let slice: &'static [u8] =
unsafe { std::slice::from_raw_parts(buffer.as_ptr(), buffer.len()) };
let ttf = rustybuzz::Face::from_slice(slice, index)?;
// Look up some metrics we may need often.
let units_per_em = f64::from(ttf.units_per_em());
let ascender = ttf.typographic_ascender().unwrap_or(ttf.ascender());
let cap_height = ttf.capital_height().filter(|&h| h > 0).unwrap_or(ascender);
let x_height = ttf.x_height().filter(|&h| h > 0).unwrap_or(ascender);
let descender = ttf.typographic_descender().unwrap_or(ttf.descender());
Some(Self {
buffer,
index,
ttf,
units_per_em,
ascender: Em::from_units(ascender, units_per_em),
cap_height: Em::from_units(cap_height, units_per_em),
x_height: Em::from_units(x_height, units_per_em),
descender: Em::from_units(descender, units_per_em),
})
}
/// The underlying buffer.
pub fn buffer(&self) -> &Buffer {
&self.buffer
}
/// The collection index.
@ -29,74 +62,27 @@ impl FaceBuf {
self.index
}
/// Get a reference to the underlying ttf-parser/rustybuzz face.
/// A reference to the underlying `ttf-parser` / `rustybuzz` face.
pub fn ttf(&self) -> &rustybuzz::Face<'_> {
// We can't implement Deref because that would leak the internal 'static
// lifetime.
&self.inner
&self.ttf
}
/// Look up a vertical metric.
pub fn vertical_metric(&self, metric: VerticalFontMetric) -> EmLength {
self.convert(match metric {
pub fn vertical_metric(&self, metric: VerticalFontMetric) -> Em {
match metric {
VerticalFontMetric::Ascender => self.ascender,
VerticalFontMetric::CapHeight => self.cap_height,
VerticalFontMetric::XHeight => self.x_height,
VerticalFontMetric::Baseline => 0.0,
VerticalFontMetric::Baseline => Em::ZERO,
VerticalFontMetric::Descender => self.descender,
})
}
}
/// Convert from font units to an em length length.
pub fn convert(&self, units: impl Into<f64>) -> EmLength {
EmLength(units.into() / self.units_per_em)
}
}
impl FaceFromVec for FaceBuf {
fn from_vec(vec: Vec<u8>, index: u32) -> Option<Self> {
let data = vec.into_boxed_slice();
// SAFETY: The slices's location is stable in memory since we don't
// touch it and it can't be touched from outside this type.
let slice: &'static [u8] =
unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
let inner = rustybuzz::Face::from_slice(slice, index)?;
// Look up some metrics we may need often.
let units_per_em = inner.units_per_em();
let ascender = inner.typographic_ascender().unwrap_or(inner.ascender());
let cap_height = inner.capital_height().filter(|&h| h > 0).unwrap_or(ascender);
let x_height = inner.x_height().filter(|&h| h > 0).unwrap_or(ascender);
let descender = inner.typographic_descender().unwrap_or(inner.descender());
Some(Self {
data,
index,
inner,
units_per_em: f64::from(units_per_em),
ascender: f64::from(ascender),
cap_height: f64::from(cap_height),
x_height: f64::from(x_height),
descender: f64::from(descender),
})
}
}
/// A length in resolved em units.
#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct EmLength(f64);
impl EmLength {
/// Convert to a length at the given font size.
pub fn scale(self, size: Length) -> Length {
self.0 * size
}
/// Get the number of em units.
pub fn get(self) -> f64 {
self.0
/// Convert from font units to an em length.
pub fn to_em(&self, units: impl Into<f64>) -> Em {
Em::from_units(units, self.units_per_em)
}
}
@ -132,3 +118,373 @@ impl Display for VerticalFontMetric {
})
}
}
/// A length in em units.
///
/// `1em` is the same as the font size.
#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct Em(f64);
impl Em {
/// The zero length.
pub const ZERO: Self = Self(0.0);
/// Create an em length.
pub fn new(em: f64) -> Self {
Self(em)
}
/// Convert units to an em length at the given units per em.
pub fn from_units(units: impl Into<f64>, units_per_em: f64) -> Self {
Self(units.into() / units_per_em)
}
/// The number of em units.
pub fn get(self) -> f64 {
self.0
}
/// Convert to a length at the given font size.
pub fn to_length(self, font_size: Length) -> Length {
self.0 * font_size
}
}
/// Properties of a single font face.
#[derive(Debug, Clone, PartialEq)]
pub struct FaceInfo {
/// The typographic font family this face is part of.
pub family: String,
/// Properties that distinguish this face from other faces in the same
/// family.
pub variant: FontVariant,
/// The collection index in the font file.
pub index: u32,
}
/// Properties that distinguish a face from other faces in the same family.
#[derive(Default, Debug, Copy, Clone, PartialEq)]
pub struct FontVariant {
/// The style of the face (normal / italic / oblique).
pub style: FontStyle,
/// How heavy the face is (100 - 900).
pub weight: FontWeight,
/// How condensed or expanded the face is (0.5 - 2.0).
pub stretch: FontStretch,
}
impl FontVariant {
/// Create a variant from its three components.
pub fn new(style: FontStyle, weight: FontWeight, stretch: FontStretch) -> Self {
Self { style, weight, stretch }
}
}
/// The style of a font face.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[derive(Serialize, Deserialize)]
pub enum FontStyle {
/// The default style.
Normal,
/// A cursive style.
Italic,
/// A slanted style.
Oblique,
}
impl FontStyle {
/// Create a font style from a lowercase name like `italic`.
pub fn from_str(name: &str) -> Option<FontStyle> {
Some(match name {
"normal" => Self::Normal,
"italic" => Self::Italic,
"oblique" => Self::Oblique,
_ => return None,
})
}
/// The lowercase string representation of this style.
pub fn to_str(self) -> &'static str {
match self {
Self::Normal => "normal",
Self::Italic => "italic",
Self::Oblique => "oblique",
}
}
}
impl Default for FontStyle {
fn default() -> Self {
Self::Normal
}
}
impl Display for FontStyle {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(self.to_str())
}
}
/// The weight of a font face.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct FontWeight(u16);
impl FontWeight {
/// Thin weight (100).
pub const THIN: Self = Self(100);
/// Extra light weight (200).
pub const EXTRALIGHT: Self = Self(200);
/// Light weight (300).
pub const LIGHT: Self = Self(300);
/// Regular weight (400).
pub const REGULAR: Self = Self(400);
/// Medium weight (500).
pub const MEDIUM: Self = Self(500);
/// Semibold weight (600).
pub const SEMIBOLD: Self = Self(600);
/// Bold weight (700).
pub const BOLD: Self = Self(700);
/// Extrabold weight (800).
pub const EXTRABOLD: Self = Self(800);
/// Black weight (900).
pub const BLACK: Self = Self(900);
/// Create a font weight from a number between 100 and 900, clamping it if
/// necessary.
pub fn from_number(weight: u16) -> Self {
Self(weight.max(100).min(900))
}
/// Create a font weight from a lowercase name like `light`.
pub fn from_str(name: &str) -> Option<Self> {
Some(match name {
"thin" => Self::THIN,
"extralight" => Self::EXTRALIGHT,
"light" => Self::LIGHT,
"regular" => Self::REGULAR,
"medium" => Self::MEDIUM,
"semibold" => Self::SEMIBOLD,
"bold" => Self::BOLD,
"extrabold" => Self::EXTRABOLD,
"black" => Self::BLACK,
_ => return None,
})
}
/// The number between 100 and 900.
pub fn to_number(self) -> u16 {
self.0
}
/// The lowercase string representation of this weight if it is divisible by
/// 100.
pub fn to_str(self) -> Option<&'static str> {
Some(match self {
Self::THIN => "thin",
Self::EXTRALIGHT => "extralight",
Self::LIGHT => "light",
Self::REGULAR => "regular",
Self::MEDIUM => "medium",
Self::SEMIBOLD => "semibold",
Self::BOLD => "bold",
Self::EXTRABOLD => "extrabold",
Self::BLACK => "black",
_ => return None,
})
}
/// Add (or remove) weight, saturating at the boundaries of 100 and 900.
pub fn thicken(self, delta: i16) -> Self {
Self((self.0 as i16).saturating_add(delta).max(100).min(900) as u16)
}
/// The absolute number distance between this and another font weight.
pub fn distance(self, other: Self) -> u16 {
(self.0 as i16 - other.0 as i16).abs() as u16
}
}
impl Default for FontWeight {
fn default() -> Self {
Self::REGULAR
}
}
impl Display for FontWeight {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self.to_str() {
Some(name) => f.pad(name),
None => write!(f, "{}", self.0),
}
}
}
impl Debug for FontWeight {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.pad(match *self {
Self::THIN => "Thin",
Self::EXTRALIGHT => "Extralight",
Self::LIGHT => "Light",
Self::REGULAR => "Regular",
Self::MEDIUM => "Medium",
Self::SEMIBOLD => "Semibold",
Self::BOLD => "Bold",
Self::EXTRABOLD => "Extrabold",
Self::BLACK => "Black",
_ => return write!(f, "{}", self.0),
})
}
}
/// The width of a font face.
#[derive(Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct FontStretch(f32);
impl FontStretch {
/// Ultra-condensed stretch (50%).
pub const ULTRA_CONDENSED: Self = Self(0.5);
/// Extra-condensed stretch weight (62.5%).
pub const EXTRA_CONDENSED: Self = Self(0.625);
/// Condensed stretch (75%).
pub const CONDENSED: Self = Self(0.75);
/// Semi-condensed stretch (87.5%).
pub const SEMI_CONDENSED: Self = Self(0.875);
/// Normal stretch (100%).
pub const NORMAL: Self = Self(1.0);
/// Semi-expanded stretch (112.5%).
pub const SEMI_EXPANDED: Self = Self(1.125);
/// Expanded stretch (125%).
pub const EXPANDED: Self = Self(1.25);
/// Extra-expanded stretch (150%).
pub const EXTRA_EXPANDED: Self = Self(1.5);
/// Ultra-expanded stretch (200%).
pub const ULTRA_EXPANDED: Self = Self(2.0);
/// Create a font stretch from a ratio between 0.5 and 2.0, clamping it if
/// necessary.
pub fn from_ratio(ratio: f32) -> Self {
Self(ratio.max(0.5).min(2.0))
}
/// Create a font stretch from an OpenType-style number between 1 and 9,
/// clamping it if necessary.
pub fn from_number(stretch: u16) -> Self {
match stretch {
0 | 1 => Self::ULTRA_CONDENSED,
2 => Self::EXTRA_CONDENSED,
3 => Self::CONDENSED,
4 => Self::SEMI_CONDENSED,
5 => Self::NORMAL,
6 => Self::SEMI_EXPANDED,
7 => Self::EXPANDED,
8 => Self::EXTRA_EXPANDED,
_ => Self::ULTRA_EXPANDED,
}
}
/// Create a font stretch from a lowercase name like `extra-expanded`.
pub fn from_str(name: &str) -> Option<Self> {
Some(match name {
"ultra-condensed" => Self::ULTRA_CONDENSED,
"extra-condensed" => Self::EXTRA_CONDENSED,
"condensed" => Self::CONDENSED,
"semi-condensed" => Self::SEMI_CONDENSED,
"normal" => Self::NORMAL,
"semi-expanded" => Self::SEMI_EXPANDED,
"expanded" => Self::EXPANDED,
"extra-expanded" => Self::EXTRA_EXPANDED,
"ultra-expanded" => Self::ULTRA_EXPANDED,
_ => return None,
})
}
/// The ratio between 0.5 and 2.0 corresponding to this stretch.
pub fn to_ratio(self) -> f32 {
self.0
}
/// The lowercase string representation of this stretch is one of the named
/// ones.
pub fn to_str(self) -> Option<&'static str> {
Some(match self {
s if s == Self::ULTRA_CONDENSED => "ultra-condensed",
s if s == Self::EXTRA_CONDENSED => "extra-condensed",
s if s == Self::CONDENSED => "condensed",
s if s == Self::SEMI_CONDENSED => "semi-condensed",
s if s == Self::NORMAL => "normal",
s if s == Self::SEMI_EXPANDED => "semi-expanded",
s if s == Self::EXPANDED => "expanded",
s if s == Self::EXTRA_EXPANDED => "extra-expanded",
s if s == Self::ULTRA_EXPANDED => "ultra-expanded",
_ => return None,
})
}
/// The absolute ratio distance between this and another font stretch.
pub fn distance(self, other: Self) -> f32 {
(self.0 - other.0).abs()
}
}
impl Default for FontStretch {
fn default() -> Self {
Self::NORMAL
}
}
impl Display for FontStretch {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self.to_str() {
Some(name) => f.pad(name),
None => write!(f, "{}", self.0),
}
}
}
impl Debug for FontStretch {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(match *self {
s if s == Self::ULTRA_CONDENSED => "UltraCondensed",
s if s == Self::EXTRA_CONDENSED => "ExtraCondensed",
s if s == Self::CONDENSED => "Condensed",
s if s == Self::SEMI_CONDENSED => "SemiCondensed",
s if s == Self::NORMAL => "Normal",
s if s == Self::SEMI_EXPANDED => "SemiExpanded",
s if s == Self::EXPANDED => "Expanded",
s if s == Self::EXTRA_EXPANDED => "ExtraExpanded",
s if s == Self::ULTRA_EXPANDED => "UltraExpanded",
_ => return write!(f, "{}", self.0),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_font_weight_distance() {
let d = |a, b| FontWeight(a).distance(FontWeight(b));
assert_eq!(d(500, 200), 300);
assert_eq!(d(500, 500), 0);
assert_eq!(d(500, 900), 400);
assert_eq!(d(10, 100), 90);
}
}

View File

@ -1,7 +1,5 @@
use fontdock::FaceId;
use crate::color::Color;
use crate::env::ResourceId;
use crate::env::{FaceId, ResourceId};
use crate::geom::{Length, Path, Point, Size};
use serde::{Deserialize, Serialize};
@ -114,15 +112,13 @@ pub enum Shape {
pub enum Fill {
/// The fill is a color.
Color(Color),
/// The fill is an image.
Image(Image),
}
/// An image element.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct Image {
/// The image resource.
pub res: ResourceId,
pub id: ResourceId,
/// The size of the image in the document.
pub size: Size,
}

View File

@ -2,14 +2,13 @@ use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter};
use std::ops::Range;
use fontdock::FaceId;
use rustybuzz::UnicodeBuffer;
use ttf_parser::GlyphId;
use super::{Element, Frame, Glyph, LayoutContext, Text};
use crate::env::FontLoader;
use crate::env::FaceId;
use crate::exec::FontProps;
use crate::font::FaceBuf;
use crate::font::Face;
use crate::geom::{Dir, Length, Point, Size};
use crate::util::SliceExt;
@ -75,10 +74,10 @@ impl<'a> ShapedText<'a> {
glyphs: vec![],
};
let face = ctx.env.fonts.face(face_id);
let face = ctx.env.face(face_id);
for glyph in group {
let x_advance = face.convert(glyph.x_advance).scale(self.props.size);
let x_offset = face.convert(glyph.x_offset).scale(self.props.size);
let x_advance = face.to_em(glyph.x_advance).to_length(self.props.size);
let x_offset = face.to_em(glyph.x_offset).to_length(self.props.size);
text.glyphs.push(Glyph {
id: glyph.glyph_id.0,
x_advance,
@ -101,7 +100,7 @@ impl<'a> ShapedText<'a> {
text_range: Range<usize>,
) -> ShapedText<'a> {
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
let (size, baseline) = measure(&mut ctx.env.fonts, glyphs, self.props);
let (size, baseline) = measure(ctx, glyphs, self.props);
Self {
text: &self.text[text_range],
dir: self.dir,
@ -187,15 +186,13 @@ pub fn shape<'a>(
dir: Dir,
props: &'a FontProps,
) -> ShapedText<'a> {
let loader = &mut ctx.env.fonts;
let mut glyphs = vec![];
let families = props.families.iter();
if !text.is_empty() {
shape_segment(loader, &mut glyphs, 0, text, dir, props, families, None);
shape_segment(ctx, &mut glyphs, 0, text, dir, props, families, None);
}
let (size, baseline) = measure(loader, &glyphs, props);
let (size, baseline) = measure(ctx, &glyphs, props);
ShapedText {
text,
@ -209,7 +206,7 @@ pub fn shape<'a>(
/// Shape text with font fallback using the `families` iterator.
fn shape_segment<'a>(
loader: &mut FontLoader,
ctx: &mut LayoutContext,
glyphs: &mut Vec<ShapedGlyph>,
base: usize,
text: &str,
@ -222,7 +219,7 @@ fn shape_segment<'a>(
let (face_id, fallback) = loop {
// Try to load the next available font family.
match families.next() {
Some(family) => match loader.query(family, props.variant) {
Some(family) => match ctx.env.query_face(family, props.variant) {
Some(id) => break (id, true),
None => {}
},
@ -249,7 +246,7 @@ fn shape_segment<'a>(
});
// Shape!
let buffer = rustybuzz::shape(loader.face(face_id).ttf(), &[], buffer);
let buffer = rustybuzz::shape(ctx.env.face(face_id).ttf(), &[], buffer);
let infos = buffer.glyph_infos();
let pos = buffer.glyph_positions();
@ -313,7 +310,7 @@ fn shape_segment<'a>(
// Recursively shape the tofu sequence with the next family.
shape_segment(
loader,
ctx,
glyphs,
base + range.start,
&text[range],
@ -331,34 +328,35 @@ fn shape_segment<'a>(
/// Measure the size and baseline of a run of shaped glyphs with the given
/// properties.
fn measure(
loader: &mut FontLoader,
ctx: &mut LayoutContext,
glyphs: &[ShapedGlyph],
props: &FontProps,
) -> (Size, Length) {
let mut width = Length::ZERO;
let mut top = Length::ZERO;
let mut bottom = Length::ZERO;
let mut expand_vertical = |face: &FaceBuf| {
top = top.max(face.vertical_metric(props.top_edge).scale(props.size));
bottom = bottom.max(-face.vertical_metric(props.bottom_edge).scale(props.size));
let mut expand_vertical = |face: &Face| {
top = top.max(face.vertical_metric(props.top_edge).to_length(props.size));
bottom =
bottom.max(-face.vertical_metric(props.bottom_edge).to_length(props.size));
};
if glyphs.is_empty() {
// When there are no glyphs, we just use the vertical metrics of the
// first available font.
for family in props.families.iter() {
if let Some(face_id) = loader.query(family, props.variant) {
expand_vertical(loader.face(face_id));
if let Some(face_id) = ctx.env.query_face(family, props.variant) {
expand_vertical(ctx.env.face(face_id));
break;
}
}
} else {
for (face_id, group) in glyphs.group_by_key(|g| g.face_id) {
let face = loader.face(face_id);
let face = ctx.env.face(face_id);
expand_vertical(face);
for glyph in group {
width += face.convert(glyph.x_advance).scale(props.size);
width += face.to_em(glyph.x_advance).to_length(props.size);
}
}
}

View File

@ -1,5 +1,5 @@
use crate::font::{FontStretch, FontStyle, FontWeight};
use crate::layout::Fill;
use fontdock::{FontStretch, FontStyle, FontWeight};
use super::*;
@ -156,7 +156,12 @@ typify! {
FontWeight: "font weight",
Value::Int(number) => {
let [min, max] = [Self::THIN, Self::BLACK];
let message = || format!("should be between {:#?} and {:#?}", min, max);
let message = || format!(
"should be between {} and {}",
min.to_number(),
max.to_number(),
);
return if number < i64::from(min.to_number()) {
CastResult::Warn(min, message())
} else if number > i64::from(max.to_number()) {
@ -170,11 +175,18 @@ typify! {
typify! {
FontStretch: "font stretch",
Value::Relative(relative) => {
let f = |stretch: Self| Relative::new(stretch.to_ratio());
let [min, max] = [f(Self::UltraCondensed), f(Self::UltraExpanded)];
let value = Self::from_ratio(relative.get());
return if relative < min || relative > max {
CastResult::Warn(value, format!("should be between {} and {}", min, max))
let [min, max] = [Self::ULTRA_CONDENSED, Self::ULTRA_EXPANDED];
let message = || format!(
"should be between {} and {}",
Relative::new(min.to_ratio() as f64),
Relative::new(max.to_ratio() as f64),
);
let ratio = relative.get() as f32;
let value = Self::from_ratio(ratio);
return if ratio < min.to_ratio() || ratio > max.to_ratio() {
CastResult::Warn(value, message())
} else {
CastResult::Ok(value)
};

View File

@ -20,10 +20,11 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
Value::template("image", move |ctx| {
if let Some(path) = &path {
let loaded = ctx.env.resources.load(&path.v, ImageResource::parse);
if let Some((res, img)) = loaded {
let loaded = ctx.env.load_resource(&path.v, ImageResource::parse);
if let Some(id) = loaded {
let img = ctx.env.resource::<ImageResource>(id);
let dimensions = img.buf.dimensions();
ctx.push(ImageNode { res, dimensions, width, height });
ctx.push(ImageNode { id, dimensions, width, height });
} else {
ctx.diag(error!(path.span, "failed to load image"));
}
@ -35,7 +36,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
#[derive(Debug, Clone, PartialEq)]
struct ImageNode {
/// The resource id of the image file.
res: ResourceId,
id: ResourceId,
/// The pixel dimensions of the image.
dimensions: (u32, u32),
/// The fixed width, if any.
@ -74,7 +75,7 @@ impl Layout for ImageNode {
};
let mut frame = Frame::new(size, size.height);
frame.push(Point::ZERO, Element::Image(Image { res: self.res, size }));
frame.push(Point::ZERO, Element::Image(Image { id: self.id, size }));
vec![frame]
}

View File

@ -29,12 +29,11 @@ pub use spacing::*;
use std::fmt::{self, Display, Formatter};
use fontdock::{FontStyle, FontWeight};
use crate::eval::{AnyValue, FuncValue, Scope};
use crate::eval::{EvalContext, FuncArgs, TemplateValue, Value};
use crate::eval::{
AnyValue, EvalContext, FuncArgs, FuncValue, Scope, TemplateValue, Value,
};
use crate::exec::{Exec, FontFamily};
use crate::font::VerticalFontMetric;
use crate::font::{FontStyle, FontWeight, VerticalFontMetric};
use crate::geom::*;
use crate::syntax::{Node, Spanned};

View File

@ -2,10 +2,9 @@ use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, bail, Context};
use fontdock::FsIndex;
use typst::diag::Pass;
use typst::env::{Env, FsIndexExt, ResourceLoader};
use typst::env::{Env, FsLoader};
use typst::exec::State;
use typst::library;
use typst::parse::LineMap;
@ -35,14 +34,11 @@ fn main() -> anyhow::Result<()> {
let src = fs::read_to_string(src_path).context("Failed to read from source file.")?;
let mut index = FsIndex::new();
index.search_dir("fonts");
index.search_system();
let mut loader = FsLoader::new();
loader.search_dir("fonts");
loader.search_system();
let mut env = Env {
fonts: index.into_dynamic_loader(),
resources: ResourceLoader::new(),
};
let mut env = Env::new(loader);
let scope = library::_new();
let state = State::default();

View File

@ -4,7 +4,6 @@ use std::cmp::Eq;
use std::collections::HashMap;
use std::hash::Hash;
use fontdock::FaceId;
use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba};
use miniz_oxide::deflate;
use pdf_writer::{
@ -14,8 +13,8 @@ use pdf_writer::{
use ttf_parser::{name_id, GlyphId};
use crate::color::Color;
use crate::env::{Env, ImageResource, ResourceId};
use crate::font::{EmLength, VerticalFontMetric};
use crate::env::{Env, FaceId, ImageResource, ResourceId};
use crate::font::{Em, VerticalFontMetric};
use crate::geom::{self, Length, Size};
use crate::layout::{Element, Fill, Frame, Image, Shape};
@ -53,11 +52,11 @@ impl<'a> PdfExporter<'a> {
match element {
Element::Text(shaped) => fonts.insert(shaped.face_id),
Element::Image(image) => {
let img = env.resources.loaded::<ImageResource>(image.res);
let img = env.resource::<ImageResource>(image.id);
if img.buf.color().has_alpha() {
alpha_masks += 1;
}
images.insert(image.res);
images.insert(image.id);
}
Element::Geometry(_) => {}
}
@ -141,8 +140,8 @@ impl<'a> PdfExporter<'a> {
let y = (page.size.height - pos.y).to_pt() as f32;
match element {
&Element::Image(Image { res, size: Size { width, height } }) => {
let name = format!("Im{}", self.images.map(res));
&Element::Image(Image { id, size: Size { width, height } }) => {
let name = format!("Im{}", self.images.map(id));
let w = width.to_pt() as f32;
let h = height.to_pt() as f32;
@ -208,7 +207,7 @@ impl<'a> PdfExporter<'a> {
fn write_fonts(&mut self) {
for (refs, face_id) in self.refs.fonts().zip(self.fonts.layout_indices()) {
let face = self.env.fonts.face(face_id);
let face = self.env.face(face_id);
let ttf = face.ttf();
let name = ttf
@ -237,10 +236,10 @@ impl<'a> PdfExporter<'a> {
let global_bbox = ttf.global_bounding_box();
let bbox = Rect::new(
face.convert(global_bbox.x_min).to_pdf(),
face.convert(global_bbox.y_min).to_pdf(),
face.convert(global_bbox.x_max).to_pdf(),
face.convert(global_bbox.y_max).to_pdf(),
face.to_em(global_bbox.x_min).to_pdf(),
face.to_em(global_bbox.y_min).to_pdf(),
face.to_em(global_bbox.x_max).to_pdf(),
face.to_em(global_bbox.y_max).to_pdf(),
);
let italic_angle = ttf.italic_angle().unwrap_or(0.0);
@ -268,7 +267,7 @@ impl<'a> PdfExporter<'a> {
let num_glyphs = ttf.number_of_glyphs();
(0 .. num_glyphs).map(|g| {
let x = ttf.glyph_hor_advance(GlyphId(g)).unwrap_or(0);
face.convert(x).to_pdf()
face.to_em(x).to_pdf()
})
});
@ -305,7 +304,7 @@ impl<'a> PdfExporter<'a> {
.system_info(system_info);
// Write the face's bytes.
self.writer.stream(refs.data, face.data());
self.writer.stream(refs.data, face.buffer());
}
}
@ -313,7 +312,7 @@ impl<'a> PdfExporter<'a> {
let mut masks_seen = 0;
for (id, resource) in self.refs.images().zip(self.images.layout_indices()) {
let img = self.env.resources.loaded::<ImageResource>(resource);
let img = self.env.resource::<ImageResource>(resource);
let (width, height) = img.buf.dimensions();
// Add the primary image.
@ -361,7 +360,6 @@ fn write_fill(content: &mut Content, fill: Fill) {
Fill::Color(Color::Rgba(c)) => {
content.fill_rgb(c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0);
}
Fill::Image(_) => todo!(),
}
}
@ -567,13 +565,13 @@ where
}
}
/// Additional methods for [`EmLength`].
trait EmLengthExt {
/// Additional methods for [`Em`].
trait EmExt {
/// Convert an em length to a number of PDF font units.
fn to_pdf(self) -> f32;
}
impl EmLengthExt for EmLength {
impl EmExt for Em {
fn to_pdf(self) -> f32 {
1000.0 * self.get() as f32
}

View File

@ -5,7 +5,6 @@ use std::fs;
use std::path::Path;
use std::rc::Rc;
use fontdock::FsIndex;
use image::{GenericImageView, Rgba};
use tiny_skia::{
Color, ColorU8, FillRule, FilterQuality, Paint, Pattern, Pixmap, Rect, SpreadMode,
@ -16,7 +15,7 @@ use walkdir::WalkDir;
use typst::color;
use typst::diag::{Diag, DiagSet, Level, Pass};
use typst::env::{Env, FsIndexExt, ImageResource, ResourceLoader};
use typst::env::{Env, FsLoader, ImageResource};
use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value};
use typst::exec::State;
use typst::geom::{self, Length, Point, Sides, Size};
@ -63,13 +62,10 @@ fn main() {
println!("Running {} tests", len);
}
let mut index = FsIndex::new();
index.search_dir(FONT_DIR);
let mut loader = FsLoader::new();
loader.search_dir(FONT_DIR);
let mut env = Env {
fonts: index.into_dynamic_loader(),
resources: ResourceLoader::new(),
};
let mut env = Env::new(loader);
let mut ok = true;
for src_path in filtered {
@ -414,7 +410,7 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap {
}
fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &Text) {
let ttf = env.fonts.face(shaped.face_id).ttf();
let ttf = env.face(shaped.face_id).ttf();
let mut x = 0.0;
for glyph in &shaped.glyphs {
@ -484,7 +480,7 @@ fn draw_geometry(canvas: &mut Pixmap, ts: Transform, element: &Geometry) {
}
fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, element: &Image) {
let img = &env.resources.loaded::<ImageResource>(element.res);
let img = &env.resource::<ImageResource>(element.id);
let mut pixmap = Pixmap::new(img.buf.width(), img.buf.height()).unwrap();
for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) {
@ -513,10 +509,9 @@ fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, element: &Image) {
fn convert_typst_fill(fill: Fill) -> Paint<'static> {
let mut paint = Paint::default();
match fill {
Fill::Color(c) => match c {
color::Color::Rgba(c) => paint.set_color_rgba8(c.r, c.g, c.b, c.a),
},
Fill::Image(_) => todo!(),
Fill::Color(color::Color::Rgba(c)) => {
paint.set_color_rgba8(c.r, c.g, c.b, c.a);
}
}
paint
}