servo/components/fonts/platform/macos/font.rs
Simon Wülker 3d320fa96a
Update rustfmt to the 2024 style edition (#35764)
* Use 2024 style edition

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Reformat all code

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
2025-03-03 11:26:53 +00:00

395 lines
15 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cmp::Ordering;
use std::ops::Range;
use std::{fmt, ptr};
/// Implementation of Quartz (CoreGraphics) fonts.
use app_units::Au;
use byteorder::{BigEndian, ByteOrder};
use core_foundation::data::CFData;
use core_foundation::string::UniChar;
use core_graphics::font::CGGlyph;
use core_text::font::CTFont;
use core_text::font_descriptor::{
CTFontTraits, SymbolicTraitAccessors, TraitAccessors, kCTFontDefaultOrientation,
};
use euclid::default::{Point2D, Rect, Size2D};
use log::debug;
use style::values::computed::font::{FontStretch, FontStyle, FontWeight};
use webrender_api::FontInstanceFlags;
use super::core_text_font_cache::CoreTextFontCache;
use super::font_list::LocalFontIdentifier;
use crate::{
CBDT, COLR, FontData, FontIdentifier, FontMetrics, FontTableMethods, FontTableTag,
FontTemplateDescriptor, FractionalPixel, GlyphId, KERN, PlatformFontMethods, SBIX,
map_platform_values_to_style_values,
};
const KERN_PAIR_LEN: usize = 6;
pub struct FontTable {
data: CFData,
}
// assumes 72 points per inch, and 96 px per inch
fn pt_to_px(pt: f64) -> f64 {
pt / 72. * 96.
}
impl FontTable {
pub fn wrap(data: CFData) -> FontTable {
FontTable { data }
}
}
impl FontTableMethods for FontTable {
fn buffer(&self) -> &[u8] {
self.data.bytes()
}
}
#[derive(Debug)]
pub struct PlatformFont {
ctfont: CTFont,
h_kern_subtable: Option<CachedKernTable>,
}
// From https://developer.apple.com/documentation/coretext:
// > All individual functions in Core Text are thread-safe. Font objects (CTFont,
// > CTFontDescriptor, and associated objects) can be used simultaneously by multiple
// > operations, work queues, or threads. However, the layout objects (CTTypesetter,
// > CTFramesetter, CTRun, CTLine, CTFrame, and associated objects) should be used in a
// > single operation, work queue, or thread.
//
// The other element is a read-only CachedKernTable which is stored in a CFData.
unsafe impl Sync for PlatformFont {}
unsafe impl Send for PlatformFont {}
impl PlatformFont {
/// Cache all the data needed for basic horizontal kerning. This is used only as a fallback or
/// fast path (when the GPOS table is missing or unnecessary) so it needn't handle every case.
fn find_h_kern_subtable(&self) -> Option<CachedKernTable> {
let font_table = self.table_for_tag(KERN)?;
let mut result = CachedKernTable {
font_table,
pair_data_range: 0..0,
px_per_font_unit: 0.0,
};
// Look for a subtable with horizontal kerning in format 0.
// https://www.microsoft.com/typography/otspec/kern.htm
const KERN_COVERAGE_HORIZONTAL_FORMAT_0: u16 = 1;
const SUBTABLE_HEADER_LEN: usize = 6;
const FORMAT_0_HEADER_LEN: usize = 8;
{
let table = result.font_table.buffer();
let version = BigEndian::read_u16(table);
if version != 0 {
return None;
}
let num_subtables = BigEndian::read_u16(&table[2..]);
let mut start = 4;
for _ in 0..num_subtables {
// TODO: Check the subtable version number?
let len = BigEndian::read_u16(&table[start + 2..]) as usize;
let cov = BigEndian::read_u16(&table[start + 4..]);
let end = start + len;
if cov == KERN_COVERAGE_HORIZONTAL_FORMAT_0 {
// Found a matching subtable.
if !result.pair_data_range.is_empty() {
debug!("Found multiple horizontal kern tables. Disable fast path.");
return None;
}
// Read the subtable header.
let subtable_start = start + SUBTABLE_HEADER_LEN;
let n_pairs = BigEndian::read_u16(&table[subtable_start..]) as usize;
let pair_data_start = subtable_start + FORMAT_0_HEADER_LEN;
result.pair_data_range = pair_data_start..end;
if result.pair_data_range.len() != n_pairs * KERN_PAIR_LEN {
debug!("Bad data in kern header. Disable fast path.");
return None;
}
let pt_per_font_unit =
self.ctfont.pt_size() / self.ctfont.units_per_em() as f64;
result.px_per_font_unit = pt_to_px(pt_per_font_unit);
}
start = end;
}
}
if !result.pair_data_range.is_empty() {
Some(result)
} else {
None
}
}
}
struct CachedKernTable {
font_table: FontTable,
pair_data_range: Range<usize>,
px_per_font_unit: f64,
}
impl CachedKernTable {
/// Search for a glyph pair in the kern table and return the corresponding value.
fn binary_search(&self, first_glyph: GlyphId, second_glyph: GlyphId) -> Option<i16> {
let pairs = &self.font_table.buffer()[self.pair_data_range.clone()];
let query = (first_glyph << 16) | second_glyph;
let (mut start, mut end) = (0, pairs.len() / KERN_PAIR_LEN);
while start < end {
let i = (start + end) / 2;
let key = BigEndian::read_u32(&pairs[i * KERN_PAIR_LEN..]);
match key.cmp(&query) {
Ordering::Less => start = i + 1,
Ordering::Equal => {
return Some(BigEndian::read_i16(&pairs[i * KERN_PAIR_LEN + 4..]));
},
Ordering::Greater => end = i,
}
}
None
}
}
impl fmt::Debug for CachedKernTable {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "CachedKernTable")
}
}
impl PlatformFont {
fn new(
font_identifier: FontIdentifier,
data: Option<&FontData>,
requested_size: Option<Au>,
) -> Result<PlatformFont, &'static str> {
let size = match requested_size {
Some(s) => s.to_f64_px(),
None => 0.0,
};
let Some(core_text_font) = CoreTextFontCache::core_text_font(font_identifier, data, size)
else {
return Err("Could not generate CTFont for FontTemplateData");
};
let mut handle = PlatformFont {
ctfont: core_text_font.clone_with_font_size(size),
h_kern_subtable: None,
};
handle.h_kern_subtable = handle.find_h_kern_subtable();
Ok(handle)
}
}
impl PlatformFontMethods for PlatformFont {
fn new_from_data(
font_identifier: FontIdentifier,
data: &FontData,
requested_size: Option<Au>,
) -> Result<PlatformFont, &'static str> {
Self::new(font_identifier, Some(data), requested_size)
}
fn new_from_local_font_identifier(
font_identifier: LocalFontIdentifier,
requested_size: Option<Au>,
) -> Result<PlatformFont, &'static str> {
Self::new(FontIdentifier::Local(font_identifier), None, requested_size)
}
fn descriptor(&self) -> FontTemplateDescriptor {
let traits = self.ctfont.all_traits();
FontTemplateDescriptor::new(traits.weight(), traits.stretch(), traits.style())
}
fn glyph_index(&self, codepoint: char) -> Option<GlyphId> {
// CTFontGetGlyphsForCharacters takes UniChar, which are UTF-16 encoded characters. We are taking
// a char here which is a 32bit Unicode character. This will encode into a maximum of two
// UTF-16 code units and produce a maximum of 1 glyph. We could safely pass 2 as the length
// of the buffer to CTFontGetGlyphsForCharacters, but passing the actual number of encoded
// code units ensures that the resulting glyph is always placed in the first slot in the output
// buffer.
let mut characters: [UniChar; 2] = [0, 0];
let encoded_characters = codepoint.encode_utf16(&mut characters);
let mut glyphs: [CGGlyph; 2] = [0, 0];
let result = unsafe {
self.ctfont.get_glyphs_for_characters(
encoded_characters.as_ptr(),
glyphs.as_mut_ptr(),
encoded_characters.len() as isize,
)
};
// If the call failed or the glyph is the zero glyph no glyph was found for this character.
if !result || glyphs[0] == 0 {
return None;
}
Some(glyphs[0] as GlyphId)
}
fn glyph_h_kerning(&self, first_glyph: GlyphId, second_glyph: GlyphId) -> FractionalPixel {
if let Some(ref table) = self.h_kern_subtable {
if let Some(font_units) = table.binary_search(first_glyph, second_glyph) {
return font_units as f64 * table.px_per_font_unit;
}
}
0.0
}
fn glyph_h_advance(&self, glyph: GlyphId) -> Option<FractionalPixel> {
let glyphs = [glyph as CGGlyph];
let advance = unsafe {
self.ctfont.get_advances_for_glyphs(
kCTFontDefaultOrientation,
&glyphs[0],
ptr::null_mut(),
1,
)
};
Some(advance as FractionalPixel)
}
fn metrics(&self) -> FontMetrics {
// TODO(mrobinson): Gecko first tries to get metrics from the SFNT tables via
// HarfBuzz and only afterward falls back to platform APIs. We should do something
// similar here. This will likely address issue #201 mentioned below.
let ascent = self.ctfont.ascent();
let descent = self.ctfont.descent();
let leading = self.ctfont.leading();
let x_height = self.ctfont.x_height();
let underline_thickness = self.ctfont.underline_thickness();
let line_gap = (ascent + descent + leading + 0.5).floor();
let max_advance = Au::from_f64_px(self.ctfont.bounding_box().size.width);
let zero_horizontal_advance = self
.glyph_index('0')
.and_then(|idx| self.glyph_h_advance(idx))
.map(Au::from_f64_px);
let average_advance = zero_horizontal_advance.unwrap_or(max_advance);
let ic_horizontal_advance = self
.glyph_index('\u{6C34}')
.and_then(|idx| self.glyph_h_advance(idx))
.map(Au::from_f64_px);
let space_advance = self
.glyph_index(' ')
.and_then(|index| self.glyph_h_advance(index))
.map(Au::from_f64_px)
.unwrap_or(average_advance);
let metrics = FontMetrics {
underline_size: Au::from_f64_au(underline_thickness),
// TODO(Issue #201): underline metrics are not reliable. Have to pull out of font table
// directly.
//
// see also: https://bugs.webkit.org/show_bug.cgi?id=16768
// see also: https://bugreports.qt-project.org/browse/QTBUG-13364
underline_offset: Au::from_f64_px(self.ctfont.underline_position()),
// There is no way to get these from CoreText or CoreGraphics APIs, so
// derive them from the other font metrics. These should eventually be
// found in the font tables directly when #201 is fixed.
strikeout_size: Au::from_f64_px(underline_thickness),
strikeout_offset: Au::from_f64_px((x_height + underline_thickness) / 2.0),
leading: Au::from_f64_px(leading),
x_height: Au::from_f64_px(x_height),
em_size: Au::from_f64_px(self.ctfont.pt_size()),
ascent: Au::from_f64_px(ascent),
descent: Au::from_f64_px(descent),
max_advance,
average_advance,
line_gap: Au::from_f64_px(line_gap),
zero_horizontal_advance,
ic_horizontal_advance,
space_advance,
};
debug!(
"Font metrics (@{} pt): {:?}",
self.ctfont.pt_size(),
metrics
);
metrics
}
fn table_for_tag(&self, tag: FontTableTag) -> Option<FontTable> {
let result: Option<CFData> = self.ctfont.get_font_table(tag);
result.map(FontTable::wrap)
}
/// Get the necessary [`FontInstanceFlags`]` for this font.
fn webrender_font_instance_flags(&self) -> FontInstanceFlags {
// TODO: Should this also validate these tables?
if self.table_for_tag(COLR).is_some() ||
self.table_for_tag(CBDT).is_some() ||
self.table_for_tag(SBIX).is_some()
{
return FontInstanceFlags::EMBEDDED_BITMAPS;
}
FontInstanceFlags::empty()
}
fn typographic_bounds(&self, glyph_id: GlyphId) -> Rect<f32> {
let rect = self
.ctfont
.get_bounding_rects_for_glyphs(kCTFontDefaultOrientation, &[glyph_id as u16]);
Rect::new(
Point2D::new(rect.origin.x as f32, rect.origin.y as f32),
Size2D::new(rect.size.width as f32, rect.size.height as f32),
)
}
}
pub(super) trait CoreTextFontTraitsMapping {
fn weight(&self) -> FontWeight;
fn style(&self) -> FontStyle;
fn stretch(&self) -> FontStretch;
}
impl CoreTextFontTraitsMapping for CTFontTraits {
fn weight(&self) -> FontWeight {
// From https://developer.apple.com/documentation/coretext/kctfontweighttrait?language=objc
// > The value returned is a CFNumberRef representing a float value between -1.0 and
// > 1.0 for normalized weight. The value of 0.0 corresponds to the regular or
// > medium font weight.
let mapping = [(-1., 0.), (0., 400.), (1., 1000.)];
let mapped_weight = map_platform_values_to_style_values(&mapping, self.normalized_weight());
FontWeight::from_float(mapped_weight as f32)
}
fn style(&self) -> FontStyle {
let slant = self.normalized_slant();
if slant == 0. && self.symbolic_traits().is_italic() {
return FontStyle::ITALIC;
}
if slant == 0. {
return FontStyle::NORMAL;
}
// From https://developer.apple.com/documentation/coretext/kctfontslanttrait?language=objc
// > The value returned is a CFNumberRef object representing a float value
// > between -1.0 and 1.0 for normalized slant angle. The value of 0.0
// > corresponds to 0 degrees clockwise rotation from the vertical and 1.0
// > corresponds to 30 degrees clockwise rotation.
let mapping = [(-1., -30.), (0., 0.), (1., 30.)];
let mapped_slant = map_platform_values_to_style_values(&mapping, slant);
FontStyle::oblique(mapped_slant as f32)
}
fn stretch(&self) -> FontStretch {
// From https://developer.apple.com/documentation/coretext/kctfontwidthtrait?language=objc
// > This value corresponds to the relative interglyph spacing for a given font.
// > The value returned is a CFNumberRef object representing a float between -1.0
// > and 1.0. The value of 0.0 corresponds to regular glyph spacing, and negative
// > values represent condensed glyph spacing.
FontStretch::from_percentage(self.normalized_width() as f32 + 1.0)
}
}