Remove font store
This commit is contained in:
parent
59f67b79c7
commit
e29f55bb29
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -833,6 +833,7 @@ checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae"
|
||||
name = "typst"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytemuck",
|
||||
"codespan-reporting",
|
||||
"csv",
|
||||
|
@ -7,13 +7,14 @@ edition = "2021"
|
||||
[features]
|
||||
default = ["fs"]
|
||||
cli = ["fs", "pico-args", "codespan-reporting", "same-file"]
|
||||
fs = ["dirs", "memmap2", "same-file", "walkdir"]
|
||||
fs = ["dirs", "memmap2", "walkdir", "same-file"]
|
||||
|
||||
[dependencies]
|
||||
# Workspace
|
||||
typst-macros = { path = "./macros" }
|
||||
|
||||
# Utilities
|
||||
bitflags = "1"
|
||||
bytemuck = "1"
|
||||
fxhash = "0.2"
|
||||
lipsum = { git = "https://github.com/reknih/lipsum" }
|
||||
|
@ -1,10 +1,12 @@
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use iai::{black_box, main, Iai};
|
||||
use unscanny::Scanner;
|
||||
|
||||
use typst::loading::MemLoader;
|
||||
use typst::font::{Font, FontBook};
|
||||
use typst::loading::{Buffer, FileHash, Loader};
|
||||
use typst::parse::{TokenMode, Tokens};
|
||||
use typst::source::SourceId;
|
||||
use typst::{Config, Context};
|
||||
@ -13,7 +15,7 @@ const SRC: &str = include_str!("bench.typ");
|
||||
const FONT: &[u8] = include_bytes!("../fonts/IBMPlexSans-Regular.ttf");
|
||||
|
||||
fn context() -> (Context, SourceId) {
|
||||
let loader = MemLoader::new().with(Path::new("font.ttf"), FONT);
|
||||
let loader = BenchLoader::new();
|
||||
let mut ctx = Context::new(Arc::new(loader), Config::default());
|
||||
let id = ctx.sources.provide(Path::new("src.typ"), SRC.to_string());
|
||||
(ctx, id)
|
||||
@ -94,5 +96,36 @@ fn bench_layout(iai: &mut Iai) {
|
||||
fn bench_render(iai: &mut Iai) {
|
||||
let (mut ctx, id) = context();
|
||||
let frames = typst::typeset(&mut ctx, id).unwrap();
|
||||
iai.run(|| typst::export::render(&mut ctx, &frames[0], 1.0))
|
||||
iai.run(|| typst::export::render(&frames[0], 1.0))
|
||||
}
|
||||
|
||||
struct BenchLoader {
|
||||
book: FontBook,
|
||||
font: Font,
|
||||
}
|
||||
|
||||
impl BenchLoader {
|
||||
fn new() -> Self {
|
||||
let font = Font::new(FONT.into(), 0).unwrap();
|
||||
let book = FontBook::from_fonts([&font]);
|
||||
Self { book, font }
|
||||
}
|
||||
}
|
||||
|
||||
impl Loader for BenchLoader {
|
||||
fn book(&self) -> &FontBook {
|
||||
&self.book
|
||||
}
|
||||
|
||||
fn font(&self, _: usize) -> io::Result<Font> {
|
||||
Ok(self.font.clone())
|
||||
}
|
||||
|
||||
fn resolve(&self, _: &Path) -> io::Result<FileHash> {
|
||||
Err(io::ErrorKind::NotFound.into())
|
||||
}
|
||||
|
||||
fn file(&self, _: &Path) -> io::Result<Buffer> {
|
||||
Err(io::ErrorKind::NotFound.into())
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ use crate::util::SliceExt;
|
||||
|
||||
/// Embed all used fonts into the PDF.
|
||||
pub fn write_fonts(ctx: &mut PdfContext) {
|
||||
for &font_id in ctx.font_map.items() {
|
||||
for font in ctx.font_map.items() {
|
||||
let type0_ref = ctx.alloc.bump();
|
||||
let cid_ref = ctx.alloc.bump();
|
||||
let descriptor_ref = ctx.alloc.bump();
|
||||
@ -17,8 +17,7 @@ pub fn write_fonts(ctx: &mut PdfContext) {
|
||||
let data_ref = ctx.alloc.bump();
|
||||
ctx.font_refs.push(type0_ref);
|
||||
|
||||
let glyphs = &ctx.glyph_sets[&font_id];
|
||||
let font = ctx.fonts.get(font_id);
|
||||
let glyphs = &ctx.glyph_sets[font];
|
||||
let metrics = font.metrics();
|
||||
let ttf = font.ttf();
|
||||
|
||||
@ -161,7 +160,7 @@ pub fn write_fonts(ctx: &mut PdfContext) {
|
||||
.filter(Filter::FlateDecode);
|
||||
|
||||
// Subset and write the font's bytes.
|
||||
let data = font.buffer();
|
||||
let data = font.data();
|
||||
let subsetted = {
|
||||
let glyphs: Vec<_> = glyphs.iter().copied().collect();
|
||||
let profile = subsetter::Profile::pdf(&glyphs);
|
||||
|
@ -14,12 +14,11 @@ use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr};
|
||||
|
||||
use self::outline::{Heading, HeadingNode};
|
||||
use self::page::Page;
|
||||
use crate::font::{FontId, FontStore};
|
||||
use crate::font::Font;
|
||||
use crate::frame::Frame;
|
||||
use crate::geom::{Dir, Em, Length};
|
||||
use crate::image::Image;
|
||||
use crate::library::text::Lang;
|
||||
use crate::Context;
|
||||
|
||||
/// Export a collection of frames into a PDF file.
|
||||
///
|
||||
@ -28,8 +27,8 @@ use crate::Context;
|
||||
/// included in the PDF.
|
||||
///
|
||||
/// Returns the raw bytes making up the PDF file.
|
||||
pub fn pdf(ctx: &Context, frames: &[Frame]) -> Vec<u8> {
|
||||
let mut ctx = PdfContext::new(ctx);
|
||||
pub fn pdf(frames: &[Frame]) -> Vec<u8> {
|
||||
let mut ctx = PdfContext::new();
|
||||
page::construct_pages(&mut ctx, frames);
|
||||
font::write_fonts(&mut ctx);
|
||||
image::write_images(&mut ctx);
|
||||
@ -43,9 +42,8 @@ const SRGB: Name<'static> = Name(b"srgb");
|
||||
const D65_GRAY: Name<'static> = Name(b"d65gray");
|
||||
|
||||
/// Context for exporting a whole PDF document.
|
||||
pub struct PdfContext<'a> {
|
||||
pub struct PdfContext {
|
||||
writer: PdfWriter,
|
||||
fonts: &'a FontStore,
|
||||
pages: Vec<Page>,
|
||||
page_heights: Vec<f32>,
|
||||
alloc: Ref,
|
||||
@ -53,20 +51,19 @@ pub struct PdfContext<'a> {
|
||||
font_refs: Vec<Ref>,
|
||||
image_refs: Vec<Ref>,
|
||||
page_refs: Vec<Ref>,
|
||||
font_map: Remapper<FontId>,
|
||||
font_map: Remapper<Font>,
|
||||
image_map: Remapper<Image>,
|
||||
glyph_sets: HashMap<FontId, HashSet<u16>>,
|
||||
glyph_sets: HashMap<Font, HashSet<u16>>,
|
||||
languages: HashMap<Lang, usize>,
|
||||
heading_tree: Vec<HeadingNode>,
|
||||
}
|
||||
|
||||
impl<'a> PdfContext<'a> {
|
||||
fn new(ctx: &'a Context) -> Self {
|
||||
impl PdfContext {
|
||||
fn new() -> Self {
|
||||
let mut alloc = Ref::new(1);
|
||||
let page_tree_ref = alloc.bump();
|
||||
Self {
|
||||
writer: PdfWriter::new(),
|
||||
fonts: &ctx.fonts,
|
||||
pages: vec![],
|
||||
page_heights: vec![],
|
||||
alloc,
|
||||
|
@ -5,7 +5,7 @@ use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str};
|
||||
use super::{
|
||||
deflate, EmExt, Heading, HeadingNode, LengthExt, PdfContext, RefExt, D65_GRAY, SRGB,
|
||||
};
|
||||
use crate::font::FontId;
|
||||
use crate::font::Font;
|
||||
use crate::frame::{Destination, Element, Frame, Group, Role, Text};
|
||||
use crate::geom::{
|
||||
self, Color, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size, Stroke,
|
||||
@ -154,8 +154,8 @@ pub struct Page {
|
||||
}
|
||||
|
||||
/// An exporter for the contents of a single PDF page.
|
||||
struct PageContext<'a, 'b> {
|
||||
parent: &'a mut PdfContext<'b>,
|
||||
struct PageContext<'a> {
|
||||
parent: &'a mut PdfContext,
|
||||
page_ref: Ref,
|
||||
content: Content,
|
||||
state: State,
|
||||
@ -169,14 +169,14 @@ struct PageContext<'a, 'b> {
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct State {
|
||||
transform: Transform,
|
||||
font: Option<(FontId, Length)>,
|
||||
font: Option<(Font, Length)>,
|
||||
fill: Option<Paint>,
|
||||
fill_space: Option<Name<'static>>,
|
||||
stroke: Option<Stroke>,
|
||||
stroke_space: Option<Name<'static>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> PageContext<'a, 'b> {
|
||||
impl<'a> PageContext<'a> {
|
||||
fn save_state(&mut self) {
|
||||
self.saves.push(self.state.clone());
|
||||
self.content.save_state();
|
||||
@ -200,12 +200,12 @@ impl<'a, 'b> PageContext<'a, 'b> {
|
||||
]);
|
||||
}
|
||||
|
||||
fn set_font(&mut self, font_id: FontId, size: Length) {
|
||||
if self.state.font != Some((font_id, size)) {
|
||||
self.parent.font_map.insert(font_id);
|
||||
let name = format_eco!("F{}", self.parent.font_map.map(font_id));
|
||||
fn set_font(&mut self, font: &Font, size: Length) {
|
||||
if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) {
|
||||
self.parent.font_map.insert(font.clone());
|
||||
let name = format_eco!("F{}", self.parent.font_map.map(font.clone()));
|
||||
self.content.set_font(Name(name.as_bytes()), size.to_f32());
|
||||
self.state.font = Some((font_id, size));
|
||||
self.state.font = Some((font.clone(), size));
|
||||
}
|
||||
}
|
||||
|
||||
@ -328,17 +328,15 @@ fn write_text(ctx: &mut PageContext, x: f32, y: f32, text: &Text) {
|
||||
*ctx.parent.languages.entry(text.lang).or_insert(0) += text.glyphs.len();
|
||||
ctx.parent
|
||||
.glyph_sets
|
||||
.entry(text.font_id)
|
||||
.entry(text.font.clone())
|
||||
.or_default()
|
||||
.extend(text.glyphs.iter().map(|g| g.id));
|
||||
|
||||
let font = ctx.parent.fonts.get(text.font_id);
|
||||
|
||||
ctx.set_fill(text.fill);
|
||||
ctx.set_font(text.font_id, text.size);
|
||||
ctx.set_font(&text.font, text.size);
|
||||
ctx.content.begin_text();
|
||||
|
||||
// Position the text.
|
||||
// Positiosn the text.
|
||||
ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]);
|
||||
|
||||
let mut positioned = ctx.content.show_positioned();
|
||||
@ -363,7 +361,7 @@ fn write_text(ctx: &mut PageContext, x: f32, y: f32, text: &Text) {
|
||||
encoded.push((glyph.id >> 8) as u8);
|
||||
encoded.push((glyph.id & 0xff) as u8);
|
||||
|
||||
if let Some(advance) = font.advance(glyph.id) {
|
||||
if let Some(advance) = text.font.advance(glyph.id) {
|
||||
adjustment += glyph.x_advance - advance;
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,6 @@ use crate::geom::{
|
||||
self, Geometry, Length, Paint, PathElement, Shape, Size, Stroke, Transform,
|
||||
};
|
||||
use crate::image::{DecodedImage, Image};
|
||||
use crate::Context;
|
||||
|
||||
/// Export a frame into a rendered image.
|
||||
///
|
||||
@ -22,7 +21,7 @@ use crate::Context;
|
||||
///
|
||||
/// In addition to the frame, you need to pass in the context used during
|
||||
/// compilation so that fonts and images can be rendered.
|
||||
pub fn render(ctx: &Context, frame: &Frame, pixel_per_pt: f32) -> sk::Pixmap {
|
||||
pub fn render(frame: &Frame, pixel_per_pt: f32) -> sk::Pixmap {
|
||||
let size = frame.size();
|
||||
let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32;
|
||||
let pxh = (pixel_per_pt * size.y.to_f32()).round().max(1.0) as u32;
|
||||
@ -31,7 +30,7 @@ pub fn render(ctx: &Context, frame: &Frame, pixel_per_pt: f32) -> sk::Pixmap {
|
||||
canvas.fill(sk::Color::WHITE);
|
||||
|
||||
let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt);
|
||||
render_frame(&mut canvas, ts, None, ctx, frame);
|
||||
render_frame(&mut canvas, ts, None, frame);
|
||||
|
||||
canvas
|
||||
}
|
||||
@ -41,7 +40,6 @@ fn render_frame(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::ClipMask>,
|
||||
ctx: &Context,
|
||||
frame: &Frame,
|
||||
) {
|
||||
for (pos, element) in frame.elements() {
|
||||
@ -51,10 +49,10 @@ fn render_frame(
|
||||
|
||||
match element {
|
||||
Element::Group(group) => {
|
||||
render_group(canvas, ts, mask, ctx, group);
|
||||
render_group(canvas, ts, mask, group);
|
||||
}
|
||||
Element::Text(text) => {
|
||||
render_text(canvas, ts, mask, ctx, text);
|
||||
render_text(canvas, ts, mask, text);
|
||||
}
|
||||
Element::Shape(shape) => {
|
||||
render_shape(canvas, ts, mask, shape);
|
||||
@ -72,7 +70,6 @@ fn render_group(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::ClipMask>,
|
||||
ctx: &Context,
|
||||
group: &Group,
|
||||
) {
|
||||
let ts = ts.pre_concat(group.transform.into());
|
||||
@ -107,7 +104,7 @@ fn render_group(
|
||||
}
|
||||
}
|
||||
|
||||
render_frame(canvas, ts, mask, ctx, &group.frame);
|
||||
render_frame(canvas, ts, mask, &group.frame);
|
||||
}
|
||||
|
||||
/// Render a text run into the canvas.
|
||||
@ -115,7 +112,6 @@ fn render_text(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::ClipMask>,
|
||||
ctx: &Context,
|
||||
text: &Text,
|
||||
) {
|
||||
let mut x = 0.0;
|
||||
@ -124,9 +120,9 @@ fn render_text(
|
||||
let offset = x + glyph.x_offset.at(text.size).to_f32();
|
||||
let ts = ts.pre_translate(offset, 0.0);
|
||||
|
||||
render_svg_glyph(canvas, ts, mask, ctx, text, id)
|
||||
.or_else(|| render_bitmap_glyph(canvas, ts, mask, ctx, text, id))
|
||||
.or_else(|| render_outline_glyph(canvas, ts, mask, ctx, text, id));
|
||||
render_svg_glyph(canvas, ts, mask, text, id)
|
||||
.or_else(|| render_bitmap_glyph(canvas, ts, mask, text, id))
|
||||
.or_else(|| render_outline_glyph(canvas, ts, mask, text, id));
|
||||
|
||||
x += glyph.x_advance.at(text.size).to_f32();
|
||||
}
|
||||
@ -137,12 +133,10 @@ fn render_svg_glyph(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
_: Option<&sk::ClipMask>,
|
||||
ctx: &Context,
|
||||
text: &Text,
|
||||
id: GlyphId,
|
||||
) -> Option<()> {
|
||||
let font = ctx.fonts.get(text.font_id);
|
||||
let mut data = font.ttf().glyph_svg_image(id)?;
|
||||
let mut data = text.font.ttf().glyph_svg_image(id)?;
|
||||
|
||||
// Decompress SVGZ.
|
||||
let mut decoded = vec![];
|
||||
@ -164,7 +158,7 @@ fn render_svg_glyph(
|
||||
|
||||
// If there's no viewbox defined, use the em square for our scale
|
||||
// transformation ...
|
||||
let upem = font.units_per_em() as f32;
|
||||
let upem = text.font.units_per_em() as f32;
|
||||
let (mut width, mut height) = (upem, upem);
|
||||
|
||||
// ... but if there's a viewbox or width, use that.
|
||||
@ -188,14 +182,12 @@ fn render_bitmap_glyph(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::ClipMask>,
|
||||
ctx: &Context,
|
||||
text: &Text,
|
||||
id: GlyphId,
|
||||
) -> Option<()> {
|
||||
let size = text.size.to_f32();
|
||||
let ppem = size * ts.sy;
|
||||
let font = ctx.fonts.get(text.font_id);
|
||||
let raster = font.ttf().glyph_raster_image(id, ppem as u16)?;
|
||||
let raster = text.font.ttf().glyph_raster_image(id, ppem as u16)?;
|
||||
let ext = match raster.format {
|
||||
ttf_parser::RasterImageFormat::PNG => "png",
|
||||
};
|
||||
@ -217,7 +209,6 @@ fn render_outline_glyph(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
mask: Option<&sk::ClipMask>,
|
||||
ctx: &Context,
|
||||
text: &Text,
|
||||
id: GlyphId,
|
||||
) -> Option<()> {
|
||||
@ -227,10 +218,9 @@ fn render_outline_glyph(
|
||||
// rasterization can't be used due to very large text size or weird
|
||||
// scale/skewing transforms.
|
||||
if ppem > 100.0 || ts.kx != 0.0 || ts.ky != 0.0 || ts.sx != ts.sy {
|
||||
let font = ctx.fonts.get(text.font_id);
|
||||
let path = {
|
||||
let mut builder = WrappedPathBuilder(sk::PathBuilder::new());
|
||||
font.ttf().outline_glyph(id, &mut builder)?;
|
||||
text.font.ttf().outline_glyph(id, &mut builder)?;
|
||||
builder.0.finish()?
|
||||
};
|
||||
|
||||
@ -239,7 +229,7 @@ fn render_outline_glyph(
|
||||
|
||||
// Flip vertically because font design coordinate
|
||||
// system is Y-up.
|
||||
let scale = text.size.to_f32() / font.units_per_em() as f32;
|
||||
let scale = text.size.to_f32() / text.font.units_per_em() as f32;
|
||||
let ts = ts.pre_scale(scale, -scale);
|
||||
canvas.fill_path(&path, &paint, rule, ts, mask)?;
|
||||
return Some(());
|
||||
@ -248,9 +238,8 @@ fn render_outline_glyph(
|
||||
// Rasterize the glyph with `pixglyph`.
|
||||
// Try to retrieve a prepared glyph or prepare it from scratch if it
|
||||
// doesn't exist, yet.
|
||||
let glyph = pixglyph::Glyph::load(ctx.fonts.get(text.font_id).ttf(), id)?;
|
||||
let glyph = pixglyph::Glyph::load(text.font.ttf(), id)?;
|
||||
let bitmap = glyph.rasterize(ts.tx, ts.ty, ppem);
|
||||
|
||||
let cw = canvas.width() as i32;
|
||||
let ch = canvas.height() as i32;
|
||||
let mw = bitmap.width as i32;
|
||||
|
975
src/font.rs
975
src/font.rs
@ -1,975 +0,0 @@
|
||||
//! Font handling.
|
||||
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::{hash_map::Entry, BTreeMap, HashMap};
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use rex::font::MathHeader;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ttf_parser::{name_id, GlyphId, PlatformId, Tag};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::geom::Em;
|
||||
use crate::loading::{Buffer, FileHash, Loader};
|
||||
|
||||
/// A unique identifier for a loaded font.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct FontId(u32);
|
||||
|
||||
impl FontId {
|
||||
/// Create a font id from the raw underlying value.
|
||||
///
|
||||
/// This should only be called with values returned by
|
||||
/// [`into_raw`](Self::into_raw).
|
||||
pub const fn from_raw(v: u32) -> Self {
|
||||
Self(v)
|
||||
}
|
||||
|
||||
/// Convert into the raw underlying value.
|
||||
pub const fn into_raw(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage for loaded and parsed fonts.
|
||||
pub struct FontStore {
|
||||
loader: Arc<dyn Loader>,
|
||||
failed: Vec<bool>,
|
||||
fonts: Vec<Option<Font>>,
|
||||
families: BTreeMap<String, Vec<FontId>>,
|
||||
buffers: HashMap<FileHash, Buffer>,
|
||||
}
|
||||
|
||||
impl FontStore {
|
||||
/// Create a new, empty font store.
|
||||
pub fn new(loader: Arc<dyn Loader>) -> Self {
|
||||
let mut fonts = vec![];
|
||||
let mut failed = vec![];
|
||||
let mut families = BTreeMap::<String, Vec<FontId>>::new();
|
||||
|
||||
let infos = loader.fonts();
|
||||
for (i, info) in infos.iter().enumerate() {
|
||||
let id = FontId(i as u32);
|
||||
fonts.push(None);
|
||||
failed.push(false);
|
||||
families.entry(info.family.to_lowercase()).or_default().push(id);
|
||||
}
|
||||
|
||||
for fonts in families.values_mut() {
|
||||
fonts.sort_by_key(|id| infos[id.0 as usize].variant);
|
||||
fonts.dedup_by_key(|id| infos[id.0 as usize].variant);
|
||||
}
|
||||
|
||||
Self {
|
||||
loader,
|
||||
fonts,
|
||||
failed,
|
||||
families,
|
||||
buffers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// An ordered iterator over all font families this loader knows and details
|
||||
/// about the fonts that are part of them.
|
||||
pub fn families(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&str, impl Iterator<Item = &FontInfo>)> + '_ {
|
||||
// Since the keys are lowercased, we instead use the family field of the
|
||||
// first font's info.
|
||||
let fonts = self.loader.fonts();
|
||||
self.families.values().map(|ids| {
|
||||
let family = fonts[ids[0].0 as usize].family.as_str();
|
||||
let infos = ids.iter().map(|&id| &fonts[id.0 as usize]);
|
||||
(family, infos)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a reference to a loaded font.
|
||||
///
|
||||
/// This panics if the font with this `id` was not loaded. This function
|
||||
/// should only be called with ids returned by this store's
|
||||
/// [`select()`](Self::select) and
|
||||
/// [`select_fallback()`](Self::select_fallback) methods.
|
||||
#[track_caller]
|
||||
pub fn get(&self, id: FontId) -> &Font {
|
||||
self.fonts[id.0 as usize].as_ref().expect("font was not loaded")
|
||||
}
|
||||
|
||||
/// Try to find and load a font from the given `family` that matches
|
||||
/// the given `variant` as closely as possible.
|
||||
pub fn select(&mut self, family: &str, variant: FontVariant) -> Option<FontId> {
|
||||
let ids = self.families.get(family)?;
|
||||
let id = self.find_best_variant(None, variant, ids.iter().copied())?;
|
||||
self.load(id)
|
||||
}
|
||||
|
||||
/// Try to find and load a fallback font that
|
||||
/// - is as close as possible to the font `like` (if any)
|
||||
/// - is as close as possible to the given `variant`
|
||||
/// - is suitable for shaping the given `text`
|
||||
pub fn select_fallback(
|
||||
&mut self,
|
||||
like: Option<FontId>,
|
||||
variant: FontVariant,
|
||||
text: &str,
|
||||
) -> Option<FontId> {
|
||||
// Find the fonts that contain the text's first char ...
|
||||
let c = text.chars().next()?;
|
||||
let ids = self
|
||||
.loader
|
||||
.fonts()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, info)| info.coverage.contains(c as u32))
|
||||
.map(|(i, _)| FontId(i as u32));
|
||||
|
||||
// ... and find the best variant among them.
|
||||
let id = self.find_best_variant(like, variant, ids)?;
|
||||
self.load(id)
|
||||
}
|
||||
|
||||
/// Find the font in the passed iterator that
|
||||
/// - is closest to the font `like` (if any)
|
||||
/// - is closest to the given `variant`
|
||||
///
|
||||
/// To do that we compute a key for all variants and select the one with the
|
||||
/// minimal key. This key prioritizes:
|
||||
/// - If `like` is some other font:
|
||||
/// - Are both fonts (not) monospaced?
|
||||
/// - Do both fonts (not) have serifs?
|
||||
/// - How many words do the families share in their prefix? E.g. "Noto
|
||||
/// Sans" and "Noto Sans Arabic" share two words, whereas "IBM Plex
|
||||
/// Arabic" shares none with "Noto Sans", so prefer "Noto Sans Arabic"
|
||||
/// if `like` is "Noto Sans". In case there are two equally good
|
||||
/// matches, we prefer the shorter one because it is less special (e.g.
|
||||
/// if `like` is "Noto Sans Arabic", we prefer "Noto Sans" over "Noto
|
||||
/// Sans CJK HK".)
|
||||
/// - The style (normal / italic / oblique). If we want italic or oblique
|
||||
/// but it doesn't exist, the other one of the two is still better than
|
||||
/// normal.
|
||||
/// - The absolute distance to the target stretch.
|
||||
/// - The absolute distance to the target weight.
|
||||
fn find_best_variant(
|
||||
&self,
|
||||
like: Option<FontId>,
|
||||
variant: FontVariant,
|
||||
ids: impl IntoIterator<Item = FontId>,
|
||||
) -> Option<FontId> {
|
||||
let infos = self.loader.fonts();
|
||||
let like = like.map(|id| &infos[id.0 as usize]);
|
||||
|
||||
let mut best = None;
|
||||
let mut best_key = None;
|
||||
|
||||
for id in ids {
|
||||
let current = &infos[id.0 as usize];
|
||||
|
||||
let key = (
|
||||
like.map(|like| {
|
||||
(
|
||||
current.monospaced != like.monospaced,
|
||||
like.serif.is_some() && current.serif != like.serif,
|
||||
Reverse(shared_prefix_words(¤t.family, &like.family)),
|
||||
current.family.len(),
|
||||
)
|
||||
}),
|
||||
current.variant.style.distance(variant.style),
|
||||
current.variant.stretch.distance(variant.stretch),
|
||||
current.variant.weight.distance(variant.weight),
|
||||
);
|
||||
|
||||
if best_key.map_or(true, |b| key < b) {
|
||||
best = Some(id);
|
||||
best_key = Some(key);
|
||||
}
|
||||
}
|
||||
|
||||
best
|
||||
}
|
||||
|
||||
/// Load the font with the given id.
|
||||
///
|
||||
/// Returns `Some(id)` if the font was loaded successfully.
|
||||
fn load(&mut self, id: FontId) -> Option<FontId> {
|
||||
let idx = id.0 as usize;
|
||||
let slot = &mut self.fonts[idx];
|
||||
if slot.is_some() {
|
||||
return Some(id);
|
||||
}
|
||||
|
||||
if self.failed[idx] {
|
||||
return None;
|
||||
}
|
||||
|
||||
let FontInfo { ref path, index, .. } = self.loader.fonts()[idx];
|
||||
self.failed[idx] = true;
|
||||
|
||||
// Check the buffer cache since multiple fonts may refer to the same
|
||||
// data (font collection).
|
||||
let hash = self.loader.resolve(path).ok()?;
|
||||
let buffer = match self.buffers.entry(hash) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
let buffer = self.loader.load(path).ok()?;
|
||||
entry.insert(buffer)
|
||||
}
|
||||
};
|
||||
|
||||
let font = Font::new(buffer.clone(), index)?;
|
||||
*slot = Some(font);
|
||||
self.failed[idx] = false;
|
||||
|
||||
Some(id)
|
||||
}
|
||||
}
|
||||
|
||||
/// How many words the two strings share in their prefix.
|
||||
fn shared_prefix_words(left: &str, right: &str) -> usize {
|
||||
left.unicode_words()
|
||||
.zip(right.unicode_words())
|
||||
.take_while(|(l, r)| l == r)
|
||||
.count()
|
||||
}
|
||||
|
||||
/// An OpenType font.
|
||||
pub struct Font {
|
||||
/// The raw font data, possibly shared with other fonts from the same
|
||||
/// collection. The vector's allocation must not move, because `ttf` points
|
||||
/// into it using unsafe code.
|
||||
data: Buffer,
|
||||
/// The font's index in the collection (zero if not a collection).
|
||||
index: u32,
|
||||
/// The underlying ttf-parser/rustybuzz face.
|
||||
ttf: rustybuzz::Face<'static>,
|
||||
/// The font's metrics.
|
||||
metrics: FontMetrics,
|
||||
/// The parsed ReX math header.
|
||||
math: OnceCell<Option<MathHeader>>,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
/// Parse a font from data and collection index.
|
||||
pub fn new(data: 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 have a strong ref to the `Arc`.
|
||||
// - 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(data.as_ptr(), data.len()) };
|
||||
|
||||
let ttf = rustybuzz::Face::from_slice(slice, index)?;
|
||||
let metrics = FontMetrics::from_ttf(&ttf);
|
||||
|
||||
Some(Self {
|
||||
data,
|
||||
index,
|
||||
ttf,
|
||||
metrics,
|
||||
math: OnceCell::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The underlying buffer.
|
||||
pub fn buffer(&self) -> &Buffer {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// The collection index.
|
||||
pub fn index(&self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
|
||||
/// 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.ttf
|
||||
}
|
||||
|
||||
/// The number of font units per one em.
|
||||
pub fn units_per_em(&self) -> f64 {
|
||||
self.metrics.units_per_em
|
||||
}
|
||||
|
||||
/// Access the font's metrics.
|
||||
pub fn metrics(&self) -> &FontMetrics {
|
||||
&self.metrics
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
|
||||
/// Look up the horizontal advance width of a glyph.
|
||||
pub fn advance(&self, glyph: u16) -> Option<Em> {
|
||||
self.ttf
|
||||
.glyph_hor_advance(GlyphId(glyph))
|
||||
.map(|units| self.to_em(units))
|
||||
}
|
||||
|
||||
/// Access the math header, if any.
|
||||
pub fn math(&self) -> Option<&MathHeader> {
|
||||
self.math
|
||||
.get_or_init(|| {
|
||||
let data = self.ttf().table_data(Tag::from_bytes(b"MATH"))?;
|
||||
MathHeader::parse(data).ok()
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
/// Lookup a name by id.
|
||||
pub fn find_name(&self, name_id: u16) -> Option<String> {
|
||||
find_name_ttf(&self.ttf, name_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics for a font.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct FontMetrics {
|
||||
/// How many font units represent one em unit.
|
||||
pub units_per_em: f64,
|
||||
/// The distance from the baseline to the typographic ascender.
|
||||
pub ascender: Em,
|
||||
/// The approximate height of uppercase letters.
|
||||
pub cap_height: Em,
|
||||
/// The approximate height of non-ascending lowercase letters.
|
||||
pub x_height: Em,
|
||||
/// The distance from the baseline to the typographic descender.
|
||||
pub descender: Em,
|
||||
/// Recommended metrics for a strikethrough line.
|
||||
pub strikethrough: LineMetrics,
|
||||
/// Recommended metrics for an underline.
|
||||
pub underline: LineMetrics,
|
||||
/// Recommended metrics for an overline.
|
||||
pub overline: LineMetrics,
|
||||
}
|
||||
|
||||
impl FontMetrics {
|
||||
/// Extract the font's metrics.
|
||||
pub fn from_ttf(ttf: &ttf_parser::Face) -> Self {
|
||||
let units_per_em = f64::from(ttf.units_per_em());
|
||||
let to_em = |units| Em::from_units(units, units_per_em);
|
||||
|
||||
let ascender = to_em(ttf.typographic_ascender().unwrap_or(ttf.ascender()));
|
||||
let cap_height = ttf.capital_height().filter(|&h| h > 0).map_or(ascender, to_em);
|
||||
let x_height = ttf.x_height().filter(|&h| h > 0).map_or(ascender, to_em);
|
||||
let descender = to_em(ttf.typographic_descender().unwrap_or(ttf.descender()));
|
||||
let strikeout = ttf.strikeout_metrics();
|
||||
let underline = ttf.underline_metrics();
|
||||
|
||||
let strikethrough = LineMetrics {
|
||||
position: strikeout.map_or(Em::new(0.25), |s| to_em(s.position)),
|
||||
thickness: strikeout
|
||||
.or(underline)
|
||||
.map_or(Em::new(0.06), |s| to_em(s.thickness)),
|
||||
};
|
||||
|
||||
let underline = LineMetrics {
|
||||
position: underline.map_or(Em::new(-0.2), |s| to_em(s.position)),
|
||||
thickness: underline
|
||||
.or(strikeout)
|
||||
.map_or(Em::new(0.06), |s| to_em(s.thickness)),
|
||||
};
|
||||
|
||||
let overline = LineMetrics {
|
||||
position: cap_height + Em::new(0.1),
|
||||
thickness: underline.thickness,
|
||||
};
|
||||
|
||||
Self {
|
||||
units_per_em,
|
||||
ascender,
|
||||
cap_height,
|
||||
x_height,
|
||||
descender,
|
||||
strikethrough,
|
||||
underline,
|
||||
overline,
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up a vertical metric.
|
||||
pub fn vertical(&self, metric: VerticalFontMetric) -> Em {
|
||||
match metric {
|
||||
VerticalFontMetric::Ascender => self.ascender,
|
||||
VerticalFontMetric::CapHeight => self.cap_height,
|
||||
VerticalFontMetric::XHeight => self.x_height,
|
||||
VerticalFontMetric::Baseline => Em::zero(),
|
||||
VerticalFontMetric::Descender => self.descender,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics for a decorative line.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct LineMetrics {
|
||||
/// The vertical offset of the line from the baseline. Positive goes
|
||||
/// upwards, negative downwards.
|
||||
pub position: Em,
|
||||
/// The thickness of the line.
|
||||
pub thickness: Em,
|
||||
}
|
||||
|
||||
/// Identifies a vertical metric of a font.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum VerticalFontMetric {
|
||||
/// The distance from the baseline to the typographic ascender.
|
||||
///
|
||||
/// Corresponds to the typographic ascender from the `OS/2` table if present
|
||||
/// and falls back to the ascender from the `hhea` table otherwise.
|
||||
Ascender,
|
||||
/// The approximate height of uppercase letters.
|
||||
CapHeight,
|
||||
/// The approximate height of non-ascending lowercase letters.
|
||||
XHeight,
|
||||
/// The baseline on which the letters rest.
|
||||
Baseline,
|
||||
/// The distance from the baseline to the typographic descender.
|
||||
///
|
||||
/// Corresponds to the typographic descender from the `OS/2` table if
|
||||
/// present and falls back to the descender from the `hhea` table otherwise.
|
||||
Descender,
|
||||
}
|
||||
|
||||
/// Properties of a single font.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FontInfo {
|
||||
/// The path to the font file.
|
||||
pub path: PathBuf,
|
||||
/// The collection index in the font file.
|
||||
pub index: u32,
|
||||
/// The typographic font family this font is part of.
|
||||
pub family: String,
|
||||
/// Properties that distinguish this font from other fonts in the same
|
||||
/// family.
|
||||
pub variant: FontVariant,
|
||||
/// Whether the font is monospaced.
|
||||
pub monospaced: bool,
|
||||
/// Whether the font has serifs (if known).
|
||||
pub serif: Option<bool>,
|
||||
/// The unicode coverage of the font.
|
||||
pub coverage: Coverage,
|
||||
}
|
||||
|
||||
impl FontInfo {
|
||||
/// Compute metadata for all fonts in the given data.
|
||||
pub fn from_data<'a>(
|
||||
path: &'a Path,
|
||||
data: &'a [u8],
|
||||
) -> impl Iterator<Item = FontInfo> + 'a {
|
||||
let count = ttf_parser::fonts_in_collection(data).unwrap_or(1);
|
||||
(0 .. count).filter_map(move |index| {
|
||||
let ttf = ttf_parser::Face::from_slice(data, index).ok()?;
|
||||
Self::from_ttf(path, index, &ttf)
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute metadata for a single ttf-parser face.
|
||||
pub fn from_ttf(path: &Path, index: u32, ttf: &ttf_parser::Face) -> Option<Self> {
|
||||
// We cannot use Name ID 16 "Typographic Family", because for some
|
||||
// fonts it groups together more than just Style / Weight / Stretch
|
||||
// variants (e.g. Display variants of Noto fonts) and then some
|
||||
// variants become inaccessible from Typst. And even though the
|
||||
// fsSelection bit WWS should help us decide whether that is the
|
||||
// case, it's wrong for some fonts (e.g. for certain variants of "Noto
|
||||
// Sans Display").
|
||||
//
|
||||
// So, instead we use Name ID 1 "Family" and trim many common
|
||||
// suffixes for which know that they just describe styling (e.g.
|
||||
// "ExtraBold").
|
||||
//
|
||||
// Also, for Noto fonts we use Name ID 4 "Full Name" instead,
|
||||
// because Name ID 1 "Family" sometimes contains "Display" and
|
||||
// sometimes doesn't for the Display variants and that mixes things
|
||||
// up.
|
||||
let family = {
|
||||
let mut family = find_name_ttf(ttf, name_id::FAMILY)?;
|
||||
if family.starts_with("Noto") {
|
||||
family = find_name_ttf(ttf, name_id::FULL_NAME)?;
|
||||
}
|
||||
trim_styles(&family).to_string()
|
||||
};
|
||||
|
||||
let variant = {
|
||||
let mut full = find_name_ttf(ttf, name_id::FULL_NAME).unwrap_or_default();
|
||||
full.make_ascii_lowercase();
|
||||
|
||||
// Some fonts miss the relevant bits for italic or oblique, so
|
||||
// we also try to infer that from the full name.
|
||||
let italic = ttf.is_italic() || full.contains("italic");
|
||||
let oblique =
|
||||
ttf.is_oblique() || full.contains("oblique") || full.contains("slanted");
|
||||
|
||||
let style = match (italic, oblique) {
|
||||
(false, false) => FontStyle::Normal,
|
||||
(true, _) => FontStyle::Italic,
|
||||
(_, true) => FontStyle::Oblique,
|
||||
};
|
||||
|
||||
let weight = FontWeight::from_number(ttf.weight().to_number());
|
||||
let stretch = FontStretch::from_number(ttf.width().to_number());
|
||||
|
||||
FontVariant { style, weight, stretch }
|
||||
};
|
||||
|
||||
// Determine the unicode coverage.
|
||||
let mut codepoints = vec![];
|
||||
for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) {
|
||||
if subtable.is_unicode() {
|
||||
subtable.codepoints(|c| codepoints.push(c));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether this is a serif or sans-serif font.
|
||||
let mut serif = None;
|
||||
if let Some(panose) = ttf
|
||||
.table_data(Tag::from_bytes(b"OS/2"))
|
||||
.and_then(|os2| os2.get(32 .. 45))
|
||||
{
|
||||
match panose {
|
||||
[2, 2 ..= 10, ..] => serif = Some(true),
|
||||
[2, 11 ..= 15, ..] => serif = Some(false),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Some(FontInfo {
|
||||
path: path.to_owned(),
|
||||
index,
|
||||
family,
|
||||
variant,
|
||||
monospaced: ttf.is_monospaced(),
|
||||
serif,
|
||||
coverage: Coverage::from_vec(codepoints),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to find and decode the name with the given id.
|
||||
fn find_name_ttf(ttf: &ttf_parser::Face, name_id: u16) -> Option<String> {
|
||||
ttf.names().into_iter().find_map(|entry| {
|
||||
if entry.name_id == name_id {
|
||||
if let Some(string) = entry.to_string() {
|
||||
return Some(string);
|
||||
}
|
||||
|
||||
if entry.platform_id == PlatformId::Macintosh && entry.encoding_id == 0 {
|
||||
return Some(decode_mac_roman(entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Decode mac roman encoded bytes into a string.
|
||||
fn decode_mac_roman(coded: &[u8]) -> String {
|
||||
#[rustfmt::skip]
|
||||
const TABLE: [char; 128] = [
|
||||
'Ä', 'Å', 'Ç', 'É', 'Ñ', 'Ö', 'Ü', 'á', 'à', 'â', 'ä', 'ã', 'å', 'ç', 'é', 'è',
|
||||
'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ñ', 'ó', 'ò', 'ô', 'ö', 'õ', 'ú', 'ù', 'û', 'ü',
|
||||
'†', '°', '¢', '£', '§', '•', '¶', 'ß', '®', '©', '™', '´', '¨', '≠', 'Æ', 'Ø',
|
||||
'∞', '±', '≤', '≥', '¥', 'µ', '∂', '∑', '∏', 'π', '∫', 'ª', 'º', 'Ω', 'æ', 'ø',
|
||||
'¿', '¡', '¬', '√', 'ƒ', '≈', '∆', '«', '»', '…', '\u{a0}', 'À', 'Ã', 'Õ', 'Œ', 'œ',
|
||||
'–', '—', '“', '”', '‘', '’', '÷', '◊', 'ÿ', 'Ÿ', '⁄', '€', '‹', '›', 'fi', 'fl',
|
||||
'‡', '·', '‚', '„', '‰', 'Â', 'Ê', 'Á', 'Ë', 'È', 'Í', 'Î', 'Ï', 'Ì', 'Ó', 'Ô',
|
||||
'\u{f8ff}', 'Ò', 'Ú', 'Û', 'Ù', 'ı', 'ˆ', '˜', '¯', '˘', '˙', '˚', '¸', '˝', '˛', 'ˇ',
|
||||
];
|
||||
|
||||
fn char_from_mac_roman(code: u8) -> char {
|
||||
if code < 128 {
|
||||
code as char
|
||||
} else {
|
||||
TABLE[(code - 128) as usize]
|
||||
}
|
||||
}
|
||||
|
||||
coded.iter().copied().map(char_from_mac_roman).collect()
|
||||
}
|
||||
|
||||
/// Trim style naming from a family name.
|
||||
fn trim_styles(mut family: &str) -> &str {
|
||||
// Separators between names, modifiers and styles.
|
||||
const SEPARATORS: [char; 3] = [' ', '-', '_'];
|
||||
|
||||
// Modifiers that can appear in combination with suffixes.
|
||||
const MODIFIERS: &[&str] = &[
|
||||
"extra", "ext", "ex", "x", "semi", "sem", "sm", "demi", "dem", "ultra",
|
||||
];
|
||||
|
||||
// Style suffixes.
|
||||
#[rustfmt::skip]
|
||||
const SUFFIXES: &[&str] = &[
|
||||
"normal", "italic", "oblique", "slanted",
|
||||
"thin", "th", "hairline", "light", "lt", "regular", "medium", "med",
|
||||
"md", "bold", "bd", "demi", "extb", "black", "blk", "bk", "heavy",
|
||||
"narrow", "condensed", "cond", "cn", "cd", "compressed", "expanded", "exp"
|
||||
];
|
||||
|
||||
// Trim spacing and weird leading dots in Apple fonts.
|
||||
family = family.trim().trim_start_matches('.');
|
||||
|
||||
// Lowercase the string so that the suffixes match case-insensitively.
|
||||
let lower = family.to_ascii_lowercase();
|
||||
let mut len = usize::MAX;
|
||||
let mut trimmed = lower.as_str();
|
||||
|
||||
// Trim style suffixes repeatedly.
|
||||
while trimmed.len() < len {
|
||||
len = trimmed.len();
|
||||
|
||||
// Find style suffix.
|
||||
let mut t = match SUFFIXES.iter().find_map(|s| trimmed.strip_suffix(s)) {
|
||||
Some(t) => t,
|
||||
None => break,
|
||||
};
|
||||
|
||||
// Strip optional separator.
|
||||
if let Some(s) = t.strip_suffix(SEPARATORS) {
|
||||
trimmed = s;
|
||||
t = s;
|
||||
}
|
||||
|
||||
// Also allow an extra modifier, but apply it only if it is separated it
|
||||
// from the text before it (to prevent false positives).
|
||||
if let Some(t) = MODIFIERS.iter().find_map(|s| t.strip_suffix(s)) {
|
||||
if let Some(stripped) = t.strip_suffix(SEPARATORS) {
|
||||
trimmed = stripped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&family[.. len]
|
||||
}
|
||||
|
||||
/// Properties that distinguish a font from other fonts in the same family.
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FontVariant {
|
||||
/// The style of the font (normal / italic / oblique).
|
||||
pub style: FontStyle,
|
||||
/// How heavy the font is (100 - 900).
|
||||
pub weight: FontWeight,
|
||||
/// How condensed or expanded the font 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 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FontVariant {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}-{:?}-{:?}", self.style, self.weight, self.stretch)
|
||||
}
|
||||
}
|
||||
|
||||
/// The style of a font.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum FontStyle {
|
||||
/// The default style.
|
||||
Normal,
|
||||
/// A cursive style.
|
||||
Italic,
|
||||
/// A slanted style.
|
||||
Oblique,
|
||||
}
|
||||
|
||||
impl FontStyle {
|
||||
/// The conceptual distance between the styles, expressed as a number.
|
||||
pub fn distance(self, other: Self) -> u16 {
|
||||
if self == other {
|
||||
0
|
||||
} else if self != Self::Normal && other != Self::Normal {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FontStyle {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
}
|
||||
|
||||
/// The weight of a font.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[derive(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))
|
||||
}
|
||||
|
||||
/// The number between 100 and 900.
|
||||
pub fn to_number(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// 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 Debug for FontWeight {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The width of a font.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct FontStretch(u16);
|
||||
|
||||
impl FontStretch {
|
||||
/// Ultra-condensed stretch (50%).
|
||||
pub const ULTRA_CONDENSED: Self = Self(500);
|
||||
|
||||
/// Extra-condensed stretch weight (62.5%).
|
||||
pub const EXTRA_CONDENSED: Self = Self(625);
|
||||
|
||||
/// Condensed stretch (75%).
|
||||
pub const CONDENSED: Self = Self(750);
|
||||
|
||||
/// Semi-condensed stretch (87.5%).
|
||||
pub const SEMI_CONDENSED: Self = Self(875);
|
||||
|
||||
/// Normal stretch (100%).
|
||||
pub const NORMAL: Self = Self(1000);
|
||||
|
||||
/// Semi-expanded stretch (112.5%).
|
||||
pub const SEMI_EXPANDED: Self = Self(1125);
|
||||
|
||||
/// Expanded stretch (125%).
|
||||
pub const EXPANDED: Self = Self(1250);
|
||||
|
||||
/// Extra-expanded stretch (150%).
|
||||
pub const EXTRA_EXPANDED: Self = Self(1500);
|
||||
|
||||
/// Ultra-expanded stretch (200%).
|
||||
pub const ULTRA_EXPANDED: Self = Self(2000);
|
||||
|
||||
/// 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) * 1000.0) as u16)
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// The ratio between 0.5 and 2.0 corresponding to this stretch.
|
||||
pub fn to_ratio(self) -> f32 {
|
||||
self.0 as f32 / 1000.0
|
||||
}
|
||||
|
||||
/// The absolute ratio distance between this and another font stretch.
|
||||
pub fn distance(self, other: Self) -> f32 {
|
||||
(self.to_ratio() - other.to_ratio()).abs()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FontStretch {
|
||||
fn default() -> Self {
|
||||
Self::NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FontStretch {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}%", 100.0 * self.to_ratio())
|
||||
}
|
||||
}
|
||||
|
||||
/// A compactly encoded set of codepoints.
|
||||
///
|
||||
/// The set is represented by alternating specifications of how many codepoints
|
||||
/// are not in the set and how many are in the set.
|
||||
///
|
||||
/// For example, for the set `{2, 3, 4, 9, 10, 11, 15, 18, 19}`, there are:
|
||||
/// - 2 codepoints not inside (0, 1)
|
||||
/// - 3 codepoints inside (2, 3, 4)
|
||||
/// - 4 codepoints not inside (5, 6, 7, 8)
|
||||
/// - 3 codepoints inside (9, 10, 11)
|
||||
/// - 3 codepoints not inside (12, 13, 14)
|
||||
/// - 1 codepoint inside (15)
|
||||
/// - 2 codepoints not inside (16, 17)
|
||||
/// - 2 codepoints inside (18, 19)
|
||||
///
|
||||
/// So the resulting encoding is `[2, 3, 4, 3, 3, 1, 2, 2]`.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Coverage(Vec<u32>);
|
||||
|
||||
impl Coverage {
|
||||
/// Encode a vector of codepoints.
|
||||
pub fn from_vec(mut codepoints: Vec<u32>) -> Self {
|
||||
codepoints.sort();
|
||||
codepoints.dedup();
|
||||
|
||||
let mut runs = Vec::new();
|
||||
let mut next = 0;
|
||||
|
||||
for c in codepoints {
|
||||
if let Some(run) = runs.last_mut().filter(|_| c == next) {
|
||||
*run += 1;
|
||||
} else {
|
||||
runs.push(c - next);
|
||||
runs.push(1);
|
||||
}
|
||||
|
||||
next = c + 1;
|
||||
}
|
||||
|
||||
Self(runs)
|
||||
}
|
||||
|
||||
/// Whether the codepoint is covered.
|
||||
pub fn contains(&self, c: u32) -> bool {
|
||||
let mut inside = false;
|
||||
let mut cursor = 0;
|
||||
|
||||
for &run in &self.0 {
|
||||
if (cursor .. cursor + run).contains(&c) {
|
||||
return inside;
|
||||
}
|
||||
cursor += run;
|
||||
inside = !inside;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_font_stretch_debug() {
|
||||
assert_eq!(format!("{:?}", FontStretch::EXPANDED), "125%")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trim_styles() {
|
||||
assert_eq!(trim_styles("Atma Light"), "Atma");
|
||||
assert_eq!(trim_styles("eras bold"), "eras");
|
||||
assert_eq!(trim_styles("footlight mt light"), "footlight mt");
|
||||
assert_eq!(trim_styles("times new roman"), "times new roman");
|
||||
assert_eq!(trim_styles("noto sans mono cond sembd"), "noto sans mono");
|
||||
assert_eq!(trim_styles("noto serif SEMCOND sembd"), "noto serif");
|
||||
assert_eq!(trim_styles("crimson text"), "crimson text");
|
||||
assert_eq!(trim_styles("footlight light"), "footlight");
|
||||
assert_eq!(trim_styles("Noto Sans"), "Noto Sans");
|
||||
assert_eq!(trim_styles("Noto Sans Light"), "Noto Sans");
|
||||
assert_eq!(trim_styles("Noto Sans Semicondensed Heavy"), "Noto Sans");
|
||||
assert_eq!(trim_styles("Familx"), "Familx");
|
||||
assert_eq!(trim_styles("Font Ultra"), "Font Ultra");
|
||||
assert_eq!(trim_styles("Font Ultra Bold"), "Font");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coverage() {
|
||||
#[track_caller]
|
||||
fn test(set: &[u32], runs: &[u32]) {
|
||||
let coverage = Coverage::from_vec(set.to_vec());
|
||||
assert_eq!(coverage.0, runs);
|
||||
|
||||
let max = 5 + set.iter().copied().max().unwrap_or_default();
|
||||
for c in 0 .. max {
|
||||
assert_eq!(set.contains(&c), coverage.contains(c));
|
||||
}
|
||||
}
|
||||
|
||||
test(&[], &[]);
|
||||
test(&[0], &[0, 1]);
|
||||
test(&[1], &[1, 1]);
|
||||
test(&[0, 1], &[0, 2]);
|
||||
test(&[0, 1, 3], &[0, 2, 1, 1]);
|
||||
test(
|
||||
// {2, 3, 4, 9, 10, 11, 15, 18, 19}
|
||||
&[18, 19, 2, 4, 9, 11, 15, 3, 3, 10],
|
||||
&[2, 3, 4, 3, 3, 1, 2, 2],
|
||||
)
|
||||
}
|
||||
}
|
476
src/font/book.rs
Normal file
476
src/font/book.rs
Normal file
@ -0,0 +1,476 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ttf_parser::{name_id, PlatformId, Tag};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::{Font, FontStretch, FontStyle, FontVariant, FontWeight};
|
||||
|
||||
/// Metadata about a collection of fonts.
|
||||
#[derive(Default)]
|
||||
pub struct FontBook {
|
||||
/// Maps from lowercased family names to font indices.
|
||||
families: BTreeMap<String, Vec<usize>>,
|
||||
/// Metadata about each font in the collection.
|
||||
infos: Vec<FontInfo>,
|
||||
}
|
||||
|
||||
impl FontBook {
|
||||
/// Create a new, empty font book.
|
||||
pub fn new() -> Self {
|
||||
Self { families: BTreeMap::new(), infos: vec![] }
|
||||
}
|
||||
|
||||
/// Create a font book for a collection of fonts.
|
||||
pub fn from_fonts<'a>(fonts: impl IntoIterator<Item = &'a Font>) -> Self {
|
||||
let mut book = Self::new();
|
||||
for font in fonts {
|
||||
book.push(font.info().clone());
|
||||
}
|
||||
book
|
||||
}
|
||||
|
||||
/// Insert metadata into the font book.
|
||||
pub fn push(&mut self, info: FontInfo) {
|
||||
let index = self.infos.len();
|
||||
let family = info.family.to_lowercase();
|
||||
self.families.entry(family).or_default().push(index);
|
||||
self.infos.push(info);
|
||||
}
|
||||
|
||||
/// An ordered iterator over all font families this loader knows and details
|
||||
/// about the faces that are part of them.
|
||||
pub fn families(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&str, impl Iterator<Item = &FontInfo>)> + '_ {
|
||||
// Since the keys are lowercased, we instead use the family field of the
|
||||
// first face's info.
|
||||
self.families.values().map(|ids| {
|
||||
let family = self.infos[ids[0]].family.as_str();
|
||||
let infos = ids.iter().map(|&id| &self.infos[id]);
|
||||
(family, infos)
|
||||
})
|
||||
}
|
||||
|
||||
/// Try to find and load a font from the given `family` that matches
|
||||
/// the given `variant` as closely as possible.
|
||||
///
|
||||
/// The `family` should be all lowercase.
|
||||
pub fn select(&self, family: &str, variant: FontVariant) -> Option<usize> {
|
||||
let ids = self.families.get(family)?;
|
||||
self.find_best_variant(None, variant, ids.iter().copied())
|
||||
}
|
||||
|
||||
/// Try to find and load a fallback font that
|
||||
/// - is as close as possible to the font `like` (if any)
|
||||
/// - is as close as possible to the given `variant`
|
||||
/// - is suitable for shaping the given `text`
|
||||
pub fn select_fallback(
|
||||
&self,
|
||||
like: Option<&FontInfo>,
|
||||
variant: FontVariant,
|
||||
text: &str,
|
||||
) -> Option<usize> {
|
||||
// Find the fonts that contain the text's first char ...
|
||||
let c = text.chars().next()?;
|
||||
let ids = self
|
||||
.infos
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, info)| info.coverage.contains(c as u32))
|
||||
.map(|(index, _)| index);
|
||||
|
||||
// ... and find the best variant among them.
|
||||
self.find_best_variant(like, variant, ids)
|
||||
}
|
||||
|
||||
/// Find the font in the passed iterator that
|
||||
/// - is closest to the font `like` (if any)
|
||||
/// - is closest to the given `variant`
|
||||
///
|
||||
/// To do that we compute a key for all variants and select the one with the
|
||||
/// minimal key. This key prioritizes:
|
||||
/// - If `like` is some other font:
|
||||
/// - Are both fonts (not) monospaced?
|
||||
/// - Do both fonts (not) have serifs?
|
||||
/// - How many words do the families share in their prefix? E.g. "Noto
|
||||
/// Sans" and "Noto Sans Arabic" share two words, whereas "IBM Plex
|
||||
/// Arabic" shares none with "Noto Sans", so prefer "Noto Sans Arabic"
|
||||
/// if `like` is "Noto Sans". In case there are two equally good
|
||||
/// matches, we prefer the shorter one because it is less special (e.g.
|
||||
/// if `like` is "Noto Sans Arabic", we prefer "Noto Sans" over "Noto
|
||||
/// Sans CJK HK".)
|
||||
/// - The style (normal / italic / oblique). If we want italic or oblique
|
||||
/// but it doesn't exist, the other one of the two is still better than
|
||||
/// normal.
|
||||
/// - The absolute distance to the target stretch.
|
||||
/// - The absolute distance to the target weight.
|
||||
fn find_best_variant(
|
||||
&self,
|
||||
like: Option<&FontInfo>,
|
||||
variant: FontVariant,
|
||||
ids: impl IntoIterator<Item = usize>,
|
||||
) -> Option<usize> {
|
||||
let mut best = None;
|
||||
let mut best_key = None;
|
||||
|
||||
for id in ids {
|
||||
let current = &self.infos[id];
|
||||
let key = (
|
||||
like.map(|like| {
|
||||
(
|
||||
current.flags.contains(FontFlags::MONOSPACE)
|
||||
!= like.flags.contains(FontFlags::MONOSPACE),
|
||||
current.flags.contains(FontFlags::SERIF)
|
||||
!= like.flags.contains(FontFlags::SERIF),
|
||||
Reverse(shared_prefix_words(¤t.family, &like.family)),
|
||||
current.family.len(),
|
||||
)
|
||||
}),
|
||||
current.variant.style.distance(variant.style),
|
||||
current.variant.stretch.distance(variant.stretch),
|
||||
current.variant.weight.distance(variant.weight),
|
||||
);
|
||||
|
||||
if best_key.map_or(true, |b| key < b) {
|
||||
best = Some(id);
|
||||
best_key = Some(key);
|
||||
}
|
||||
}
|
||||
|
||||
best
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties of a single font.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FontInfo {
|
||||
/// The typographic font family this font is part of.
|
||||
pub family: String,
|
||||
/// Properties that distinguish this font from other fonts in the same
|
||||
/// family.
|
||||
pub variant: FontVariant,
|
||||
/// Properties of the font.
|
||||
pub flags: FontFlags,
|
||||
/// The unicode coverage of the font.
|
||||
pub coverage: Coverage,
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// Bitflags describing characteristics of a font.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FontFlags: u32 {
|
||||
/// All glyphs have the same width.
|
||||
const MONOSPACE = 1 << 0;
|
||||
/// Glyphs have short strokes at their stems.
|
||||
const SERIF = 1 << 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl FontInfo {
|
||||
/// Compute metadata for all fonts in the given data.
|
||||
pub fn from_data<'a>(data: &'a [u8]) -> impl Iterator<Item = FontInfo> + 'a {
|
||||
let count = ttf_parser::fonts_in_collection(data).unwrap_or(1);
|
||||
(0 .. count).filter_map(move |index| {
|
||||
let ttf = ttf_parser::Face::from_slice(data, index).ok()?;
|
||||
Self::from_ttf(&ttf)
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute metadata for a single ttf-parser face.
|
||||
pub fn from_ttf(ttf: &ttf_parser::Face) -> Option<Self> {
|
||||
// We cannot use Name ID 16 "Typographic Family", because for some
|
||||
// fonts it groups together more than just Style / Weight / Stretch
|
||||
// variants (e.g. Display variants of Noto fonts) and then some
|
||||
// variants become inaccessible from Typst. And even though the
|
||||
// fsSelection bit WWS should help us decide whether that is the
|
||||
// case, it's wrong for some fonts (e.g. for certain variants of "Noto
|
||||
// Sans Display").
|
||||
//
|
||||
// So, instead we use Name ID 1 "Family" and trim many common
|
||||
// suffixes for which know that they just describe styling (e.g.
|
||||
// "ExtraBold").
|
||||
//
|
||||
// Also, for Noto fonts we use Name ID 4 "Full Name" instead,
|
||||
// because Name ID 1 "Family" sometimes contains "Display" and
|
||||
// sometimes doesn't for the Display variants and that mixes things
|
||||
// up.
|
||||
let family = {
|
||||
let mut family = find_name(ttf, name_id::FAMILY)?;
|
||||
if family.starts_with("Noto") {
|
||||
family = find_name(ttf, name_id::FULL_NAME)?;
|
||||
}
|
||||
typographic_family(&family).to_string()
|
||||
};
|
||||
|
||||
let variant = {
|
||||
let mut full = find_name(ttf, name_id::FULL_NAME).unwrap_or_default();
|
||||
full.make_ascii_lowercase();
|
||||
|
||||
// Some fonts miss the relevant bits for italic or oblique, so
|
||||
// we also try to infer that from the full name.
|
||||
let italic = ttf.is_italic() || full.contains("italic");
|
||||
let oblique =
|
||||
ttf.is_oblique() || full.contains("oblique") || full.contains("slanted");
|
||||
|
||||
let style = match (italic, oblique) {
|
||||
(false, false) => FontStyle::Normal,
|
||||
(true, _) => FontStyle::Italic,
|
||||
(_, true) => FontStyle::Oblique,
|
||||
};
|
||||
|
||||
let weight = FontWeight::from_number(ttf.weight().to_number());
|
||||
let stretch = FontStretch::from_number(ttf.width().to_number());
|
||||
|
||||
FontVariant { style, weight, stretch }
|
||||
};
|
||||
|
||||
// Determine the unicode coverage.
|
||||
let mut codepoints = vec![];
|
||||
for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) {
|
||||
if subtable.is_unicode() {
|
||||
subtable.codepoints(|c| codepoints.push(c));
|
||||
}
|
||||
}
|
||||
|
||||
let mut flags = FontFlags::empty();
|
||||
flags.set(FontFlags::MONOSPACE, ttf.is_monospaced());
|
||||
|
||||
// Determine whether this is a serif or sans-serif font.
|
||||
if let Some(panose) = ttf
|
||||
.table_data(Tag::from_bytes(b"OS/2"))
|
||||
.and_then(|os2| os2.get(32 .. 45))
|
||||
{
|
||||
if matches!(panose, [2, 2 ..= 10, ..]) {
|
||||
flags.insert(FontFlags::SERIF);
|
||||
}
|
||||
}
|
||||
|
||||
Some(FontInfo {
|
||||
family,
|
||||
variant,
|
||||
flags,
|
||||
coverage: Coverage::from_vec(codepoints),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to find and decode the name with the given id.
|
||||
pub(super) fn find_name(ttf: &ttf_parser::Face, name_id: u16) -> Option<String> {
|
||||
ttf.names().into_iter().find_map(|entry| {
|
||||
if entry.name_id == name_id {
|
||||
if let Some(string) = entry.to_string() {
|
||||
return Some(string);
|
||||
}
|
||||
|
||||
if entry.platform_id == PlatformId::Macintosh && entry.encoding_id == 0 {
|
||||
return Some(decode_mac_roman(entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Decode mac roman encoded bytes into a string.
|
||||
fn decode_mac_roman(coded: &[u8]) -> String {
|
||||
#[rustfmt::skip]
|
||||
const TABLE: [char; 128] = [
|
||||
'Ä', 'Å', 'Ç', 'É', 'Ñ', 'Ö', 'Ü', 'á', 'à', 'â', 'ä', 'ã', 'å', 'ç', 'é', 'è',
|
||||
'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ñ', 'ó', 'ò', 'ô', 'ö', 'õ', 'ú', 'ù', 'û', 'ü',
|
||||
'†', '°', '¢', '£', '§', '•', '¶', 'ß', '®', '©', '™', '´', '¨', '≠', 'Æ', 'Ø',
|
||||
'∞', '±', '≤', '≥', '¥', 'µ', '∂', '∑', '∏', 'π', '∫', 'ª', 'º', 'Ω', 'æ', 'ø',
|
||||
'¿', '¡', '¬', '√', 'ƒ', '≈', '∆', '«', '»', '…', '\u{a0}', 'À', 'Ã', 'Õ', 'Œ', 'œ',
|
||||
'–', '—', '“', '”', '‘', '’', '÷', '◊', 'ÿ', 'Ÿ', '⁄', '€', '‹', '›', 'fi', 'fl',
|
||||
'‡', '·', '‚', '„', '‰', 'Â', 'Ê', 'Á', 'Ë', 'È', 'Í', 'Î', 'Ï', 'Ì', 'Ó', 'Ô',
|
||||
'\u{f8ff}', 'Ò', 'Ú', 'Û', 'Ù', 'ı', 'ˆ', '˜', '¯', '˘', '˙', '˚', '¸', '˝', '˛', 'ˇ',
|
||||
];
|
||||
|
||||
fn char_from_mac_roman(code: u8) -> char {
|
||||
if code < 128 {
|
||||
code as char
|
||||
} else {
|
||||
TABLE[(code - 128) as usize]
|
||||
}
|
||||
}
|
||||
|
||||
coded.iter().copied().map(char_from_mac_roman).collect()
|
||||
}
|
||||
|
||||
/// Trim style naming from a family name.
|
||||
fn typographic_family(mut family: &str) -> &str {
|
||||
// Separators between names, modifiers and styles.
|
||||
const SEPARATORS: [char; 3] = [' ', '-', '_'];
|
||||
|
||||
// Modifiers that can appear in combination with suffixes.
|
||||
const MODIFIERS: &[&str] = &[
|
||||
"extra", "ext", "ex", "x", "semi", "sem", "sm", "demi", "dem", "ultra",
|
||||
];
|
||||
|
||||
// Style suffixes.
|
||||
#[rustfmt::skip]
|
||||
const SUFFIXES: &[&str] = &[
|
||||
"normal", "italic", "oblique", "slanted",
|
||||
"thin", "th", "hairline", "light", "lt", "regular", "medium", "med",
|
||||
"md", "bold", "bd", "demi", "extb", "black", "blk", "bk", "heavy",
|
||||
"narrow", "condensed", "cond", "cn", "cd", "compressed", "expanded", "exp"
|
||||
];
|
||||
|
||||
// Trim spacing and weird leading dots in Apple fonts.
|
||||
family = family.trim().trim_start_matches('.');
|
||||
|
||||
// Lowercase the string so that the suffixes match case-insensitively.
|
||||
let lower = family.to_ascii_lowercase();
|
||||
let mut len = usize::MAX;
|
||||
let mut trimmed = lower.as_str();
|
||||
|
||||
// Trim style suffixes repeatedly.
|
||||
while trimmed.len() < len {
|
||||
len = trimmed.len();
|
||||
|
||||
// Find style suffix.
|
||||
let mut t = match SUFFIXES.iter().find_map(|s| trimmed.strip_suffix(s)) {
|
||||
Some(t) => t,
|
||||
None => break,
|
||||
};
|
||||
|
||||
// Strip optional separator.
|
||||
if let Some(s) = t.strip_suffix(SEPARATORS) {
|
||||
trimmed = s;
|
||||
t = s;
|
||||
}
|
||||
|
||||
// Also allow an extra modifier, but apply it only if it is separated it
|
||||
// from the text before it (to prevent false positives).
|
||||
if let Some(t) = MODIFIERS.iter().find_map(|s| t.strip_suffix(s)) {
|
||||
if let Some(stripped) = t.strip_suffix(SEPARATORS) {
|
||||
trimmed = stripped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&family[.. len]
|
||||
}
|
||||
|
||||
/// How many words the two strings share in their prefix.
|
||||
fn shared_prefix_words(left: &str, right: &str) -> usize {
|
||||
left.unicode_words()
|
||||
.zip(right.unicode_words())
|
||||
.take_while(|(l, r)| l == r)
|
||||
.count()
|
||||
}
|
||||
|
||||
/// A compactly encoded set of codepoints.
|
||||
///
|
||||
/// The set is represented by alternating specifications of how many codepoints
|
||||
/// are not in the set and how many are in the set.
|
||||
///
|
||||
/// For example, for the set `{2, 3, 4, 9, 10, 11, 15, 18, 19}`, there are:
|
||||
/// - 2 codepoints not inside (0, 1)
|
||||
/// - 3 codepoints inside (2, 3, 4)
|
||||
/// - 4 codepoints not inside (5, 6, 7, 8)
|
||||
/// - 3 codepoints inside (9, 10, 11)
|
||||
/// - 3 codepoints not inside (12, 13, 14)
|
||||
/// - 1 codepoint inside (15)
|
||||
/// - 2 codepoints not inside (16, 17)
|
||||
/// - 2 codepoints inside (18, 19)
|
||||
///
|
||||
/// So the resulting encoding is `[2, 3, 4, 3, 3, 1, 2, 2]`.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Coverage(Vec<u32>);
|
||||
|
||||
impl Coverage {
|
||||
/// Encode a vector of codepoints.
|
||||
pub fn from_vec(mut codepoints: Vec<u32>) -> Self {
|
||||
codepoints.sort();
|
||||
codepoints.dedup();
|
||||
|
||||
let mut runs = Vec::new();
|
||||
let mut next = 0;
|
||||
|
||||
for c in codepoints {
|
||||
if let Some(run) = runs.last_mut().filter(|_| c == next) {
|
||||
*run += 1;
|
||||
} else {
|
||||
runs.push(c - next);
|
||||
runs.push(1);
|
||||
}
|
||||
|
||||
next = c + 1;
|
||||
}
|
||||
|
||||
Self(runs)
|
||||
}
|
||||
|
||||
/// Whether the codepoint is covered.
|
||||
pub fn contains(&self, c: u32) -> bool {
|
||||
let mut inside = false;
|
||||
let mut cursor = 0;
|
||||
|
||||
for &run in &self.0 {
|
||||
if (cursor .. cursor + run).contains(&c) {
|
||||
return inside;
|
||||
}
|
||||
cursor += run;
|
||||
inside = !inside;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_trim_styles() {
|
||||
assert_eq!(typographic_family("Atma Light"), "Atma");
|
||||
assert_eq!(typographic_family("eras bold"), "eras");
|
||||
assert_eq!(typographic_family("footlight mt light"), "footlight mt");
|
||||
assert_eq!(typographic_family("times new roman"), "times new roman");
|
||||
assert_eq!(
|
||||
typographic_family("noto sans mono cond sembd"),
|
||||
"noto sans mono"
|
||||
);
|
||||
assert_eq!(typographic_family("noto serif SEMCOND sembd"), "noto serif");
|
||||
assert_eq!(typographic_family("crimson text"), "crimson text");
|
||||
assert_eq!(typographic_family("footlight light"), "footlight");
|
||||
assert_eq!(typographic_family("Noto Sans"), "Noto Sans");
|
||||
assert_eq!(typographic_family("Noto Sans Light"), "Noto Sans");
|
||||
assert_eq!(
|
||||
typographic_family("Noto Sans Semicondensed Heavy"),
|
||||
"Noto Sans"
|
||||
);
|
||||
assert_eq!(typographic_family("Familx"), "Familx");
|
||||
assert_eq!(typographic_family("Font Ultra"), "Font Ultra");
|
||||
assert_eq!(typographic_family("Font Ultra Bold"), "Font");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coverage() {
|
||||
#[track_caller]
|
||||
fn test(set: &[u32], runs: &[u32]) {
|
||||
let coverage = Coverage::from_vec(set.to_vec());
|
||||
assert_eq!(coverage.0, runs);
|
||||
|
||||
let max = 5 + set.iter().copied().max().unwrap_or_default();
|
||||
for c in 0 .. max {
|
||||
assert_eq!(set.contains(&c), coverage.contains(c));
|
||||
}
|
||||
}
|
||||
|
||||
test(&[], &[]);
|
||||
test(&[0], &[0, 1]);
|
||||
test(&[1], &[1, 1]);
|
||||
test(&[0, 1], &[0, 2]);
|
||||
test(&[0, 1, 3], &[0, 2, 1, 1]);
|
||||
test(
|
||||
// {2, 3, 4, 9, 10, 11, 15, 18, 19}
|
||||
&[18, 19, 2, 4, 9, 11, 15, 3, 3, 10],
|
||||
&[2, 3, 4, 3, 3, 1, 2, 2],
|
||||
)
|
||||
}
|
||||
}
|
256
src/font/mod.rs
Normal file
256
src/font/mod.rs
Normal file
@ -0,0 +1,256 @@
|
||||
//! Font handling.
|
||||
|
||||
mod book;
|
||||
mod variant;
|
||||
|
||||
pub use book::*;
|
||||
pub use variant::*;
|
||||
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use rex::font::MathHeader;
|
||||
use ttf_parser::{GlyphId, Tag};
|
||||
|
||||
use crate::geom::Em;
|
||||
use crate::loading::Buffer;
|
||||
|
||||
/// An OpenType font.
|
||||
#[derive(Clone)]
|
||||
pub struct Font(Arc<Repr>);
|
||||
|
||||
/// The internal representation of a font.
|
||||
struct Repr {
|
||||
/// The raw font data, possibly shared with other fonts from the same
|
||||
/// collection. The vector's allocation must not move, because `ttf` points
|
||||
/// into it using unsafe code.
|
||||
data: Buffer,
|
||||
/// The font's index in the buffer.
|
||||
index: u32,
|
||||
/// Metadata about the font.
|
||||
info: FontInfo,
|
||||
/// The font's metrics.
|
||||
metrics: FontMetrics,
|
||||
/// The underlying ttf-parser/rustybuzz face.
|
||||
ttf: rustybuzz::Face<'static>,
|
||||
/// The parsed ReX math header.
|
||||
math: OnceCell<Option<MathHeader>>,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
/// Parse a font from data and collection index.
|
||||
pub fn new(data: 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 have a strong ref to the `Arc`.
|
||||
// - 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(data.as_ptr(), data.len()) };
|
||||
|
||||
let ttf = rustybuzz::Face::from_slice(slice, index)?;
|
||||
let metrics = FontMetrics::from_ttf(&ttf);
|
||||
let info = FontInfo::from_ttf(&ttf)?;
|
||||
|
||||
Some(Self(Arc::new(Repr {
|
||||
data,
|
||||
index,
|
||||
info,
|
||||
ttf,
|
||||
metrics,
|
||||
math: OnceCell::new(),
|
||||
})))
|
||||
}
|
||||
|
||||
/// The underlying buffer.
|
||||
pub fn data(&self) -> &Buffer {
|
||||
&self.0.data
|
||||
}
|
||||
|
||||
/// The font's index in the buffer.
|
||||
pub fn index(&self) -> u32 {
|
||||
self.0.index
|
||||
}
|
||||
|
||||
/// The font's metadata.
|
||||
pub fn info(&self) -> &FontInfo {
|
||||
&self.0.info
|
||||
}
|
||||
|
||||
/// The font's metrics.
|
||||
pub fn metrics(&self) -> &FontMetrics {
|
||||
&self.0.metrics
|
||||
}
|
||||
|
||||
/// The number of font units per one em.
|
||||
pub fn units_per_em(&self) -> f64 {
|
||||
self.0.metrics.units_per_em
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
|
||||
/// Look up the horizontal advance width of a glyph.
|
||||
pub fn advance(&self, glyph: u16) -> Option<Em> {
|
||||
self.0
|
||||
.ttf
|
||||
.glyph_hor_advance(GlyphId(glyph))
|
||||
.map(|units| self.to_em(units))
|
||||
}
|
||||
|
||||
/// Lookup a name by id.
|
||||
pub fn find_name(&self, id: u16) -> Option<String> {
|
||||
find_name(&self.0.ttf, id)
|
||||
}
|
||||
|
||||
/// 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.0.ttf
|
||||
}
|
||||
|
||||
/// Access the math header, if any.
|
||||
pub fn math(&self) -> Option<&MathHeader> {
|
||||
self.0
|
||||
.math
|
||||
.get_or_init(|| {
|
||||
let data = self.ttf().table_data(Tag::from_bytes(b"MATH"))?;
|
||||
MathHeader::parse(data).ok()
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Font {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.0.data.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Font {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "Font({})", self.info().family)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Font {}
|
||||
|
||||
impl PartialEq for Font {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.data.eq(&other.0.data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics of a font.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct FontMetrics {
|
||||
/// How many font units represent one em unit.
|
||||
pub units_per_em: f64,
|
||||
/// The distance from the baseline to the typographic ascender.
|
||||
pub ascender: Em,
|
||||
/// The approximate height of uppercase letters.
|
||||
pub cap_height: Em,
|
||||
/// The approximate height of non-ascending lowercase letters.
|
||||
pub x_height: Em,
|
||||
/// The distance from the baseline to the typographic descender.
|
||||
pub descender: Em,
|
||||
/// Recommended metrics for a strikethrough line.
|
||||
pub strikethrough: LineMetrics,
|
||||
/// Recommended metrics for an underline.
|
||||
pub underline: LineMetrics,
|
||||
/// Recommended metrics for an overline.
|
||||
pub overline: LineMetrics,
|
||||
}
|
||||
|
||||
impl FontMetrics {
|
||||
/// Extract the font's metrics.
|
||||
pub fn from_ttf(ttf: &ttf_parser::Face) -> Self {
|
||||
let units_per_em = f64::from(ttf.units_per_em());
|
||||
let to_em = |units| Em::from_units(units, units_per_em);
|
||||
|
||||
let ascender = to_em(ttf.typographic_ascender().unwrap_or(ttf.ascender()));
|
||||
let cap_height = ttf.capital_height().filter(|&h| h > 0).map_or(ascender, to_em);
|
||||
let x_height = ttf.x_height().filter(|&h| h > 0).map_or(ascender, to_em);
|
||||
let descender = to_em(ttf.typographic_descender().unwrap_or(ttf.descender()));
|
||||
let strikeout = ttf.strikeout_metrics();
|
||||
let underline = ttf.underline_metrics();
|
||||
|
||||
let strikethrough = LineMetrics {
|
||||
position: strikeout.map_or(Em::new(0.25), |s| to_em(s.position)),
|
||||
thickness: strikeout
|
||||
.or(underline)
|
||||
.map_or(Em::new(0.06), |s| to_em(s.thickness)),
|
||||
};
|
||||
|
||||
let underline = LineMetrics {
|
||||
position: underline.map_or(Em::new(-0.2), |s| to_em(s.position)),
|
||||
thickness: underline
|
||||
.or(strikeout)
|
||||
.map_or(Em::new(0.06), |s| to_em(s.thickness)),
|
||||
};
|
||||
|
||||
let overline = LineMetrics {
|
||||
position: cap_height + Em::new(0.1),
|
||||
thickness: underline.thickness,
|
||||
};
|
||||
|
||||
Self {
|
||||
units_per_em,
|
||||
ascender,
|
||||
cap_height,
|
||||
x_height,
|
||||
descender,
|
||||
strikethrough,
|
||||
underline,
|
||||
overline,
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up a vertical metric.
|
||||
pub fn vertical(&self, metric: VerticalFontMetric) -> Em {
|
||||
match metric {
|
||||
VerticalFontMetric::Ascender => self.ascender,
|
||||
VerticalFontMetric::CapHeight => self.cap_height,
|
||||
VerticalFontMetric::XHeight => self.x_height,
|
||||
VerticalFontMetric::Baseline => Em::zero(),
|
||||
VerticalFontMetric::Descender => self.descender,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics for a decorative line.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct LineMetrics {
|
||||
/// The vertical offset of the line from the baseline. Positive goes
|
||||
/// upwards, negative downwards.
|
||||
pub position: Em,
|
||||
/// The thickness of the line.
|
||||
pub thickness: Em,
|
||||
}
|
||||
|
||||
/// Identifies a vertical metric of a font.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum VerticalFontMetric {
|
||||
/// The distance from the baseline to the typographic ascender.
|
||||
///
|
||||
/// Corresponds to the typographic ascender from the `OS/2` table if present
|
||||
/// and falls back to the ascender from the `hhea` table otherwise.
|
||||
Ascender,
|
||||
/// The approximate height of uppercase letters.
|
||||
CapHeight,
|
||||
/// The approximate height of non-ascending lowercase letters.
|
||||
XHeight,
|
||||
/// The baseline on which the letters rest.
|
||||
Baseline,
|
||||
/// The distance from the baseline to the typographic descender.
|
||||
///
|
||||
/// Corresponds to the typographic descender from the `OS/2` table if
|
||||
/// present and falls back to the descender from the `hhea` table otherwise.
|
||||
Descender,
|
||||
}
|
226
src/font/variant.rs
Normal file
226
src/font/variant.rs
Normal file
@ -0,0 +1,226 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Properties that distinguish a font from other fonts in the same family.
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FontVariant {
|
||||
/// The style of the font (normal / italic / oblique).
|
||||
pub style: FontStyle,
|
||||
/// How heavy the font is (100 - 900).
|
||||
pub weight: FontWeight,
|
||||
/// How condensed or expanded the font 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 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FontVariant {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}-{:?}-{:?}", self.style, self.weight, self.stretch)
|
||||
}
|
||||
}
|
||||
|
||||
/// The style of a font.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum FontStyle {
|
||||
/// The default style.
|
||||
Normal,
|
||||
/// A cursive style.
|
||||
Italic,
|
||||
/// A slanted style.
|
||||
Oblique,
|
||||
}
|
||||
|
||||
impl FontStyle {
|
||||
/// The conceptual distance between the styles, expressed as a number.
|
||||
pub fn distance(self, other: Self) -> u16 {
|
||||
if self == other {
|
||||
0
|
||||
} else if self != Self::Normal && other != Self::Normal {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FontStyle {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
}
|
||||
|
||||
/// The weight of a font.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[derive(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))
|
||||
}
|
||||
|
||||
/// The number between 100 and 900.
|
||||
pub fn to_number(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// 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 Debug for FontWeight {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The width of a font.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct FontStretch(u16);
|
||||
|
||||
impl FontStretch {
|
||||
/// Ultra-condensed stretch (50%).
|
||||
pub const ULTRA_CONDENSED: Self = Self(500);
|
||||
|
||||
/// Extra-condensed stretch weight (62.5%).
|
||||
pub const EXTRA_CONDENSED: Self = Self(625);
|
||||
|
||||
/// Condensed stretch (75%).
|
||||
pub const CONDENSED: Self = Self(750);
|
||||
|
||||
/// Semi-condensed stretch (87.5%).
|
||||
pub const SEMI_CONDENSED: Self = Self(875);
|
||||
|
||||
/// Normal stretch (100%).
|
||||
pub const NORMAL: Self = Self(1000);
|
||||
|
||||
/// Semi-expanded stretch (112.5%).
|
||||
pub const SEMI_EXPANDED: Self = Self(1125);
|
||||
|
||||
/// Expanded stretch (125%).
|
||||
pub const EXPANDED: Self = Self(1250);
|
||||
|
||||
/// Extra-expanded stretch (150%).
|
||||
pub const EXTRA_EXPANDED: Self = Self(1500);
|
||||
|
||||
/// Ultra-expanded stretch (200%).
|
||||
pub const ULTRA_EXPANDED: Self = Self(2000);
|
||||
|
||||
/// 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) * 1000.0) as u16)
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// The ratio between 0.5 and 2.0 corresponding to this stretch.
|
||||
pub fn to_ratio(self) -> f32 {
|
||||
self.0 as f32 / 1000.0
|
||||
}
|
||||
|
||||
/// The absolute ratio distance between this and another font stretch.
|
||||
pub fn distance(self, other: Self) -> f32 {
|
||||
(self.to_ratio() - other.to_ratio()).abs()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FontStretch {
|
||||
fn default() -> Self {
|
||||
Self::NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FontStretch {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}%", 100.0 * self.to_ratio())
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_font_stretch_debug() {
|
||||
assert_eq!(format!("{:?}", FontStretch::EXPANDED), "125%")
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::eval::{Dict, Value};
|
||||
use crate::font::FontId;
|
||||
use crate::font::Font;
|
||||
use crate::geom::{
|
||||
Align, Em, Length, Numeric, Paint, Point, Shape, Size, Spec, Transform,
|
||||
};
|
||||
@ -353,7 +353,7 @@ impl Debug for Group {
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct Text {
|
||||
/// The font the glyphs are contained in.
|
||||
pub font_id: FontId,
|
||||
pub font: Font,
|
||||
/// The font size.
|
||||
pub size: Length,
|
||||
/// Glyph color.
|
||||
|
@ -55,7 +55,6 @@ use std::sync::Arc;
|
||||
|
||||
use crate::diag::TypResult;
|
||||
use crate::eval::Scope;
|
||||
use crate::font::FontStore;
|
||||
use crate::frame::Frame;
|
||||
use crate::loading::Loader;
|
||||
use crate::model::StyleMap;
|
||||
@ -77,8 +76,6 @@ pub struct Context {
|
||||
pub loader: Arc<dyn Loader>,
|
||||
/// Stores loaded source files.
|
||||
pub sources: SourceStore,
|
||||
/// Stores parsed fonts.
|
||||
pub fonts: FontStore,
|
||||
/// The context's configuration.
|
||||
config: Config,
|
||||
}
|
||||
@ -89,7 +86,6 @@ impl Context {
|
||||
Self {
|
||||
loader: Arc::clone(&loader),
|
||||
sources: SourceStore::new(Arc::clone(&loader)),
|
||||
fonts: FontStore::new(Arc::clone(&loader)),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ impl ImageNode {
|
||||
let image = vm
|
||||
.ctx
|
||||
.loader
|
||||
.load(&full)
|
||||
.file(&full)
|
||||
.and_then(|buffer| Image::new(buffer, ext))
|
||||
.map_err(|err| failed_to_load("image", &full, err))
|
||||
.at(span)?;
|
||||
|
@ -4,7 +4,7 @@ use rex::layout::{LayoutSettings, Style};
|
||||
use rex::parser::color::RGBA;
|
||||
use rex::render::{Backend, Cursor, Renderer};
|
||||
|
||||
use crate::font::FontId;
|
||||
use crate::font::Font;
|
||||
use crate::library::prelude::*;
|
||||
use crate::library::text::{variant, FontFamily, Lang, TextNode};
|
||||
|
||||
@ -28,14 +28,15 @@ impl Layout for RexNode {
|
||||
) -> TypResult<Vec<Frame>> {
|
||||
// Load the font.
|
||||
let span = self.tex.span;
|
||||
let font_id = ctx
|
||||
.fonts
|
||||
let font = ctx
|
||||
.loader
|
||||
.book()
|
||||
.select(self.family.as_str(), variant(styles))
|
||||
.and_then(|id| ctx.loader.font(id).ok())
|
||||
.ok_or("failed to find math font")
|
||||
.at(span)?;
|
||||
|
||||
// Prepare the font context.
|
||||
let font = ctx.fonts.get(font_id);
|
||||
let ctx = font
|
||||
.math()
|
||||
.map(|math| FontContext::new(font.ttf(), math))
|
||||
@ -76,7 +77,7 @@ impl Layout for RexNode {
|
||||
frame
|
||||
},
|
||||
baseline: top,
|
||||
font_id,
|
||||
font: font.clone(),
|
||||
fill: styles.get(TextNode::FILL),
|
||||
lang: styles.get(TextNode::LANG),
|
||||
colors: vec![],
|
||||
@ -93,7 +94,7 @@ impl Layout for RexNode {
|
||||
struct FrameBackend {
|
||||
frame: Frame,
|
||||
baseline: Length,
|
||||
font_id: FontId,
|
||||
font: Font,
|
||||
fill: Paint,
|
||||
lang: Lang,
|
||||
colors: Vec<RGBA>,
|
||||
@ -119,7 +120,7 @@ impl Backend for FrameBackend {
|
||||
self.frame.push(
|
||||
self.transform(pos),
|
||||
Element::Text(Text {
|
||||
font_id: self.font_id,
|
||||
font: self.font.clone(),
|
||||
size: Length::pt(scale),
|
||||
fill: self.fill(),
|
||||
lang: self.lang,
|
||||
|
@ -17,6 +17,7 @@ pub use crate::eval::{
|
||||
};
|
||||
pub use crate::frame::*;
|
||||
pub use crate::geom::*;
|
||||
pub use crate::loading::Loader;
|
||||
pub use crate::model::{
|
||||
Content, Fold, Key, Layout, LayoutNode, Regions, Resolve, Selector, Show, ShowNode,
|
||||
StyleChain, StyleMap, StyleVec,
|
||||
|
@ -2,7 +2,6 @@ use kurbo::{BezPath, Line, ParamCurve};
|
||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||
|
||||
use super::TextNode;
|
||||
use crate::font::FontStore;
|
||||
use crate::library::prelude::*;
|
||||
|
||||
/// Typeset underline, stricken-through or overlined text.
|
||||
@ -88,14 +87,12 @@ pub const OVERLINE: DecoLine = 2;
|
||||
pub fn decorate(
|
||||
frame: &mut Frame,
|
||||
deco: &Decoration,
|
||||
fonts: &FontStore,
|
||||
text: &Text,
|
||||
shift: Length,
|
||||
pos: Point,
|
||||
width: Length,
|
||||
) {
|
||||
let font = fonts.get(text.font_id);
|
||||
let font_metrics = font.metrics();
|
||||
let font_metrics = text.font.metrics();
|
||||
let metrics = match deco.line {
|
||||
STRIKETHROUGH => font_metrics.strikethrough,
|
||||
OVERLINE => font_metrics.overline,
|
||||
@ -143,7 +140,7 @@ pub fn decorate(
|
||||
let mut builder =
|
||||
BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw());
|
||||
|
||||
let bbox = font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder);
|
||||
let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder);
|
||||
let path = builder.finish();
|
||||
|
||||
x += glyph.x_advance.at(text.size);
|
||||
@ -151,8 +148,8 @@ pub fn decorate(
|
||||
// Only do the costly segments intersection test if the line
|
||||
// intersects the bounding box.
|
||||
if bbox.map_or(false, |bbox| {
|
||||
let y_min = -font.to_em(bbox.y_max).at(text.size);
|
||||
let y_max = -font.to_em(bbox.y_min).at(text.size);
|
||||
let y_min = -text.font.to_em(bbox.y_max).at(text.size);
|
||||
let y_max = -text.font.to_em(bbox.y_min).at(text.size);
|
||||
|
||||
offset >= y_min && offset <= y_max
|
||||
}) {
|
||||
|
@ -24,9 +24,7 @@ use std::borrow::Cow;
|
||||
|
||||
use ttf_parser::Tag;
|
||||
|
||||
use crate::font::{
|
||||
Font, FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric,
|
||||
};
|
||||
use crate::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric};
|
||||
use crate::library::prelude::*;
|
||||
use crate::util::EcoString;
|
||||
|
||||
|
@ -5,7 +5,6 @@ use unicode_script::{Script, UnicodeScript};
|
||||
use xi_unicode::LineBreakIterator;
|
||||
|
||||
use super::{shape, Lang, Quoter, Quotes, RepeatNode, ShapedText, TextNode};
|
||||
use crate::font::FontStore;
|
||||
use crate::library::layout::Spacing;
|
||||
use crate::library::prelude::*;
|
||||
use crate::util::EcoString;
|
||||
@ -78,7 +77,7 @@ impl Layout for ParNode {
|
||||
let p = prepare(ctx, self, &text, segments, regions, styles)?;
|
||||
|
||||
// Break the paragraph into lines.
|
||||
let lines = linebreak(&p, &mut ctx.fonts, regions.first.x);
|
||||
let lines = linebreak(&p, ctx.loader.as_ref(), regions.first.x);
|
||||
|
||||
// Stack the lines into one frame per region.
|
||||
stack(&p, ctx, &lines, regions)
|
||||
@ -518,7 +517,13 @@ fn prepare<'a>(
|
||||
let end = cursor + segment.len();
|
||||
match segment {
|
||||
Segment::Text(_) => {
|
||||
shape_range(&mut items, &mut ctx.fonts, &bidi, cursor .. end, styles);
|
||||
shape_range(
|
||||
&mut items,
|
||||
ctx.loader.as_ref(),
|
||||
&bidi,
|
||||
cursor .. end,
|
||||
styles,
|
||||
);
|
||||
}
|
||||
Segment::Spacing(spacing) => match spacing {
|
||||
Spacing::Relative(v) => {
|
||||
@ -562,14 +567,14 @@ fn prepare<'a>(
|
||||
/// items for them.
|
||||
fn shape_range<'a>(
|
||||
items: &mut Vec<Item<'a>>,
|
||||
fonts: &mut FontStore,
|
||||
loader: &dyn Loader,
|
||||
bidi: &BidiInfo<'a>,
|
||||
range: Range,
|
||||
styles: StyleChain<'a>,
|
||||
) {
|
||||
let mut process = |text, level: Level| {
|
||||
let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
|
||||
let shaped = shape(fonts, text, styles, dir);
|
||||
let shaped = shape(loader, text, styles, dir);
|
||||
items.push(Item::Text(shaped));
|
||||
};
|
||||
|
||||
@ -628,12 +633,12 @@ fn shared_get<'a, K: Key<'a>>(
|
||||
/// Find suitable linebreaks.
|
||||
fn linebreak<'a>(
|
||||
p: &'a Preparation<'a>,
|
||||
fonts: &mut FontStore,
|
||||
loader: &dyn Loader,
|
||||
width: Length,
|
||||
) -> Vec<Line<'a>> {
|
||||
match p.styles.get(ParNode::LINEBREAKS) {
|
||||
Linebreaks::Simple => linebreak_simple(p, fonts, width),
|
||||
Linebreaks::Optimized => linebreak_optimized(p, fonts, width),
|
||||
Linebreaks::Simple => linebreak_simple(p, loader, width),
|
||||
Linebreaks::Optimized => linebreak_optimized(p, loader, width),
|
||||
}
|
||||
}
|
||||
|
||||
@ -642,7 +647,7 @@ fn linebreak<'a>(
|
||||
/// very unbalanced line, but is fast and simple.
|
||||
fn linebreak_simple<'a>(
|
||||
p: &'a Preparation<'a>,
|
||||
fonts: &mut FontStore,
|
||||
loader: &dyn Loader,
|
||||
width: Length,
|
||||
) -> Vec<Line<'a>> {
|
||||
let mut lines = vec![];
|
||||
@ -651,7 +656,7 @@ fn linebreak_simple<'a>(
|
||||
|
||||
for (end, mandatory, hyphen) in breakpoints(p) {
|
||||
// Compute the line and its size.
|
||||
let mut attempt = line(p, fonts, start .. end, mandatory, hyphen);
|
||||
let mut attempt = line(p, loader, start .. end, mandatory, hyphen);
|
||||
|
||||
// If the line doesn't fit anymore, we push the last fitting attempt
|
||||
// into the stack and rebuild the line from the attempt's end. The
|
||||
@ -660,7 +665,7 @@ fn linebreak_simple<'a>(
|
||||
if let Some((last_attempt, last_end)) = last.take() {
|
||||
lines.push(last_attempt);
|
||||
start = last_end;
|
||||
attempt = line(p, fonts, start .. end, mandatory, hyphen);
|
||||
attempt = line(p, loader, start .. end, mandatory, hyphen);
|
||||
}
|
||||
}
|
||||
|
||||
@ -702,7 +707,7 @@ fn linebreak_simple<'a>(
|
||||
/// text.
|
||||
fn linebreak_optimized<'a>(
|
||||
p: &'a Preparation<'a>,
|
||||
fonts: &mut FontStore,
|
||||
loader: &dyn Loader,
|
||||
width: Length,
|
||||
) -> Vec<Line<'a>> {
|
||||
/// The cost of a line or paragraph layout.
|
||||
@ -727,7 +732,7 @@ fn linebreak_optimized<'a>(
|
||||
let mut table = vec![Entry {
|
||||
pred: 0,
|
||||
total: 0.0,
|
||||
line: line(p, fonts, 0 .. 0, false, false),
|
||||
line: line(p, loader, 0 .. 0, false, false),
|
||||
}];
|
||||
|
||||
let em = p.styles.get(TextNode::SIZE);
|
||||
@ -741,7 +746,7 @@ fn linebreak_optimized<'a>(
|
||||
for (i, pred) in table.iter_mut().enumerate().skip(active) {
|
||||
// Layout the line.
|
||||
let start = pred.line.end;
|
||||
let attempt = line(p, fonts, start .. end, mandatory, hyphen);
|
||||
let attempt = line(p, loader, start .. end, mandatory, hyphen);
|
||||
|
||||
// Determine how much the line's spaces would need to be stretched
|
||||
// to make it the desired width.
|
||||
@ -915,7 +920,7 @@ impl Breakpoints<'_> {
|
||||
/// Create a line which spans the given range.
|
||||
fn line<'a>(
|
||||
p: &'a Preparation,
|
||||
fonts: &mut FontStore,
|
||||
loader: &dyn Loader,
|
||||
mut range: Range,
|
||||
mandatory: bool,
|
||||
hyphen: bool,
|
||||
@ -970,9 +975,9 @@ fn line<'a>(
|
||||
if hyphen || start + shaped.text.len() > range.end {
|
||||
if hyphen || start < range.end || before.is_empty() {
|
||||
let shifted = start - base .. range.end - base;
|
||||
let mut reshaped = shaped.reshape(fonts, shifted);
|
||||
let mut reshaped = shaped.reshape(loader, shifted);
|
||||
if hyphen || shy {
|
||||
reshaped.push_hyphen(fonts);
|
||||
reshaped.push_hyphen(loader);
|
||||
}
|
||||
width += reshaped.width;
|
||||
last = Some(Item::Text(reshaped));
|
||||
@ -993,7 +998,7 @@ fn line<'a>(
|
||||
if range.start + shaped.text.len() > end {
|
||||
if range.start < end {
|
||||
let shifted = range.start - base .. end - base;
|
||||
let reshaped = shaped.reshape(fonts, shifted);
|
||||
let reshaped = shaped.reshape(loader, shifted);
|
||||
width += reshaped.width;
|
||||
first = Some(Item::Text(reshaped));
|
||||
}
|
||||
@ -1144,7 +1149,7 @@ fn commit(
|
||||
offset += v.share(fr, remaining);
|
||||
}
|
||||
Item::Text(shaped) => {
|
||||
let frame = shaped.build(&mut ctx.fonts, justification);
|
||||
let frame = shaped.build(ctx.loader.as_ref(), justification);
|
||||
push(&mut offset, frame);
|
||||
}
|
||||
Item::Frame(frame) => {
|
||||
|
@ -4,7 +4,7 @@ use std::str::FromStr;
|
||||
use rustybuzz::{Feature, UnicodeBuffer};
|
||||
|
||||
use super::*;
|
||||
use crate::font::{FontId, FontStore, FontVariant};
|
||||
use crate::font::{Font, FontVariant};
|
||||
use crate::library::prelude::*;
|
||||
use crate::util::SliceExt;
|
||||
|
||||
@ -31,10 +31,10 @@ pub struct ShapedText<'a> {
|
||||
}
|
||||
|
||||
/// A single glyph resulting from shaping.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShapedGlyph {
|
||||
/// The font the glyph is contained in.
|
||||
pub font_id: FontId,
|
||||
pub font: Font,
|
||||
/// The glyph's index in the font.
|
||||
pub glyph_id: u16,
|
||||
/// The advance width of the glyph.
|
||||
@ -80,8 +80,8 @@ impl<'a> ShapedText<'a> {
|
||||
///
|
||||
/// The `justification` defines how much extra advance width each
|
||||
/// [justifiable glyph](ShapedGlyph::is_justifiable) will get.
|
||||
pub fn build(&self, fonts: &mut FontStore, justification: Length) -> Frame {
|
||||
let (top, bottom) = self.measure(fonts);
|
||||
pub fn build(&self, loader: &dyn Loader, justification: Length) -> Frame {
|
||||
let (top, bottom) = self.measure(loader);
|
||||
let size = Size::new(self.width, top + bottom);
|
||||
|
||||
let mut offset = Length::zero();
|
||||
@ -94,8 +94,8 @@ impl<'a> ShapedText<'a> {
|
||||
let fill = self.styles.get(TextNode::FILL);
|
||||
let link = self.styles.get(TextNode::LINK);
|
||||
|
||||
for ((font_id, y_offset), group) in
|
||||
self.glyphs.as_ref().group_by_key(|g| (g.font_id, g.y_offset))
|
||||
for ((font, y_offset), group) in
|
||||
self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset))
|
||||
{
|
||||
let pos = Point::new(offset, top + shift + y_offset.at(self.size));
|
||||
|
||||
@ -116,7 +116,7 @@ impl<'a> ShapedText<'a> {
|
||||
.collect();
|
||||
|
||||
let text = Text {
|
||||
font_id,
|
||||
font,
|
||||
size: self.size,
|
||||
lang,
|
||||
fill,
|
||||
@ -128,7 +128,7 @@ impl<'a> ShapedText<'a> {
|
||||
|
||||
// Apply line decorations.
|
||||
for deco in &decos {
|
||||
decorate(&mut frame, &deco, fonts, &text, shift, pos, width);
|
||||
decorate(&mut frame, &deco, &text, shift, pos, width);
|
||||
}
|
||||
|
||||
frame.insert(text_layer, pos, Element::Text(text));
|
||||
@ -144,7 +144,7 @@ impl<'a> ShapedText<'a> {
|
||||
}
|
||||
|
||||
/// Measure the top and bottom extent of this text.
|
||||
fn measure(&self, fonts: &mut FontStore) -> (Length, Length) {
|
||||
fn measure(&self, loader: &dyn Loader) -> (Length, Length) {
|
||||
let mut top = Length::zero();
|
||||
let mut bottom = Length::zero();
|
||||
|
||||
@ -162,14 +162,18 @@ impl<'a> ShapedText<'a> {
|
||||
// When there are no glyphs, we just use the vertical metrics of the
|
||||
// first available font.
|
||||
for family in families(self.styles) {
|
||||
if let Some(font_id) = fonts.select(family, self.variant) {
|
||||
expand(fonts.get(font_id));
|
||||
if let Some(font) = loader
|
||||
.book()
|
||||
.select(family, self.variant)
|
||||
.and_then(|id| loader.font(id).ok())
|
||||
{
|
||||
expand(&font);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (font_id, _) in self.glyphs.group_by_key(|g| g.font_id) {
|
||||
expand(fonts.get(font_id));
|
||||
for g in self.glyphs.iter() {
|
||||
expand(&g.font);
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,7 +199,7 @@ impl<'a> ShapedText<'a> {
|
||||
/// shaping process if possible.
|
||||
pub fn reshape(
|
||||
&'a self,
|
||||
fonts: &mut FontStore,
|
||||
loader: &dyn Loader,
|
||||
text_range: Range<usize>,
|
||||
) -> ShapedText<'a> {
|
||||
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
|
||||
@ -209,22 +213,24 @@ impl<'a> ShapedText<'a> {
|
||||
glyphs: Cow::Borrowed(glyphs),
|
||||
}
|
||||
} else {
|
||||
shape(fonts, &self.text[text_range], self.styles, self.dir)
|
||||
shape(loader, &self.text[text_range], self.styles, self.dir)
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a hyphen to end of the text.
|
||||
pub fn push_hyphen(&mut self, fonts: &mut FontStore) {
|
||||
pub fn push_hyphen(&mut self, loader: &dyn Loader) {
|
||||
families(self.styles).find_map(|family| {
|
||||
let font_id = fonts.select(family, self.variant)?;
|
||||
let font = fonts.get(font_id);
|
||||
let font = loader
|
||||
.book()
|
||||
.select(family, self.variant)
|
||||
.and_then(|id| loader.font(id).ok())?;
|
||||
let ttf = font.ttf();
|
||||
let glyph_id = ttf.glyph_index('-')?;
|
||||
let x_advance = font.to_em(ttf.glyph_hor_advance(glyph_id)?);
|
||||
let cluster = self.glyphs.last().map(|g| g.cluster).unwrap_or_default();
|
||||
self.width += x_advance.at(self.size);
|
||||
self.glyphs.to_mut().push(ShapedGlyph {
|
||||
font_id,
|
||||
font,
|
||||
glyph_id: glyph_id.0,
|
||||
x_advance,
|
||||
x_offset: Em::zero(),
|
||||
@ -300,9 +306,9 @@ impl Debug for ShapedText<'_> {
|
||||
|
||||
/// Holds shaping results and metadata common to all shaped segments.
|
||||
struct ShapingContext<'a> {
|
||||
fonts: &'a mut FontStore,
|
||||
loader: &'a dyn Loader,
|
||||
glyphs: Vec<ShapedGlyph>,
|
||||
used: Vec<FontId>,
|
||||
used: Vec<Font>,
|
||||
styles: StyleChain<'a>,
|
||||
size: Length,
|
||||
variant: FontVariant,
|
||||
@ -313,7 +319,7 @@ struct ShapingContext<'a> {
|
||||
|
||||
/// Shape text into [`ShapedText`].
|
||||
pub fn shape<'a>(
|
||||
fonts: &mut FontStore,
|
||||
loader: &dyn Loader,
|
||||
text: &'a str,
|
||||
styles: StyleChain<'a>,
|
||||
dir: Dir,
|
||||
@ -321,7 +327,7 @@ pub fn shape<'a>(
|
||||
let size = styles.get(TextNode::SIZE);
|
||||
|
||||
let mut ctx = ShapingContext {
|
||||
fonts,
|
||||
loader,
|
||||
size,
|
||||
glyphs: vec![],
|
||||
used: vec![],
|
||||
@ -362,32 +368,33 @@ fn shape_segment<'a>(
|
||||
}
|
||||
|
||||
// Find the next available family.
|
||||
let book = ctx.loader.book();
|
||||
let mut selection = families.find_map(|family| {
|
||||
ctx.fonts
|
||||
.select(family, ctx.variant)
|
||||
.filter(|id| !ctx.used.contains(id))
|
||||
book.select(family, ctx.variant)
|
||||
.and_then(|id| ctx.loader.font(id).ok())
|
||||
.filter(|font| !ctx.used.contains(font))
|
||||
});
|
||||
|
||||
// Do font fallback if the families are exhausted and fallback is enabled.
|
||||
if selection.is_none() && ctx.fallback {
|
||||
let first = ctx.used.first().copied();
|
||||
selection = ctx
|
||||
.fonts
|
||||
let first = ctx.used.first().map(Font::info);
|
||||
selection = book
|
||||
.select_fallback(first, ctx.variant, text)
|
||||
.filter(|id| !ctx.used.contains(id));
|
||||
.and_then(|id| ctx.loader.font(id).ok())
|
||||
.filter(|font| !ctx.used.contains(font));
|
||||
}
|
||||
|
||||
// Extract the font id or shape notdef glyphs if we couldn't find any font.
|
||||
let font_id = if let Some(id) = selection {
|
||||
id
|
||||
let font = if let Some(font) = selection {
|
||||
font
|
||||
} else {
|
||||
if let Some(&font_id) = ctx.used.first() {
|
||||
shape_tofus(ctx, base, text, font_id);
|
||||
if let Some(font) = ctx.used.first().cloned() {
|
||||
shape_tofus(ctx, base, text, font);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
ctx.used.push(font_id);
|
||||
ctx.used.push(font.clone());
|
||||
|
||||
// Fill the buffer with our text.
|
||||
let mut buffer = UnicodeBuffer::new();
|
||||
@ -400,7 +407,6 @@ fn shape_segment<'a>(
|
||||
});
|
||||
|
||||
// Shape!
|
||||
let mut font = ctx.fonts.get(font_id);
|
||||
let buffer = rustybuzz::shape(font.ttf(), &ctx.tags, buffer);
|
||||
let infos = buffer.glyph_infos();
|
||||
let pos = buffer.glyph_positions();
|
||||
@ -416,7 +422,7 @@ fn shape_segment<'a>(
|
||||
// Add the glyph to the shaped output.
|
||||
// TODO: Don't ignore y_advance.
|
||||
ctx.glyphs.push(ShapedGlyph {
|
||||
font_id,
|
||||
font: font.clone(),
|
||||
glyph_id: info.glyph_id as u16,
|
||||
x_advance: font.to_em(pos[i].x_advance),
|
||||
x_offset: font.to_em(pos[i].x_offset),
|
||||
@ -471,8 +477,6 @@ fn shape_segment<'a>(
|
||||
|
||||
// Recursively shape the tofu sequence with the next family.
|
||||
shape_segment(ctx, base + range.start, &text[range], families.clone());
|
||||
|
||||
font = ctx.fonts.get(font_id);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
@ -482,12 +486,11 @@ fn shape_segment<'a>(
|
||||
}
|
||||
|
||||
/// Shape the text with tofus from the given font.
|
||||
fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font_id: FontId) {
|
||||
let font = ctx.fonts.get(font_id);
|
||||
fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
|
||||
let x_advance = font.advance(0).unwrap_or_default();
|
||||
for (cluster, c) in text.char_indices() {
|
||||
ctx.glyphs.push(ShapedGlyph {
|
||||
font_id,
|
||||
font: font.clone(),
|
||||
glyph_id: 0,
|
||||
x_advance,
|
||||
x_offset: Em::zero(),
|
||||
@ -511,8 +514,7 @@ fn track_and_space(ctx: &mut ShapingContext) {
|
||||
while let Some(glyph) = glyphs.next() {
|
||||
// Make non-breaking space same width as normal space.
|
||||
if glyph.c == '\u{00A0}' {
|
||||
let font = ctx.fonts.get(glyph.font_id);
|
||||
glyph.x_advance -= nbsp_delta(font).unwrap_or_default();
|
||||
glyph.x_advance -= nbsp_delta(&glyph.font).unwrap_or_default();
|
||||
}
|
||||
|
||||
if glyph.is_space() {
|
||||
|
@ -1,5 +1,4 @@
|
||||
use super::{variant, TextNode, TextSize};
|
||||
use crate::font::FontStore;
|
||||
use crate::library::prelude::*;
|
||||
use crate::util::EcoString;
|
||||
|
||||
@ -47,7 +46,7 @@ impl<const S: ScriptKind> Show for ShiftNode<S> {
|
||||
let mut transformed = None;
|
||||
if styles.get(Self::TYPOGRAPHIC) {
|
||||
if let Some(text) = search_text(&self.0, S) {
|
||||
if is_shapable(&mut ctx.fonts, &text, styles) {
|
||||
if is_shapable(ctx.loader.as_ref(), &text, styles) {
|
||||
transformed = Some(Content::Text(text));
|
||||
}
|
||||
}
|
||||
@ -92,11 +91,14 @@ fn search_text(content: &Content, mode: ScriptKind) -> Option<EcoString> {
|
||||
|
||||
/// Checks whether the first retrievable family contains all code points of the
|
||||
/// given string.
|
||||
fn is_shapable(fonts: &mut FontStore, text: &str, styles: StyleChain) -> bool {
|
||||
fn is_shapable(loader: &dyn Loader, text: &str, styles: StyleChain) -> bool {
|
||||
let book = loader.book();
|
||||
for family in styles.get(TextNode::FAMILY).iter() {
|
||||
if let Some(font_id) = fonts.select(family.as_str(), variant(styles)) {
|
||||
let ttf = fonts.get(font_id).ttf();
|
||||
return text.chars().all(|c| ttf.glyph_index(c).is_some());
|
||||
if let Some(font) = book
|
||||
.select(family.as_str(), variant(styles))
|
||||
.and_then(|id| loader.font(id).ok())
|
||||
{
|
||||
return text.chars().all(|c| font.ttf().glyph_index(c).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ pub fn csv(vm: &mut Machine, args: &mut Args) -> TypResult<Value> {
|
||||
|
||||
let path = vm.locate(&path).at(span)?;
|
||||
let try_load = || -> io::Result<Value> {
|
||||
let data = vm.ctx.loader.load(&path)?;
|
||||
let data = vm.ctx.loader.file(&path)?;
|
||||
|
||||
let mut builder = csv::ReaderBuilder::new();
|
||||
builder.has_headers(false);
|
||||
|
239
src/loading.rs
Normal file
239
src/loading.rs
Normal file
@ -0,0 +1,239 @@
|
||||
//! Resource loading.
|
||||
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::io;
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::font::{Font, FontBook};
|
||||
use crate::util::Prehashed;
|
||||
|
||||
/// A hash that identifies a file.
|
||||
///
|
||||
/// Such a hash can be [resolved](Loader::resolve) from a path.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct FileHash(pub u64);
|
||||
|
||||
/// Loads resources from a local or remote source.
|
||||
pub trait Loader {
|
||||
/// Metadata about all known fonts.
|
||||
fn book(&self) -> &FontBook;
|
||||
|
||||
/// Access the font with the given id.
|
||||
fn font(&self, id: usize) -> io::Result<Font>;
|
||||
|
||||
/// Resolve a hash that is the same for this and all other paths pointing to
|
||||
/// the same file.
|
||||
fn resolve(&self, path: &Path) -> io::Result<FileHash>;
|
||||
|
||||
/// Load a file from a path.
|
||||
fn file(&self, path: &Path) -> io::Result<Buffer>;
|
||||
}
|
||||
|
||||
/// A shared buffer that is cheap to clone.
|
||||
#[derive(Clone, Hash, Eq, PartialEq)]
|
||||
pub struct Buffer(Prehashed<Arc<Vec<u8>>>);
|
||||
|
||||
impl Buffer {
|
||||
/// Return a view into the buffer.
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
self
|
||||
}
|
||||
|
||||
/// Return a copy of the buffer as a vector.
|
||||
pub fn to_vec(&self) -> Vec<u8> {
|
||||
self.0.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for Buffer {
|
||||
fn from(slice: &[u8]) -> Self {
|
||||
Self(Prehashed::new(Arc::new(slice.to_vec())))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Buffer {
|
||||
fn from(vec: Vec<u8>) -> Self {
|
||||
Self(Prehashed::new(Arc::new(vec)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<Vec<u8>>> for Buffer {
|
||||
fn from(arc: Arc<Vec<u8>>) -> Self {
|
||||
Self(Prehashed::new(arc))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Buffer {
|
||||
type Target = [u8];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Buffer {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Buffer {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.pad("Buffer(..)")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "fs")]
|
||||
pub use fs::*;
|
||||
|
||||
#[cfg(feature = "fs")]
|
||||
mod fs {
|
||||
use std::fs::{self, File};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use memmap2::Mmap;
|
||||
use same_file::Handle;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use super::{Buffer, FileHash, Loader};
|
||||
use crate::font::{Font, FontBook, FontInfo};
|
||||
|
||||
/// Loads fonts and files from the local file system.
|
||||
///
|
||||
/// _This is only available when the `system` feature is enabled._
|
||||
pub struct FsLoader {
|
||||
book: FontBook,
|
||||
paths: Vec<(PathBuf, u32)>,
|
||||
}
|
||||
|
||||
impl FsLoader {
|
||||
/// Create a new system loader.
|
||||
pub fn new() -> Self {
|
||||
Self { book: FontBook::new(), paths: vec![] }
|
||||
}
|
||||
|
||||
/// Builder-style variant of [`search_path`](Self::search_path).
|
||||
pub fn with_path(mut self, dir: impl AsRef<Path>) -> Self {
|
||||
self.search_path(dir);
|
||||
self
|
||||
}
|
||||
|
||||
/// Search for all fonts at a path.
|
||||
///
|
||||
/// If the path is a directory, all contained fonts will be searched for
|
||||
/// recursively.
|
||||
pub fn search_path(&mut self, path: impl AsRef<Path>) {
|
||||
let walk = WalkDir::new(path)
|
||||
.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()) {
|
||||
if matches!(
|
||||
ext,
|
||||
"ttf" | "otf" | "TTF" | "OTF" | "ttc" | "otc" | "TTC" | "OTC",
|
||||
) {
|
||||
self.search_file(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Index the fonts in the file at the given path.
|
||||
///
|
||||
/// The file may form a font collection and contain multiple fonts,
|
||||
/// which will then all be indexed.
|
||||
fn search_file(&mut self, path: impl AsRef<Path>) {
|
||||
let path = path.as_ref();
|
||||
let path = path.strip_prefix(".").unwrap_or(path);
|
||||
if let Ok(file) = File::open(path) {
|
||||
if let Ok(mmap) = unsafe { Mmap::map(&file) } {
|
||||
for (i, info) in FontInfo::from_data(&mmap).enumerate() {
|
||||
self.book.push(info);
|
||||
self.paths.push((path.into(), i as u32));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder-style variant of [`search_system`](Self::search_system).
|
||||
pub fn with_system(mut self) -> Self {
|
||||
self.search_system();
|
||||
self
|
||||
}
|
||||
|
||||
/// Search for fonts in the operating system's font directories.
|
||||
pub fn search_system(&mut self) {
|
||||
self.search_system_impl();
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn search_system_impl(&mut self) {
|
||||
self.search_path("/usr/share/fonts");
|
||||
self.search_path("/usr/local/share/fonts");
|
||||
|
||||
if let Some(dir) = dirs::font_dir() {
|
||||
self.search_path(dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn search_system_impl(&mut self) {
|
||||
self.search_path("/Library/Fonts");
|
||||
self.search_path("/Network/Library/Fonts");
|
||||
self.search_path("/System/Library/Fonts");
|
||||
|
||||
if let Some(dir) = dirs::font_dir() {
|
||||
self.search_path(dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn search_system_impl(&mut self) {
|
||||
let windir =
|
||||
std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string());
|
||||
|
||||
self.search_path(Path::new(&windir).join("Fonts"));
|
||||
|
||||
if let Some(roaming) = dirs::config_dir() {
|
||||
self.search_path(roaming.join("Microsoft\\Windows\\Fonts"));
|
||||
}
|
||||
|
||||
if let Some(local) = dirs::cache_dir() {
|
||||
self.search_path(local.join("Microsoft\\Windows\\Fonts"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Loader for FsLoader {
|
||||
fn book(&self) -> &FontBook {
|
||||
&self.book
|
||||
}
|
||||
|
||||
fn font(&self, id: usize) -> io::Result<Font> {
|
||||
let (path, index) = &self.paths[id];
|
||||
let data = self.file(path)?;
|
||||
Font::new(data, *index).ok_or_else(|| io::ErrorKind::InvalidData.into())
|
||||
}
|
||||
|
||||
fn resolve(&self, path: &Path) -> io::Result<FileHash> {
|
||||
let meta = fs::metadata(path)?;
|
||||
if meta.is_file() {
|
||||
let handle = Handle::from_path(path)?;
|
||||
Ok(FileHash(fxhash::hash64(&handle)))
|
||||
} else {
|
||||
Err(io::ErrorKind::NotFound.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn file(&self, path: &Path) -> io::Result<Buffer> {
|
||||
Ok(fs::read(path)?.into())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
use std::fs::{self, File};
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use memmap2::Mmap;
|
||||
use same_file::Handle;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use super::{Buffer, FileHash, Loader};
|
||||
use crate::font::FontInfo;
|
||||
|
||||
/// Loads fonts and files from the local file system.
|
||||
///
|
||||
/// _This is only available when the `fs` feature is enabled._
|
||||
pub struct FsLoader {
|
||||
fonts: Vec<FontInfo>,
|
||||
}
|
||||
|
||||
impl FsLoader {
|
||||
/// Create a new loader without any fonts.
|
||||
pub fn new() -> Self {
|
||||
Self { fonts: vec![] }
|
||||
}
|
||||
|
||||
/// Builder-style variant of [`search_system`](Self::search_system).
|
||||
pub fn with_system(mut self) -> Self {
|
||||
self.search_system();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-style variant of [`search_path`](Self::search_path).
|
||||
pub fn with_path(mut self, dir: impl AsRef<Path>) -> Self {
|
||||
self.search_path(dir);
|
||||
self
|
||||
}
|
||||
|
||||
/// Search for fonts in the operating system's font directories.
|
||||
pub fn search_system(&mut self) {
|
||||
self.search_system_impl();
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn search_system_impl(&mut self) {
|
||||
self.search_path("/usr/share/fonts");
|
||||
self.search_path("/usr/local/share/fonts");
|
||||
|
||||
if let Some(dir) = dirs::font_dir() {
|
||||
self.search_path(dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn search_system_impl(&mut self) {
|
||||
self.search_path("/Library/Fonts");
|
||||
self.search_path("/Network/Library/Fonts");
|
||||
self.search_path("/System/Library/Fonts");
|
||||
|
||||
if let Some(dir) = dirs::font_dir() {
|
||||
self.search_path(dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn search_system_impl(&mut self) {
|
||||
let windir =
|
||||
std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string());
|
||||
|
||||
self.search_path(Path::new(&windir).join("Fonts"));
|
||||
|
||||
if let Some(roaming) = dirs::config_dir() {
|
||||
self.search_path(roaming.join("Microsoft\\Windows\\Fonts"));
|
||||
}
|
||||
|
||||
if let Some(local) = dirs::cache_dir() {
|
||||
self.search_path(local.join("Microsoft\\Windows\\Fonts"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Search for all fonts at a path.
|
||||
///
|
||||
/// If the path is a directory, all contained fonts will be searched for
|
||||
/// recursively.
|
||||
pub fn search_path(&mut self, path: impl AsRef<Path>) {
|
||||
let walk = WalkDir::new(path)
|
||||
.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()) {
|
||||
if matches!(
|
||||
ext,
|
||||
"ttf" | "otf" | "TTF" | "OTF" | "ttc" | "otc" | "TTC" | "OTC",
|
||||
) {
|
||||
self.search_file(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Index the fonts in the file at the given path.
|
||||
///
|
||||
/// The file may form a font collection and contain multiple fonts,
|
||||
/// which will then all be indexed.
|
||||
fn search_file(&mut self, path: impl AsRef<Path>) {
|
||||
let path = path.as_ref();
|
||||
let path = path.strip_prefix(".").unwrap_or(path);
|
||||
if let Ok(file) = File::open(path) {
|
||||
if let Ok(mmap) = unsafe { Mmap::map(&file) } {
|
||||
self.fonts.extend(FontInfo::from_data(path, &mmap));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Loader for FsLoader {
|
||||
fn fonts(&self) -> &[FontInfo] {
|
||||
&self.fonts
|
||||
}
|
||||
|
||||
fn resolve(&self, path: &Path) -> io::Result<FileHash> {
|
||||
let meta = fs::metadata(path)?;
|
||||
if meta.is_file() {
|
||||
let handle = Handle::from_path(path)?;
|
||||
Ok(FileHash(fxhash::hash64(&handle)))
|
||||
} else {
|
||||
Err(io::ErrorKind::NotFound.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&self, path: &Path) -> io::Result<Buffer> {
|
||||
Ok(fs::read(path)?.into())
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::{Buffer, FileHash, Loader};
|
||||
use crate::font::FontInfo;
|
||||
use crate::util::PathExt;
|
||||
|
||||
/// Loads fonts and files from an in-memory storage.
|
||||
#[derive(Default)]
|
||||
pub struct MemLoader {
|
||||
fonts: Vec<FontInfo>,
|
||||
files: HashMap<PathBuf, Cow<'static, [u8]>>,
|
||||
}
|
||||
|
||||
impl MemLoader {
|
||||
/// Create a new from-memory loader.
|
||||
pub fn new() -> Self {
|
||||
Self { fonts: vec![], files: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Builder-style variant of [`insert`](Self::insert).
|
||||
pub fn with<P, D>(mut self, path: P, data: D) -> Self
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
D: Into<Cow<'static, [u8]>>,
|
||||
{
|
||||
self.insert(path, data);
|
||||
self
|
||||
}
|
||||
|
||||
/// Insert a path-file mapping. If the data forms a font, then that font
|
||||
/// will be available for layouting.
|
||||
///
|
||||
/// The data can either be owned or referenced, but the latter only if its
|
||||
/// lifetime is `'static`.
|
||||
pub fn insert<P, D>(&mut self, path: P, data: D)
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
D: Into<Cow<'static, [u8]>>,
|
||||
{
|
||||
let path = path.as_ref().normalize();
|
||||
let data = data.into();
|
||||
self.fonts.extend(FontInfo::from_data(&path, &data));
|
||||
self.files.insert(path, data);
|
||||
}
|
||||
}
|
||||
|
||||
impl Loader for MemLoader {
|
||||
fn fonts(&self) -> &[FontInfo] {
|
||||
&self.fonts
|
||||
}
|
||||
|
||||
fn resolve(&self, path: &Path) -> io::Result<FileHash> {
|
||||
let norm = path.normalize();
|
||||
if self.files.contains_key(&norm) {
|
||||
Ok(FileHash(fxhash::hash64(&norm)))
|
||||
} else {
|
||||
Err(io::ErrorKind::NotFound.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn load(&self, path: &Path) -> io::Result<Buffer> {
|
||||
self.files
|
||||
.get(&path.normalize())
|
||||
.map(|cow| cow.clone().into_owned().into())
|
||||
.ok_or_else(|| io::ErrorKind::NotFound.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::font::FontVariant;
|
||||
|
||||
#[test]
|
||||
fn test_recognize_and_load_font() {
|
||||
let data = include_bytes!("../../fonts/PTSans-Regular.ttf");
|
||||
let path = Path::new("PTSans.ttf");
|
||||
let loader = MemLoader::new().with(path, &data[..]);
|
||||
|
||||
// Test that the font was found.
|
||||
let info = &loader.fonts[0];
|
||||
assert_eq!(info.path, path);
|
||||
assert_eq!(info.index, 0);
|
||||
assert_eq!(info.family, "PT Sans");
|
||||
assert_eq!(info.variant, FontVariant::default());
|
||||
assert_eq!(loader.fonts.len(), 1);
|
||||
|
||||
// Test that the file can be loaded.
|
||||
assert_eq!(
|
||||
loader.load(Path::new("directory/../PTSans.ttf")).unwrap().as_slice(),
|
||||
data
|
||||
);
|
||||
}
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
//! Resource loading.
|
||||
|
||||
#[cfg(feature = "fs")]
|
||||
mod fs;
|
||||
mod mem;
|
||||
|
||||
#[cfg(feature = "fs")]
|
||||
pub use fs::*;
|
||||
pub use mem::*;
|
||||
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::io;
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::font::FontInfo;
|
||||
use crate::util::Prehashed;
|
||||
|
||||
/// A hash that identifies a file.
|
||||
///
|
||||
/// Such a hash can be [resolved](Loader::resolve) from a path.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct FileHash(pub u64);
|
||||
|
||||
/// Loads resources from a local or remote source.
|
||||
pub trait Loader {
|
||||
/// Descriptions of all fonts this loader serves.
|
||||
fn fonts(&self) -> &[FontInfo];
|
||||
|
||||
/// Resolve a hash that is the same for this and all other paths pointing to
|
||||
/// the same file.
|
||||
fn resolve(&self, path: &Path) -> io::Result<FileHash>;
|
||||
|
||||
/// Load a file from a path.
|
||||
fn load(&self, path: &Path) -> io::Result<Buffer>;
|
||||
}
|
||||
|
||||
/// A loader which serves nothing.
|
||||
pub struct BlankLoader;
|
||||
|
||||
impl Loader for BlankLoader {
|
||||
fn fonts(&self) -> &[FontInfo] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn resolve(&self, _: &Path) -> io::Result<FileHash> {
|
||||
Err(io::ErrorKind::NotFound.into())
|
||||
}
|
||||
|
||||
fn load(&self, _: &Path) -> io::Result<Buffer> {
|
||||
Err(io::ErrorKind::NotFound.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A shared buffer that is cheap to clone.
|
||||
#[derive(Clone, Hash, Eq, PartialEq)]
|
||||
pub struct Buffer(Prehashed<Arc<Vec<u8>>>);
|
||||
|
||||
impl Buffer {
|
||||
/// Return a view into the buffer.
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
self
|
||||
}
|
||||
|
||||
/// Return a copy of the buffer as a vector.
|
||||
pub fn to_vec(&self) -> Vec<u8> {
|
||||
self.0.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for Buffer {
|
||||
fn from(slice: &[u8]) -> Self {
|
||||
Self(Prehashed::new(Arc::new(slice.to_vec())))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Buffer {
|
||||
fn from(vec: Vec<u8>) -> Self {
|
||||
Self(Prehashed::new(Arc::new(vec)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<Vec<u8>>> for Buffer {
|
||||
fn from(arc: Arc<Vec<u8>>) -> Self {
|
||||
Self(Prehashed::new(arc))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Buffer {
|
||||
type Target = [u8];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Buffer {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Buffer {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.pad("Buffer(..)")
|
||||
}
|
||||
}
|
18
src/main.rs
18
src/main.rs
@ -11,9 +11,9 @@ use same_file::is_same_file;
|
||||
use termcolor::{ColorChoice, StandardStream, WriteColor};
|
||||
|
||||
use typst::diag::{Error, StrResult};
|
||||
use typst::font::{FontInfo, FontStore};
|
||||
use typst::font::FontVariant;
|
||||
use typst::library::text::THEME;
|
||||
use typst::loading::FsLoader;
|
||||
use typst::loading::{FsLoader, Loader};
|
||||
use typst::parse::TokenMode;
|
||||
use typst::source::SourceStore;
|
||||
use typst::{Config, Context};
|
||||
@ -212,7 +212,7 @@ fn typeset(command: TypesetCommand) -> StrResult<()> {
|
||||
match typst::typeset(&mut ctx, id) {
|
||||
// Export the PDF.
|
||||
Ok(frames) => {
|
||||
let buffer = typst::export::pdf(&ctx, &frames);
|
||||
let buffer = typst::export::pdf(&frames);
|
||||
fs::write(&command.output, buffer).map_err(|_| "failed to write PDF file")?;
|
||||
}
|
||||
|
||||
@ -271,16 +271,12 @@ fn highlight(command: HighlightCommand) -> StrResult<()> {
|
||||
/// Execute a font listing command.
|
||||
fn fonts(command: FontsCommand) -> StrResult<()> {
|
||||
let loader = FsLoader::new().with_system();
|
||||
let fonts = FontStore::new(Arc::new(loader));
|
||||
|
||||
for (name, infos) in fonts.families() {
|
||||
for (name, infos) in loader.book().families() {
|
||||
println!("{name}");
|
||||
if command.variants {
|
||||
for &FontInfo { variant, .. } in infos {
|
||||
println!(
|
||||
"- Style: {:?}, Weight: {:?}, Stretch: {:?}",
|
||||
variant.style, variant.weight, variant.stretch,
|
||||
);
|
||||
for info in infos {
|
||||
let FontVariant { style, weight, stretch } = info.variant;
|
||||
println!("- Style: {style:?}, Weight: {weight:?}, Stretch: {stretch:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ impl SourceStore {
|
||||
return Ok(id);
|
||||
}
|
||||
|
||||
let data = self.loader.load(path)?;
|
||||
let data = self.loader.file(path)?;
|
||||
let src = String::from_utf8(data.to_vec()).map_err(|_| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "file is not valid utf-8")
|
||||
})?;
|
||||
@ -96,7 +96,7 @@ impl SourceStore {
|
||||
///
|
||||
/// The `path` does not need to be [resolvable](Loader::resolve) through the
|
||||
/// `loader`. If it is though, imports that resolve to the same file hash
|
||||
/// will use the inserted file instead of going through [`Loader::load`].
|
||||
/// will use the inserted file instead of going through [`Loader::file`].
|
||||
///
|
||||
/// If the path is resolvable and points to an existing source file, it is
|
||||
/// [replaced](Source::replace).
|
||||
|
@ -234,7 +234,7 @@ fn test(
|
||||
|
||||
if compare_ever {
|
||||
if let Some(pdf_path) = pdf_path {
|
||||
let pdf_data = typst::export::pdf(ctx, &frames);
|
||||
let pdf_data = typst::export::pdf(&frames);
|
||||
fs::create_dir_all(&pdf_path.parent().unwrap()).unwrap();
|
||||
fs::write(pdf_path, pdf_data).unwrap();
|
||||
}
|
||||
@ -245,7 +245,7 @@ fn test(
|
||||
}
|
||||
}
|
||||
|
||||
let canvas = render(ctx, &frames);
|
||||
let canvas = render(&frames);
|
||||
fs::create_dir_all(&png_path.parent().unwrap()).unwrap();
|
||||
canvas.save_png(png_path).unwrap();
|
||||
|
||||
@ -532,7 +532,7 @@ fn test_spans_impl(node: &SyntaxNode, within: Range<u64>) -> bool {
|
||||
}
|
||||
|
||||
/// Draw all frames into one image with padding in between.
|
||||
fn render(ctx: &mut Context, frames: &[Frame]) -> sk::Pixmap {
|
||||
fn render(frames: &[Frame]) -> sk::Pixmap {
|
||||
let pixel_per_pt = 2.0;
|
||||
let pixmaps: Vec<_> = frames
|
||||
.iter()
|
||||
@ -541,7 +541,7 @@ fn render(ctx: &mut Context, frames: &[Frame]) -> sk::Pixmap {
|
||||
if frame.width() > limit || frame.height() > limit {
|
||||
panic!("overlarge frame: {:?}", frame.size());
|
||||
}
|
||||
typst::export::render(ctx, frame, pixel_per_pt)
|
||||
typst::export::render(frame, pixel_per_pt)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@ -555,7 +555,7 @@ fn render(ctx: &mut Context, frames: &[Frame]) -> sk::Pixmap {
|
||||
let [x, mut y] = [pad; 2];
|
||||
for (frame, mut pixmap) in frames.iter().zip(pixmaps) {
|
||||
let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt);
|
||||
render_links(&mut pixmap, ts, ctx, frame);
|
||||
render_links(&mut pixmap, ts, frame);
|
||||
|
||||
canvas.draw_pixmap(
|
||||
x as i32,
|
||||
@ -573,18 +573,13 @@ fn render(ctx: &mut Context, frames: &[Frame]) -> sk::Pixmap {
|
||||
}
|
||||
|
||||
/// Draw extra boxes for links so we can see whether they are there.
|
||||
fn render_links(
|
||||
canvas: &mut sk::Pixmap,
|
||||
ts: sk::Transform,
|
||||
ctx: &Context,
|
||||
frame: &Frame,
|
||||
) {
|
||||
fn render_links(canvas: &mut sk::Pixmap, ts: sk::Transform, frame: &Frame) {
|
||||
for (pos, element) in frame.elements() {
|
||||
let ts = ts.pre_translate(pos.x.to_pt() as f32, pos.y.to_pt() as f32);
|
||||
match *element {
|
||||
Element::Group(ref group) => {
|
||||
let ts = ts.pre_concat(group.transform.into());
|
||||
render_links(canvas, ts, ctx, &group.frame);
|
||||
render_links(canvas, ts, &group.frame);
|
||||
}
|
||||
Element::Link(_, size) => {
|
||||
let w = size.x.to_pt() as f32;
|
||||
|
Loading…
x
Reference in New Issue
Block a user