mirror of
https://github.com/servo/servo.git
synced 2025-06-04 07:35:36 +00:00
* 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>
395 lines
15 KiB
Rust
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)
|
|
}
|
|
}
|