Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CoreText: Faster OTC font loading #232

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
//!
//! To open the font referenced by a handle, use a loader.

use std::any::Any;
use std::path::PathBuf;
use std::sync::Arc;

use crate::error::FontLoadingError;
use crate::font::Font;
use crate::loader::Loader;

/// Encapsulates the information needed to locate and open a font.
///
Expand All @@ -45,6 +47,11 @@ pub enum Handle {
/// If the memory consists of a single font, this value will be 0.
font_index: u32,
},
/// An already-loaded font.
Native {
/// Type-erased font storage. Use [`Self::as_native`] to retrieve the font object.
inner: Arc<dyn Any + Sync + Send>,
},
}

impl Handle {
Expand All @@ -66,6 +73,25 @@ impl Handle {
Handle::Memory { bytes, font_index }
}

/// Creates a new handle from a system handle.
pub fn from_native<T: Loader>(inner: &T) -> Self
where
T::NativeFont: Sync + Send,
{
Self::Native {
inner: Arc::new(inner.native_font()),
}
}
/// Retrieves a handle to the font object.
///
/// May return None if inner object is not of type `T` or if this handle does not contain a native font object.
pub fn native_as<T: 'static>(&self) -> Option<&T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrm. Any reason this isn't called as_native?

Copy link
Author

@osiewicz osiewicz Jan 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, no. There's no particular reason for that, I'll change it. Funnily enough, in doc comments I've referred to it as as_native. Oh well. :p

if let Self::Native { inner } = self {
inner.downcast_ref()
} else {
None
}
}
/// A convenience method to load this handle with the default loader, producing a Font.
#[inline]
pub fn load(&self) -> Result<Font, FontLoadingError> {
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,5 @@ pub mod source;
#[cfg(feature = "source")]
pub mod sources;

mod matching;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this change be split out into a different PR?

pub mod matching;
mod utils;
23 changes: 12 additions & 11 deletions src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use std::path::Path;
/// fonts.
pub trait Loader: Clone + Sized {
/// The handle that the API natively uses to represent a font.
type NativeFont;
type NativeFont: 'static;

/// Loads a font from raw font data (the contents of a `.ttf`/`.otf`/etc. file).
///
Expand Down Expand Up @@ -63,22 +63,23 @@ pub trait Loader: Clone + Sized {
}

/// Creates a font from a native API handle.
unsafe fn from_native_font(native_font: Self::NativeFont) -> Self;
unsafe fn from_native_font(native_font: &Self::NativeFont) -> Self;

/// Loads the font pointed to by a handle.
fn from_handle(handle: &Handle) -> Result<Self, FontLoadingError> {
match *handle {
Handle::Memory {
ref bytes,
font_index,
} => Self::from_bytes((*bytes).clone(), font_index),
match handle {
Handle::Memory { bytes, font_index } => Self::from_bytes((*bytes).clone(), *font_index),
#[cfg(not(target_arch = "wasm32"))]
Handle::Path {
ref path,
font_index,
} => Self::from_path(path, font_index),
Handle::Path { path, font_index } => Self::from_path(path, *font_index),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind removing unrelated reformatting?

#[cfg(target_arch = "wasm32")]
Handle::Path { .. } => Err(FontLoadingError::NoFilesystem),
Handle::Native { .. } => {
if let Some(native) = handle.native_as::<Self::NativeFont>() {
unsafe { Ok(Self::from_native_font(native)) }
} else {
Err(FontLoadingError::UnknownFormat)
}
}
}
}

Expand Down
34 changes: 11 additions & 23 deletions src/loaders/core_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,29 +122,14 @@ impl Font {
}

/// Creates a font from a native API handle.
pub unsafe fn from_native_font(core_text_font: NativeFont) -> Font {
Font::from_core_text_font(core_text_font)
}

unsafe fn from_core_text_font(core_text_font: NativeFont) -> Font {
let mut font_data = FontData::Unavailable;
match core_text_font.url() {
None => warn!("No URL found for Core Text font!"),
Some(url) => match url.to_path() {
Some(path) => match File::open(path) {
Ok(ref mut file) => match utils::slurp_file(file) {
Ok(data) => font_data = FontData::Memory(Arc::new(data)),
Err(_) => warn!("Couldn't read file data for Core Text font!"),
},
Err(_) => warn!("Could not open file for Core Text font!"),
},
None => warn!("Could not convert URL from Core Text font to path!"),
},
}

pub unsafe fn from_native_font(core_text_font: &NativeFont) -> Font {
Font::from_core_text_font_no_path(core_text_font.clone())
}
/// Creates a font from a native API handle, without performing a lookup on the disk.
pub unsafe fn from_core_text_font_no_path(core_text_font: NativeFont) -> Font {
Font {
core_text_font,
font_data,
font_data: FontData::Unavailable,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What the repercussions of no longer having this font data?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, good catch; Font implements Deref to [u8] which will panic if there's no data associated: https://github.com/servo/font-kit/blob/master/src/loaders/core_text.rs#L784
Granted, this can still happen on master if a font has no path.
Also, Font::copy_font_data will return None instead of font contents, but that at least does not panic in surprising ways. I'd say that if the panic and implicit deref to [u8] poses a problem, one can always turn to using Font::copy_font_data, which gives a chance to avoid a panic.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, this PR is already Semver-breaking with a change to Loader::from_native_font, so we might as well get rid of the Deref implementation?

}
}

Expand All @@ -153,7 +138,10 @@ impl Font {
/// This function is only available on the Core Text backend.
pub fn from_core_graphics_font(core_graphics_font: CGFont) -> Font {
unsafe {
Font::from_core_text_font(core_text::font::new_from_CGFont(&core_graphics_font, 16.0))
Font::from_core_text_font_no_path(core_text::font::new_from_CGFont(
&core_graphics_font,
16.0,
))
Comment on lines +141 to +144
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change seems unrelated?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it kinda is; Font::from_core_text_font was always trying to load data from disk, which is not necessary as we already have a font.

}
}

Expand Down Expand Up @@ -628,7 +616,7 @@ impl Loader for Font {
}

#[inline]
unsafe fn from_native_font(native_font: Self::NativeFont) -> Self {
unsafe fn from_native_font(native_font: &Self::NativeFont) -> Self {
Font::from_native_font(native_font)
}

Expand Down
6 changes: 4 additions & 2 deletions src/loaders/directwrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const OPENTYPE_TABLE_TAG_HEAD: u32 = 0x68656164;

/// DirectWrite's representation of a font.
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct NativeFont {
/// The native DirectWrite font object.
pub dwrite_font: DWriteFont,
Expand Down Expand Up @@ -160,7 +161,8 @@ impl Font {

/// Creates a font from a native API handle.
#[inline]
pub unsafe fn from_native_font(native_font: NativeFont) -> Font {
pub unsafe fn from_native_font(native_font: &NativeFont) -> Font {
let native_font = native_font.clone();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this cloning the font data or just the handle here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is a bit more involved as we're going through COM (https://github.com/servo/dwrote-rs/blob/master/src/font.rs#L44, https://github.com/servo/dwrote-rs/blob/master/src/font_face.rs#L31), but I'd also say that this is bumping just the refcounts. So we should be good.

Font {
dwrite_font: native_font.dwrite_font,
dwrite_font_face: native_font.dwrite_font_face,
Expand Down Expand Up @@ -747,7 +749,7 @@ impl Loader for Font {
}

#[inline]
unsafe fn from_native_font(native_font: Self::NativeFont) -> Self {
unsafe fn from_native_font(native_font: &Self::NativeFont) -> Self {
Font::from_native_font(native_font)
}

Expand Down
5 changes: 3 additions & 2 deletions src/loaders/freetype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,10 @@ impl Font {
}

/// Creates a font from a native API handle.
pub unsafe fn from_native_font(freetype_face: NativeFont) -> Font {
pub unsafe fn from_native_font(freetype_face: &NativeFont) -> Font {
// We make an in-memory copy of the underlying font data. This is because the native font
// does not necessarily hold a strong reference to the memory backing it.
let freetype_face = *freetype_face;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question effectively here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case we're copying a pointer, so it's just a handle.

const CHUNK_SIZE: usize = 4096;
let mut font_data = vec![];
loop {
Expand Down Expand Up @@ -1036,7 +1037,7 @@ impl Loader for Font {
}

#[inline]
unsafe fn from_native_font(native_font: Self::NativeFont) -> Self {
unsafe fn from_native_font(native_font: &Self::NativeFont) -> Self {
Font::from_native_font(native_font)
}

Expand Down
62 changes: 6 additions & 56 deletions src/sources/core_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use core_foundation::array::CFArray;
use core_foundation::base::{CFType, TCFType};
use core_foundation::dictionary::CFDictionary;
use core_foundation::string::CFString;
use core_text::font::new_from_descriptor;
use core_text::font_collection::{self, CTFontCollection};
use core_text::font_descriptor::{self, CTFontDescriptor};
use core_text::font_manager;
Expand Down Expand Up @@ -153,68 +154,17 @@ fn css_stretchiness_to_core_text_width(css_stretchiness: Stretch) -> f32 {
0.25 * core_text_loader::piecewise_linear_find_index(css_stretchiness, &Stretch::MAPPING) - 1.0
}

#[derive(Clone)]
struct FontDataInfo {
data: Arc<Vec<u8>>,
file_type: FileType,
}

fn create_handles_from_core_text_collection(
collection: CTFontCollection,
) -> Result<Vec<Handle>, SelectionError> {
let mut fonts = vec![];
if let Some(descriptors) = collection.get_descriptors() {
let mut font_data_info_cache: HashMap<PathBuf, FontDataInfo> = HashMap::new();

'outer: for index in 0..descriptors.len() {
for index in 0..descriptors.len() {
let descriptor = descriptors.get(index).unwrap();
let font_path = descriptor.font_path().unwrap();

let data_info = if let Some(data_info) = font_data_info_cache.get(&font_path) {
data_info.clone()
} else {
let mut file = if let Ok(file) = File::open(&font_path) {
file
} else {
continue;
};
let data = if let Ok(data) = utils::slurp_file(&mut file) {
Arc::new(data)
} else {
continue;
};

let file_type = match Font::analyze_bytes(Arc::clone(&data)) {
Ok(file_type) => file_type,
Err(_) => continue,
};

let data_info = FontDataInfo { data, file_type };

font_data_info_cache.insert(font_path.clone(), data_info.clone());

data_info
};

match data_info.file_type {
FileType::Collection(font_count) => {
let postscript_name = descriptor.font_name();
for font_index in 0..font_count {
if let Ok(font) = Font::from_bytes(Arc::clone(&data_info.data), font_index)
{
if let Some(font_postscript_name) = font.postscript_name() {
if postscript_name == font_postscript_name {
fonts.push(Handle::from_memory(data_info.data, font_index));
continue 'outer;
}
}
}
}
}
FileType::Single => {
fonts.push(Handle::from_memory(data_info.data, 0));
}
}
let native = new_from_descriptor(&descriptor, 16.);
let font = unsafe { Font::from_core_text_font_no_path(native.clone()) };

fonts.push(Handle::from_native(&font));
}
}
if fonts.is_empty() {
Expand Down
1 change: 1 addition & 0 deletions tests/select_font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ macro_rules! match_handle {
font_index, $index
);
}
Handle::Native { .. } => {}
}
};
}
Expand Down
12 changes: 0 additions & 12 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -622,18 +622,6 @@ pub fn get_font_properties() {
assert_eq!(properties.stretch, Stretch(1.0));
}

#[cfg(feature = "source")]
#[test]
pub fn get_font_data() {
Comment on lines -625 to -627
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this test being removed?
I re-added it locally and it passed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What OS are you on? It fails for me on Mac, iirc due to https://github.com/servo/font-kit/pull/232/files#diff-6ba3732b9cf93e63d757c9bbbe03032cb17ed4525b45153ec0217b7df271ebf1R132 which leads to copy_font_data call failing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im on Linux

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's unwise to remove the test. At the very least, we can cfg-gate it.

let font = SystemSource::new()
.select_best_match(&[FamilyName::SansSerif], &Properties::new())
.unwrap()
.load()
.unwrap();
let data = font.copy_font_data().unwrap();
debug_assert!(SFNT_VERSIONS.iter().any(|version| data[0..4] == *version));
}

#[cfg(feature = "source")]
#[test]
pub fn load_font_table() {
Expand Down
Loading