Reengineer font and resource loading 🏞
This commit is contained in:
parent
72478946c2
commit
6292d25afb
7
.github/workflows/rust.yml
vendored
7
.github/workflows/rust.yml
vendored
@ -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:
|
||||
|
12
Cargo.toml
12
Cargo.toml
@ -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"
|
||||
|
@ -7,7 +7,6 @@ publish = false
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.3"
|
||||
fontdock = { path = "../../fontdock" }
|
||||
typst = { path = ".." }
|
||||
|
||||
[[bench]]
|
||||
|
@ -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();
|
||||
|
153
src/env.rs
153
src/env.rs
@ -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
208
src/env/fs.rs
vendored
Normal 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
40
src/env/image.rs
vendored
Normal 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
202
src/env/mod.rs
vendored
Normal 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);
|
||||
}
|
@ -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,
|
||||
|
496
src/font.rs
496
src/font.rs
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
};
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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};
|
||||
|
||||
|
14
src/main.rs
14
src/main.rs
@ -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();
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user