feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)
Ports the macOS-specific Zig code behind cfg(target_os = "macos"): - renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend (all renderer/metal/* files, shaders.metal verbatim, runtime library compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga; OpenGL remains the backend elsewhere. - font: CoreText face, shaper and discovery with build.rs backend selection (coretext default; coretext-freetype / coretext-harfbuzz / coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple. - input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes. - os: macos, mach, cf_release_thread, NSProcessInfo args and macOS branches; config: macOS defaults, keybinds, Application Support paths. - termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit handling, os_log logging. Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot run here. The embedded runtime/C API (embedded.zig) is the next step. Also includes the cross-platform integration edits shared by the crash, verification, Windows and macOS work (Cargo target-gating, thread crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg gates), a cfg fix so ghostty-config builds on wasm32, and an allow for the intentional recursion in the crash overflow test. Test Plan: - cargo test --workspace (3224 passed) - cargo clippy --workspace --all-targets -- -D warnings - cargo check --target aarch64-apple-darwin -p ghostty-renderer (with the documented cross env)
This commit is contained in:
1 parent
76771bed0d
commit
bab49ee596
124 files changed
+14936
-1242
No files matched your search
@@ -0,0 +1,884 @@
|
||||
//! CoreText font face (macOS / iOS). Port of `font/face/coretext.zig`.
|
||||
//!
|
||||
//! Compile-checked on Linux with `cargo check --target aarch64-apple-darwin`;
|
||||
//! it cannot be run here. The platform-independent math (metrics priority,
|
||||
//! glyph bearings/canvas size, synthetic bold width) lives in
|
||||
//! [`super::coretext_math`] and is unit tested everywhere.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ptr::{self, NonNull};
|
||||
|
||||
use objc2_core_foundation::{CFData, CFNumber, CFRetained, CFString, CGAffineTransform, CGPoint};
|
||||
use objc2_core_graphics::{
|
||||
CGBitmapContextCreate,
|
||||
CGColorSpace,
|
||||
CGContext,
|
||||
CGImageAlphaInfo,
|
||||
CGImageByteOrderInfo,
|
||||
CGTextDrawingMode,
|
||||
kCGColorSpaceDisplayP3,
|
||||
kCGColorSpaceLinearGray,
|
||||
};
|
||||
use objc2_core_text::{CTFont, CTFontOrientation, CTFontSymbolicTraits};
|
||||
|
||||
use super::{
|
||||
DesiredSize,
|
||||
FontData,
|
||||
Options,
|
||||
RenderOptions,
|
||||
Variation,
|
||||
coretext_math::{self as math, CtMetrics, Measurements, Rect, synthetic_bold_line_width},
|
||||
};
|
||||
use crate::{
|
||||
atlas::{self, Atlas},
|
||||
coretext_util::{self as util, CfBox},
|
||||
glyph::Glyph,
|
||||
library::Library,
|
||||
metrics::FaceMetrics,
|
||||
opentype,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FaceError {
|
||||
/// CoreText could not create a font from the given data.
|
||||
#[error("font initialization failed")]
|
||||
FontInitFailure,
|
||||
/// The `SVG ` table is malformed.
|
||||
#[error("invalid SVG table")]
|
||||
InvalidSvgTable,
|
||||
/// The face has no backing file (used by the `coretext_freetype` backend).
|
||||
#[error("font has no file")]
|
||||
FontHasNoFile,
|
||||
/// The font file path could not be decoded.
|
||||
#[error("font path can't be decoded")]
|
||||
FontPathCantDecode,
|
||||
/// Creating a font variation failed.
|
||||
#[error("variation failed")]
|
||||
VariationFailed,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RenderError {
|
||||
#[error(transparent)]
|
||||
Atlas(#[from] atlas::Error),
|
||||
/// The atlas format does not match the glyph's color depth.
|
||||
#[error("invalid atlas format")]
|
||||
InvalidAtlasFormat,
|
||||
/// A CoreGraphics color space or bitmap context could not be created.
|
||||
#[error("coregraphics resource creation failed")]
|
||||
ContextFailed,
|
||||
/// The glyph index doesn't fit a CoreText `CGGlyph` (16 bits).
|
||||
#[error("invalid glyph index")]
|
||||
InvalidGlyph,
|
||||
}
|
||||
|
||||
/// A harfbuzz font owned by a [`Face`] (only used by the
|
||||
/// `coretext_harfbuzz` backend).
|
||||
#[cfg(font_harfbuzz)]
|
||||
pub struct HbFont(NonNull<harfbuzz_sys::hb_font_t>);
|
||||
|
||||
// SAFETY: hb_font_t is thread-safe for shaping when not mutated; Ghostty uses
|
||||
// it behind the collection lock exactly like upstream.
|
||||
#[cfg(font_harfbuzz)]
|
||||
unsafe impl Send for HbFont {}
|
||||
// SAFETY: see above.
|
||||
#[cfg(font_harfbuzz)]
|
||||
unsafe impl Sync for HbFont {}
|
||||
|
||||
#[cfg(font_harfbuzz)]
|
||||
impl HbFont {
|
||||
/// The raw `hb_font_t` pointer (valid while the `Face` lives).
|
||||
pub fn as_ptr(&self) -> *mut harfbuzz_sys::hb_font_t {
|
||||
self.0.as_ptr()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(font_harfbuzz)]
|
||||
impl Drop for HbFont {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: we own one reference to the hb font.
|
||||
unsafe { harfbuzz_sys::hb_font_destroy(self.0.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Face {
|
||||
/// Our font face
|
||||
font: CfBox<CTFont>,
|
||||
|
||||
/// Harfbuzz font corresponding to this face. We only use this
|
||||
/// if we're using Harfbuzz.
|
||||
#[cfg(font_harfbuzz)]
|
||||
hb_font: HbFont,
|
||||
|
||||
/// Set quirks.disableDefaultFontFeatures
|
||||
pub quirks_disable_default_font_features: bool,
|
||||
|
||||
/// True if this font face should be rasterized with a synthetic bold
|
||||
/// effect. This is used for fonts that don't have a bold variant.
|
||||
pub synthetic_bold: Option<f64>,
|
||||
|
||||
/// If the face can possibly be colored, then this is the state
|
||||
/// used to check for color information. This is None if the font
|
||||
/// can't possibly be colored (i.e. doesn't have SVG, sbix, etc
|
||||
/// tables).
|
||||
color: Option<ColorState>,
|
||||
|
||||
/// The current size this font is set to.
|
||||
pub size: DesiredSize,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Face {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Face")
|
||||
.field("size", &self.size)
|
||||
.field("synthetic_bold", &self.synthetic_bold)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// The matrix applied to a regular font to auto-italicize it.
|
||||
fn italic_skew() -> CGAffineTransform {
|
||||
CGAffineTransform {
|
||||
a: 1.0,
|
||||
b: 0.0,
|
||||
c: math::ITALIC_SKEW_C, // approx. tan(15)
|
||||
d: 1.0,
|
||||
tx: 0.0,
|
||||
ty: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
impl Face {
|
||||
/// Initialize a CoreText-based font from a TTF/TTC in memory.
|
||||
pub fn init(_lib: &Library, source: FontData, opts: Options) -> Result<Face, FaceError> {
|
||||
let data = match source {
|
||||
FontData::Static(s) => CFData::from_static_bytes(s),
|
||||
FontData::Shared(s) => CFData::from_bytes(&s),
|
||||
};
|
||||
|
||||
// SAFETY: `data` is a valid CFData.
|
||||
let desc = unsafe { objc2_core_text::CTFontManagerCreateFontDescriptorFromData(&data) }
|
||||
.ok_or(FaceError::FontInitFailure)?;
|
||||
|
||||
// SAFETY: valid descriptor, null matrix is allowed.
|
||||
let ct_font = unsafe { CTFont::with_font_descriptor(&desc, 12.0, ptr::null()) };
|
||||
|
||||
Self::init_font_copy(&ct_font, opts)
|
||||
}
|
||||
|
||||
/// Initialize a CoreText-based face from another initialized font face
|
||||
/// but with a new size. This is often how CoreText fonts are initialized
|
||||
/// because the font is loaded at a default size during discovery, and then
|
||||
/// adjusted to the final size for final load.
|
||||
pub fn init_font_copy(base: &CTFont, opts: Options) -> Result<Face, FaceError> {
|
||||
// Create a copy. The copyWithAttributes docs say the size is in points,
|
||||
// but we need to scale the points by the DPI and to do that we use our
|
||||
// function called "pixels".
|
||||
// SAFETY: null matrix and no descriptor are allowed.
|
||||
let ct_font =
|
||||
unsafe { base.copy_with_attributes(f64::from(opts.size.pixels()), ptr::null(), None) };
|
||||
|
||||
Self::init_font(ct_font, opts)
|
||||
}
|
||||
|
||||
/// Initialize a face with a CTFont. This takes ownership of the CTFont.
|
||||
pub fn init_font(ct_font: CFRetained<CTFont>, opts: Options) -> Result<Face, FaceError> {
|
||||
// SAFETY: plain CoreText call.
|
||||
let traits = unsafe { ct_font.symbolic_traits() };
|
||||
|
||||
#[cfg(font_harfbuzz)]
|
||||
let hb_font = {
|
||||
// SAFETY: valid CTFont; hb takes its own reference.
|
||||
let p = unsafe { harfbuzz_sys::coretext::hb_coretext_font_create(&ct_font) };
|
||||
let hb = NonNull::new(p)
|
||||
.map(HbFont)
|
||||
.ok_or(FaceError::FontInitFailure)?;
|
||||
// 26.6 fixed point scale
|
||||
let scale = (f64::from(opts.size.pixels()) * 64.0).round() as i32;
|
||||
// SAFETY: valid hb font.
|
||||
unsafe { harfbuzz_sys::hb_font_set_scale(hb.as_ptr(), scale, scale) };
|
||||
hb
|
||||
};
|
||||
|
||||
let color = if traits.contains(CTFontSymbolicTraits::TraitColorGlyphs) {
|
||||
Some(ColorState::new(&ct_font)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let result = Face {
|
||||
font: CfBox::new(ct_font),
|
||||
#[cfg(font_harfbuzz)]
|
||||
hb_font,
|
||||
quirks_disable_default_font_features: false,
|
||||
synthetic_bold: None,
|
||||
color,
|
||||
size: opts.size,
|
||||
};
|
||||
|
||||
// In debug mode, we output information about available variation axes,
|
||||
// if they exist.
|
||||
if cfg!(debug_assertions)
|
||||
&& let Some(axes) = util::variation_axes(&result.font)
|
||||
{
|
||||
log::debug!("variation axes font={}", result.name());
|
||||
for axis in axes {
|
||||
log::debug!(
|
||||
"variation axis: name={} id={} min={} max={} def={}",
|
||||
axis.name,
|
||||
String::from_utf8_lossy(&axis.id.to_be_bytes()),
|
||||
axis.min,
|
||||
axis.max,
|
||||
axis.default,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Return a new face that is the same as this but has a transformation
|
||||
/// matrix applied to italicize it.
|
||||
pub fn synthetic_italic(&self, opts: Options) -> Result<Face, FaceError> {
|
||||
let skew = italic_skew();
|
||||
// SAFETY: `skew` outlives the call.
|
||||
let ct_font = unsafe { self.font.copy_with_attributes(0.0, &raw const skew, None) };
|
||||
Self::init_font(ct_font, opts)
|
||||
}
|
||||
|
||||
/// Return a new face that is the same as this but applies a synthetic
|
||||
/// bold effect to it. This is useful for fonts that don't have a bold
|
||||
/// variant.
|
||||
pub fn synthetic_bold(&self, opts: Options) -> Result<Face, FaceError> {
|
||||
// SAFETY: null matrix and no descriptor are allowed.
|
||||
let ct_font = unsafe { self.font.copy_with_attributes(0.0, ptr::null(), None) };
|
||||
let mut face = Self::init_font(ct_font, opts)?;
|
||||
face.synthetic_bold = Some(synthetic_bold_line_width(f64::from(opts.size.points)));
|
||||
Ok(face)
|
||||
}
|
||||
|
||||
/// Returns the font name (the family name).
|
||||
pub fn name(&self) -> String {
|
||||
// SAFETY: plain CoreText call.
|
||||
util::cf_to_string(&*unsafe { self.font.family_name() })
|
||||
}
|
||||
|
||||
/// Resize the font in-place. If this succeeds, the caller is responsible
|
||||
/// for clearing any glyph caches, font atlas data, etc.
|
||||
pub fn set_size(&mut self, opts: Options) -> Result<(), FaceError> {
|
||||
// We just create a copy and replace ourself
|
||||
let face = Self::init_font_copy(&self.font, opts)?;
|
||||
*self = face;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the variation axes for this font. This will modify this font
|
||||
/// in-place.
|
||||
pub fn set_variations(&mut self, vs: &[Variation], opts: Options) -> Result<(), FaceError> {
|
||||
// If we have no variations, we don't need to do anything.
|
||||
if vs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create a new font descriptor with all the variations set.
|
||||
// SAFETY: plain CoreText call.
|
||||
let mut desc = unsafe { self.font.font_descriptor() };
|
||||
for v in vs {
|
||||
let id = CFNumber::new_i32(v.id.0 as i32);
|
||||
// SAFETY: valid number and descriptor.
|
||||
desc = unsafe { desc.copy_with_variation(&id, v.value) };
|
||||
}
|
||||
|
||||
// Put our current size in the opts so that we don't change size.
|
||||
let mut new_opts = opts;
|
||||
new_opts.size = self.size;
|
||||
|
||||
// Initialize a font based on these attributes.
|
||||
// SAFETY: null matrix is allowed.
|
||||
let ct_font = unsafe {
|
||||
self.font
|
||||
.copy_with_attributes(0.0, ptr::null(), Some(&desc))
|
||||
};
|
||||
let face = Self::init_font(ct_font, new_opts)?;
|
||||
*self = face;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if the face has any glyphs that are colorized.
|
||||
/// To determine if an individual glyph is colorized you must use
|
||||
/// `is_color_glyph`.
|
||||
pub fn has_color(&self) -> bool {
|
||||
self.color.is_some()
|
||||
}
|
||||
|
||||
/// Returns true if the given glyph ID is colorized.
|
||||
pub fn is_color_glyph(&self, glyph_id: u32) -> bool {
|
||||
match &self.color {
|
||||
Some(c) => c.is_color_glyph(glyph_id),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the glyph index for the given Unicode code point. If this
|
||||
/// face doesn't support this glyph, None is returned.
|
||||
pub fn glyph_index(&self, cp: u32) -> Option<u32> {
|
||||
let (glyph, ok) = util::glyphs_for_codepoint(&self.font, cp);
|
||||
if !ok {
|
||||
return None;
|
||||
}
|
||||
Some(u32::from(glyph))
|
||||
}
|
||||
|
||||
/// Render a glyph using the glyph index. The rendered glyph is stored in
|
||||
/// the given texture atlas.
|
||||
pub fn render_glyph(
|
||||
&self,
|
||||
atlas: &mut Atlas,
|
||||
glyph_index: u32,
|
||||
opts: RenderOptions,
|
||||
) -> Result<Glyph, RenderError> {
|
||||
let mut glyph = [u16::try_from(glyph_index).map_err(|_| RenderError::InvalidGlyph)?];
|
||||
// SAFETY: one valid glyph in the buffer.
|
||||
let ct_rect = unsafe {
|
||||
self.font.bounding_rects_for_glyphs(
|
||||
CTFontOrientation::Horizontal,
|
||||
NonNull::new_unchecked(glyph.as_mut_ptr()),
|
||||
ptr::null_mut(),
|
||||
1,
|
||||
)
|
||||
};
|
||||
// This is in a coordinate space with (0.0, 0.0)
|
||||
// in the bottom left and +Y pointing up.
|
||||
let rect = Rect {
|
||||
x: ct_rect.origin.x,
|
||||
y: ct_rect.origin.y,
|
||||
width: ct_rect.size.width,
|
||||
height: ct_rect.size.height,
|
||||
};
|
||||
|
||||
// Determine whether this is a color glyph.
|
||||
let is_color = self.is_color_glyph(glyph_index);
|
||||
// And whether it's (probably) a bitmap (sbix).
|
||||
let sbix = is_color && self.color.as_ref().is_some_and(|c| c.sbix);
|
||||
|
||||
let Some(layout) = math::glyph_layout(rect, sbix, self.synthetic_bold, &opts) else {
|
||||
return Ok(Glyph {
|
||||
width: 0,
|
||||
height: 0,
|
||||
offset_x: 0,
|
||||
offset_y: 0,
|
||||
atlas_x: 0,
|
||||
atlas_y: 0,
|
||||
});
|
||||
};
|
||||
|
||||
// Settings that are specific to if we are rendering text or emoji.
|
||||
let depth = math::bitmap_depth(is_color);
|
||||
let (space, context_opts) = if is_color {
|
||||
// SAFETY: valid static CFString constant.
|
||||
let space = CGColorSpace::with_name(Some(unsafe { kCGColorSpaceDisplayP3 }));
|
||||
(
|
||||
space,
|
||||
CGImageByteOrderInfo::Order32Little.0 | CGImageAlphaInfo::PremultipliedFirst.0,
|
||||
)
|
||||
} else {
|
||||
// SAFETY: valid static CFString constant.
|
||||
let space = CGColorSpace::with_name(Some(unsafe { kCGColorSpaceLinearGray }));
|
||||
(space, CGImageAlphaInfo::Only.0)
|
||||
};
|
||||
let space = space.ok_or(RenderError::ContextFailed)?;
|
||||
|
||||
// This is just a safety check.
|
||||
if u32::from(atlas.format.depth()) != depth {
|
||||
log::warn!(
|
||||
"font atlas color depth doesn't equal font color depth atlas={} font={}",
|
||||
atlas.format.depth(),
|
||||
depth,
|
||||
);
|
||||
return Err(RenderError::InvalidAtlasFormat);
|
||||
}
|
||||
|
||||
// Our buffer for rendering. We could cache this but glyph rasterization
|
||||
// usually stabilizes pretty quickly and is very infrequent so I think
|
||||
// the allocation overhead is acceptable compared to the cost of
|
||||
// caching it forever or having to deal with a cache lifetime.
|
||||
let width = layout.px_width as usize;
|
||||
let height = layout.px_height as usize;
|
||||
let mut buf = vec![0u8; width * height * depth as usize];
|
||||
|
||||
// SAFETY: `buf` is `width * height * depth` bytes, matching the
|
||||
// bytes-per-row and dimensions given; it outlives the context (the
|
||||
// context is dropped before `buf` at the end of this function).
|
||||
let ctx = unsafe {
|
||||
CGBitmapContextCreate(
|
||||
buf.as_mut_ptr().cast(),
|
||||
width,
|
||||
height,
|
||||
8,
|
||||
width * depth as usize,
|
||||
Some(&space),
|
||||
context_opts,
|
||||
)
|
||||
}
|
||||
.ok_or(RenderError::ContextFailed)?;
|
||||
let c = Some(&*ctx);
|
||||
|
||||
// Perform an initial fill. This ensures that we don't have any
|
||||
// uninitialized pixels in the bitmap.
|
||||
if is_color {
|
||||
CGContext::set_rgb_fill_color(c, 0.0, 0.0, 0.0, 0.0);
|
||||
} else {
|
||||
CGContext::set_gray_fill_color(c, 0.0, 0.0);
|
||||
}
|
||||
CGContext::fill_rect(
|
||||
c,
|
||||
objc2_core_foundation::CGRect::new(
|
||||
CGPoint::new(0.0, 0.0),
|
||||
objc2_core_foundation::CGSize::new(width as f64, height as f64),
|
||||
),
|
||||
);
|
||||
|
||||
// "Font smoothing" is what we call "thickening", it's an attempt
|
||||
// to compensate for optical thinning of fonts, but at this point
|
||||
// it's just something that makes the text look closer to system
|
||||
// applications if users want that.
|
||||
CGContext::set_allows_font_smoothing(c, true);
|
||||
CGContext::set_should_smooth_fonts(c, opts.thicken);
|
||||
|
||||
// Subpixel positioning allows glyphs to be placed at non-integer
|
||||
// coordinates. We need this for our alignment.
|
||||
CGContext::set_allows_font_subpixel_positioning(c, true);
|
||||
CGContext::set_should_subpixel_position_fonts(c, true);
|
||||
|
||||
// We don't want subpixel quantization, since we very carefully
|
||||
// manage the position of our glyphs ourselves, and dont want to
|
||||
// mess that up.
|
||||
CGContext::set_allows_font_subpixel_quantization(c, false);
|
||||
CGContext::set_should_subpixel_quantize_fonts(c, false);
|
||||
|
||||
// Anti-aliasing is self explanatory.
|
||||
CGContext::set_allows_antialiasing(c, true);
|
||||
CGContext::set_should_antialias(c, true);
|
||||
|
||||
// Set our color for drawing
|
||||
if is_color {
|
||||
CGContext::set_rgb_fill_color(c, 1.0, 1.0, 1.0, 1.0);
|
||||
CGContext::set_rgb_stroke_color(c, 1.0, 1.0, 1.0, 1.0);
|
||||
} else {
|
||||
let strength = f64::from(opts.thicken_strength);
|
||||
CGContext::set_gray_fill_color(c, strength / 255.0, 1.0);
|
||||
CGContext::set_gray_stroke_color(c, strength / 255.0, 1.0);
|
||||
}
|
||||
|
||||
// If we are drawing with synthetic bold then use a fill stroke
|
||||
// which strokes the outlines of the glyph making a more bold look.
|
||||
if let Some(line_width) = self.synthetic_bold {
|
||||
CGContext::set_text_drawing_mode(c, CGTextDrawingMode::FillStroke);
|
||||
CGContext::set_line_width(c, line_width);
|
||||
}
|
||||
|
||||
// Translate our drawing context so that when we draw our
|
||||
// glyph the bottom/left edge is at the correct sub-pixel
|
||||
// position. The bottom/left edges are guaranteed to be at
|
||||
// exactly [0, 0] relative to this because when we call to
|
||||
// `drawGlyphs`, we pass the negated bearings.
|
||||
CGContext::translate_ctm(
|
||||
c,
|
||||
layout.frac_x + f64::from(layout.canvas_padding),
|
||||
layout.frac_y + f64::from(layout.canvas_padding),
|
||||
);
|
||||
|
||||
// Scale the drawing context so that when we draw
|
||||
// our glyph it's stretched to the constrained size.
|
||||
CGContext::scale_ctm(c, layout.scale_x, layout.scale_y);
|
||||
|
||||
// Draw our glyph.
|
||||
//
|
||||
// We offset the position by the negated bearings so that the
|
||||
// glyph is drawn at exactly [0, 0], which is then offset to
|
||||
// the appropriate fractional position by the translation we
|
||||
// did before scaling.
|
||||
let mut position = CGPoint::new(-layout.rect_x, -layout.rect_y);
|
||||
// SAFETY: one glyph and one position, valid context.
|
||||
unsafe {
|
||||
self.font.draw_glyphs(
|
||||
NonNull::new_unchecked(glyph.as_mut_ptr()),
|
||||
NonNull::from(&mut position),
|
||||
1,
|
||||
&ctx,
|
||||
);
|
||||
}
|
||||
|
||||
// The context must be released before we read the buffer.
|
||||
drop(ctx);
|
||||
|
||||
// Write our rasterized glyph to the atlas.
|
||||
let region = atlas.reserve(layout.px_width, layout.px_height)?;
|
||||
atlas.set(region, &buf);
|
||||
|
||||
Ok(Glyph {
|
||||
width: layout.px_width,
|
||||
height: layout.px_height,
|
||||
offset_x: layout.offset_x(),
|
||||
offset_y: layout.offset_y(),
|
||||
atlas_x: region.x,
|
||||
atlas_y: region.y,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the `FaceMetrics` for this face.
|
||||
pub fn get_metrics(&self) -> FaceMetrics {
|
||||
let ct_font: &CTFont = &self.font;
|
||||
|
||||
// Read the 'head' table out of the font data.
|
||||
//
|
||||
// macOS bitmap-only fonts use a 'bhed' tag rather than 'head', but
|
||||
// the table format is byte-identical to the 'head' table, so if we
|
||||
// can't find 'head' we try 'bhed' instead before failing.
|
||||
//
|
||||
// ref: https://fontforge.org/docs/techref/bitmaponlysfnt.html
|
||||
let head = util::font_table_bytes(ct_font, b"head")
|
||||
.or_else(|| util::font_table_bytes(ct_font, b"bhed"))
|
||||
.and_then(|d| {
|
||||
opentype::Head::init(&d)
|
||||
.inspect_err(|e| log::warn!("error parsing head table: {e}"))
|
||||
.ok()
|
||||
});
|
||||
|
||||
// Read the 'post' table out of the font data.
|
||||
let post = util::font_table_bytes(ct_font, b"post").and_then(|d| {
|
||||
opentype::Post::init(&d)
|
||||
.inspect_err(|e| log::warn!("error parsing post table: {e}"))
|
||||
.ok()
|
||||
});
|
||||
|
||||
// Read the 'OS/2' table out of the font data if it's available.
|
||||
let os2 = util::font_table_bytes(ct_font, b"OS/2").and_then(|d| {
|
||||
opentype::OS2::init(&d)
|
||||
.inspect_err(|e| log::warn!("error parsing OS/2 table: {e}"))
|
||||
.ok()
|
||||
});
|
||||
|
||||
// Read the 'hhea' table out of the font data.
|
||||
let hhea = util::font_table_bytes(ct_font, b"hhea").and_then(|d| {
|
||||
opentype::Hhea::init(&d)
|
||||
.inspect_err(|e| log::warn!("error parsing hhea table: {e}"))
|
||||
.ok()
|
||||
});
|
||||
|
||||
// SAFETY: plain CoreText getters.
|
||||
let (units_per_em, px_per_em, ct) = unsafe {
|
||||
(
|
||||
head.map_or_else(
|
||||
|| f64::from(ct_font.units_per_em()),
|
||||
|h| f64::from(h.unitsPerEm),
|
||||
),
|
||||
ct_font.size(),
|
||||
CtMetrics {
|
||||
ascent: ct_font.ascent(),
|
||||
descent: ct_font.descent(),
|
||||
leading: ct_font.leading(),
|
||||
cap_height: ct_font.cap_height(),
|
||||
x_height: ct_font.x_height(),
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
// Cell width is calculated by calculating the widest width of the
|
||||
// visible ASCII characters. Usually 'M' is widest but we just take
|
||||
// whatever is widest.
|
||||
//
|
||||
// ASCII height is calculated as the height of the overall bounding
|
||||
// box of the same characters.
|
||||
let (cell_width, ascii_height) = {
|
||||
// All the visible ASCII chars
|
||||
let mut unichars: Vec<u16> = (32u16..127).collect();
|
||||
let n = unichars.len();
|
||||
|
||||
// Get our glyph IDs for the ASCII chars
|
||||
let mut glyphs = vec![0u16; n];
|
||||
// SAFETY: both buffers have `n` elements.
|
||||
unsafe {
|
||||
ct_font.glyphs_for_characters(
|
||||
NonNull::new_unchecked(unichars.as_mut_ptr()),
|
||||
NonNull::new_unchecked(glyphs.as_mut_ptr()),
|
||||
n as isize,
|
||||
);
|
||||
}
|
||||
|
||||
// Get all our advances
|
||||
let mut advances = vec![objc2_core_foundation::CGSize::new(0.0, 0.0); n];
|
||||
// SAFETY: buffers have `n` elements.
|
||||
unsafe {
|
||||
ct_font.advances_for_glyphs(
|
||||
CTFontOrientation::Horizontal,
|
||||
NonNull::new_unchecked(glyphs.as_mut_ptr()),
|
||||
advances.as_mut_ptr(),
|
||||
n as isize,
|
||||
);
|
||||
}
|
||||
|
||||
// Find the maximum advance
|
||||
let max = advances.iter().fold(0.0f64, |m, a| a.width.max(m));
|
||||
|
||||
// Get the overall bounding rect for the glyphs
|
||||
// SAFETY: `glyphs` has `n` elements.
|
||||
let rect = unsafe {
|
||||
ct_font.bounding_rects_for_glyphs(
|
||||
CTFontOrientation::Horizontal,
|
||||
NonNull::new_unchecked(glyphs.as_mut_ptr()),
|
||||
ptr::null_mut(),
|
||||
n as isize,
|
||||
)
|
||||
};
|
||||
|
||||
(max, rect.size.height)
|
||||
};
|
||||
|
||||
// Measure "水" (CJK water ideograph, U+6C34) for our ic width.
|
||||
let ic_width = self.glyph_index('水' as u32).and_then(|glyph| {
|
||||
let mut g = [glyph as u16];
|
||||
// SAFETY: one valid glyph.
|
||||
let (advance, bounds) = unsafe {
|
||||
(
|
||||
ct_font.advances_for_glyphs(
|
||||
CTFontOrientation::Horizontal,
|
||||
NonNull::new_unchecked(g.as_mut_ptr()),
|
||||
ptr::null_mut(),
|
||||
1,
|
||||
),
|
||||
ct_font.bounding_rects_for_glyphs(
|
||||
CTFontOrientation::Horizontal,
|
||||
NonNull::new_unchecked(g.as_mut_ptr()),
|
||||
ptr::null_mut(),
|
||||
1,
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
// If the advance of the glyph is less than the width of the actual
|
||||
// glyph then we just treat it as invalid since it's probably wrong
|
||||
// and using it for size normalization will instead make the font
|
||||
// way too big.
|
||||
//
|
||||
// This can sometimes happen if there's a CJK font that has been
|
||||
// patched with the nerd fonts patcher and it butchers the advance
|
||||
// values so the advance ends up half the width of the actual glyph.
|
||||
if !math::ic_width_is_valid(bounds.size.width, advance) {
|
||||
log::warn!(
|
||||
"(getMetrics) Width of glyph '水' for font \"{}\" is greater than its advance ({} > {}), discarding ic_width metric.",
|
||||
self.name(),
|
||||
bounds.size.width,
|
||||
advance,
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(advance)
|
||||
});
|
||||
|
||||
math::face_metrics(
|
||||
px_per_em,
|
||||
units_per_em,
|
||||
hhea.as_ref(),
|
||||
os2.as_ref(),
|
||||
post.as_ref(),
|
||||
&ct,
|
||||
Measurements {
|
||||
cell_width,
|
||||
ascii_height,
|
||||
ic_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Copy the font table data for the given tag.
|
||||
pub fn copy_table(&self, tag: &[u8; 4]) -> Result<Option<Vec<u8>>, FaceError> {
|
||||
Ok(util::font_table_bytes(&self.font, tag))
|
||||
}
|
||||
|
||||
/// The underlying CTFont (for shapers).
|
||||
pub(crate) fn ct_font(&self) -> &CTFont {
|
||||
&self.font
|
||||
}
|
||||
|
||||
/// The harfbuzz font corresponding to this face (for the HarfBuzz shaper).
|
||||
#[cfg(font_harfbuzz)]
|
||||
pub fn hb_font(&self) -> &HbFont {
|
||||
&self.hb_font
|
||||
}
|
||||
|
||||
/// The `FontHasNoFile`-style path of this face's file (for the
|
||||
/// `coretext_freetype` backend this is done by the deferred face).
|
||||
pub fn font_descriptor(&self) -> CFRetained<objc2_core_text::CTFontDescriptor> {
|
||||
// SAFETY: plain CoreText call.
|
||||
unsafe { self.font.font_descriptor() }
|
||||
}
|
||||
}
|
||||
|
||||
/// The state associated with a font face that may have colorized glyphs.
|
||||
/// This is used to determine if a specific glyph ID is colorized.
|
||||
struct ColorState {
|
||||
/// True if there is an sbix font table. For now, the mere presence
|
||||
/// of an sbix font table causes us to assume the glyph is colored.
|
||||
/// We can improve this later.
|
||||
sbix: bool,
|
||||
|
||||
/// The SVG font table data (if any), which we can use to determine
|
||||
/// if a glyph is present in the SVG table.
|
||||
svg: Option<SvgTable>,
|
||||
}
|
||||
|
||||
/// An owned copy of the `SVG ` table and the glyph range it covers.
|
||||
struct SvgTable {
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ColorState {
|
||||
fn new(f: &CTFont) -> Result<ColorState, FaceError> {
|
||||
// sbix is true if the table exists in the font data at all.
|
||||
// In the future we probably want to actually parse it and
|
||||
// check for glyphs.
|
||||
let sbix = util::font_table(f, b"sbix").is_some_and(|d| !d.is_empty());
|
||||
|
||||
// Read the SVG table out of the font data.
|
||||
let svg = match util::font_table_bytes(f, b"SVG ") {
|
||||
None => None,
|
||||
Some(data) => {
|
||||
// Validate the table up front like Zig.
|
||||
opentype::SVG::init(&data).map_err(|_| FaceError::InvalidSvgTable)?;
|
||||
Some(SvgTable { data })
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ColorState { sbix, svg })
|
||||
}
|
||||
|
||||
/// Returns true if the given glyph ID is colored.
|
||||
fn is_color_glyph(&self, glyph_id: u32) -> bool {
|
||||
// Our font system uses 32-bit glyph IDs for special values but
|
||||
// actual fonts only contain 16-bit glyph IDs so if we can't cast
|
||||
// into it it must be false.
|
||||
let Ok(glyph_u16) = u16::try_from(glyph_id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// sbix is always true for now
|
||||
if self.sbix {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if we have svg data, check it
|
||||
if let Some(svg) = &self.svg
|
||||
&& let Ok(table) = opentype::SVG::init(&svg.data)
|
||||
&& table.has_glyph(glyph_u16)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a CTFont by name at the given size (test/dev helper equivalent of
|
||||
/// `FontDescriptor.createWithNameAndSize` + `Font.createWithFontDescriptor`).
|
||||
pub fn font_with_name(name: &str, size: f64) -> CFRetained<CTFont> {
|
||||
let cf_name = CFString::from_str(name);
|
||||
// SAFETY: valid name; null matrix is allowed.
|
||||
unsafe { CTFont::with_name(&cf_name, size, ptr::null()) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{embedded, metrics::Metrics};
|
||||
|
||||
fn opts(points: f32) -> Options {
|
||||
Options::new(DesiredSize::new(points))
|
||||
}
|
||||
|
||||
fn render_ascii(face: &Face) {
|
||||
let mut atlas = Atlas::new(512, atlas::Format::Grayscale);
|
||||
for i in 32u32..127 {
|
||||
let g = face.glyph_index(i).unwrap();
|
||||
face.render_glyph(
|
||||
&mut atlas,
|
||||
g,
|
||||
RenderOptions::new(Metrics::calc(face.get_metrics())),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monaco_ascii() {
|
||||
let font = font_with_name("Monaco", 12.0);
|
||||
let face = Face::init_font_copy(&font, opts(12.0)).unwrap();
|
||||
render_ascii(&face);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name() {
|
||||
let font = font_with_name("Menlo", 12.0);
|
||||
let face = Face::init_font_copy(&font, opts(12.0)).unwrap();
|
||||
assert_eq!(face.name(), "Menlo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emoji() {
|
||||
let font = font_with_name("Apple Color Emoji", 12.0);
|
||||
let face = Face::init_font_copy(&font, opts(18.0)).unwrap();
|
||||
let id = face.glyph_index('🥸' as u32).unwrap();
|
||||
assert!(face.is_color_glyph(id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_memory() {
|
||||
let lib = Library::init().unwrap();
|
||||
let face = Face::init(&lib, FontData::Static(embedded::REGULAR), opts(12.0)).unwrap();
|
||||
render_ascii(&face);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable() {
|
||||
let lib = Library::init().unwrap();
|
||||
let face = Face::init(&lib, FontData::Static(embedded::VARIABLE), opts(12.0)).unwrap();
|
||||
render_ascii(&face);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_set_variation() {
|
||||
let lib = Library::init().unwrap();
|
||||
let mut face = Face::init(&lib, FontData::Static(embedded::VARIABLE), opts(12.0)).unwrap();
|
||||
face.set_variations(
|
||||
&[Variation {
|
||||
id: super::super::VariationId::init(b"wght"),
|
||||
value: 400.0,
|
||||
}],
|
||||
opts(12.0),
|
||||
)
|
||||
.unwrap();
|
||||
render_ascii(&face);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_font_table() {
|
||||
let lib = Library::init().unwrap();
|
||||
let face = Face::init(&lib, FontData::Static(embedded::JULIA_MONO), opts(12.0)).unwrap();
|
||||
let table = face.copy_table(b"SVG ").unwrap().unwrap();
|
||||
assert_ne!(table.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyph_index_colored_vs_text() {
|
||||
let lib = Library::init().unwrap();
|
||||
let face = Face::init(&lib, FontData::Static(embedded::JULIA_MONO), opts(12.0)).unwrap();
|
||||
let glyph = face.glyph_index('A' as u32).unwrap();
|
||||
assert_eq!(glyph, 4);
|
||||
assert!(!face.is_color_glyph(glyph));
|
||||
|
||||
let glyph = face.glyph_index(0xE800).unwrap();
|
||||
assert_eq!(glyph, 11482);
|
||||
assert!(face.is_color_glyph(glyph));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,956 @@
|
||||
//! Platform-independent math extracted from the CoreText face
|
||||
//! (`font/face/coretext.zig`) so it can be unit tested on every platform.
|
||||
//!
|
||||
//! The CoreText face itself only exists on Apple targets; everything here is
|
||||
//! plain arithmetic on values that the face reads from CoreText / the
|
||||
//! OpenType tables:
|
||||
//!
|
||||
//! * [`face_metrics`]: the `getMetrics` table-priority logic (OS/2 typo
|
||||
//! metrics vs. hhea vs. OS/2 win metrics, underline/strikethrough
|
||||
//! fallbacks, cap/ex heights).
|
||||
//! * [`synthetic_bold_line_width`]: the synthetic bold stroke width.
|
||||
//! * [`glyph_layout`]: the bearing / canvas-size computation of
|
||||
//! `renderGlyph`.
|
||||
|
||||
use super::{Constraint, ConstraintSize, GlyphSize, RenderOptions};
|
||||
use crate::{
|
||||
metrics::FaceMetrics,
|
||||
opentype::{Hhea, OS2, Post},
|
||||
};
|
||||
|
||||
/// The matrix applied to a regular font to auto-italicize it
|
||||
/// (`Face.italic_skew`): `a=1, b=0, c=tan(15deg) (approx), d=1, tx=ty=0`.
|
||||
pub const ITALIC_SKEW_C: f64 = 0.267949;
|
||||
|
||||
/// Metrics CoreText itself reports (used as fallbacks when a table is
|
||||
/// missing). `descent` is CoreText's positive-down value.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct CtMetrics {
|
||||
pub ascent: f64,
|
||||
/// `CTFontGetDescent`: positive distance below the baseline.
|
||||
pub descent: f64,
|
||||
pub leading: f64,
|
||||
pub cap_height: f64,
|
||||
pub x_height: f64,
|
||||
}
|
||||
|
||||
/// Measurements taken by drawing/measuring glyphs.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct Measurements {
|
||||
/// The widest advance of the visible ASCII characters.
|
||||
pub cell_width: f64,
|
||||
/// Height of the overall bounding box of the visible ASCII characters.
|
||||
pub ascii_height: f64,
|
||||
/// Advance of U+6C34 (None if absent or considered invalid).
|
||||
pub ic_width: Option<f64>,
|
||||
}
|
||||
|
||||
/// The line width used for synthetic bold given the font size in points.
|
||||
///
|
||||
/// To determine our synthetic bold line width we get a multiplier from the
|
||||
/// font size in points. This is a heuristic that is based on the fact that
|
||||
/// a line width of 1 looks good at a certain point size. We want to scale
|
||||
/// that up roughly linearly with the font size.
|
||||
pub fn synthetic_bold_line_width(points: f64) -> f64 {
|
||||
(points / 14.0).max(1.0)
|
||||
}
|
||||
|
||||
/// Returns `(ascent, descent, line_gap)` in pixels, +Y up (`descent` is
|
||||
/// normally negative), following the table priority of `getMetrics`.
|
||||
pub fn vertical_metrics(
|
||||
hhea: Option<&Hhea>,
|
||||
os2: Option<&OS2>,
|
||||
px_per_unit: f64,
|
||||
ct: &CtMetrics,
|
||||
) -> (f64, f64, f64) {
|
||||
// If we couldn't get the hhea table, rely on metrics from CoreText.
|
||||
let Some(hhea) = hhea else {
|
||||
return (ct.ascent, -ct.descent, ct.leading);
|
||||
};
|
||||
|
||||
let hhea_ascent = f64::from(hhea.ascender);
|
||||
let hhea_descent = f64::from(hhea.descender);
|
||||
let hhea_line_gap = f64::from(hhea.lineGap);
|
||||
|
||||
// If our font has no OS/2 table, then we just
|
||||
// blindly use the metrics from the hhea table.
|
||||
let Some(os2) = os2 else {
|
||||
return (
|
||||
hhea_ascent * px_per_unit,
|
||||
hhea_descent * px_per_unit,
|
||||
hhea_line_gap * px_per_unit,
|
||||
);
|
||||
};
|
||||
|
||||
let os2_ascent = f64::from(os2.sTypoAscender);
|
||||
let os2_descent = f64::from(os2.sTypoDescender);
|
||||
let os2_line_gap = f64::from(os2.sTypoLineGap);
|
||||
|
||||
// If the font says to use typo metrics, trust it.
|
||||
if os2.fsSelection.use_typo_metrics {
|
||||
return (
|
||||
os2_ascent * px_per_unit,
|
||||
os2_descent * px_per_unit,
|
||||
os2_line_gap * px_per_unit,
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise we prefer the height metrics from 'hhea' if they
|
||||
// are available, or else OS/2 sTypo* metrics, and if all else
|
||||
// fails then we use OS/2 usWin* metrics.
|
||||
//
|
||||
// This is not "standard" behavior, but it's our best bet to
|
||||
// account for fonts being... just weird. It's pretty much what
|
||||
// FreeType does to get its generic ascent and descent metrics.
|
||||
if hhea.ascender != 0 || hhea.descender != 0 {
|
||||
return (
|
||||
hhea_ascent * px_per_unit,
|
||||
hhea_descent * px_per_unit,
|
||||
hhea_line_gap * px_per_unit,
|
||||
);
|
||||
}
|
||||
|
||||
if os2_ascent != 0.0 || os2_descent != 0.0 {
|
||||
return (
|
||||
os2_ascent * px_per_unit,
|
||||
os2_descent * px_per_unit,
|
||||
os2_line_gap * px_per_unit,
|
||||
);
|
||||
}
|
||||
|
||||
let win_ascent = f64::from(os2.usWinAscent);
|
||||
let win_descent = f64::from(os2.usWinDescent);
|
||||
(
|
||||
win_ascent * px_per_unit,
|
||||
// usWinDescent is *positive* -> down unlike sTypoDescender
|
||||
// and hhea.Descender, so we flip its sign to fix this.
|
||||
-win_descent * px_per_unit,
|
||||
0.0,
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute the `FaceMetrics` from the (optionally present) OpenType tables,
|
||||
/// CoreText's own metrics and the measurements. `px_per_em` is
|
||||
/// `CTFontGetSize` and `units_per_em` the `head` table value (or
|
||||
/// `CTFontGetUnitsPerEm` if there is no `head`/`bhed` table).
|
||||
pub fn face_metrics(
|
||||
px_per_em: f64,
|
||||
units_per_em: f64,
|
||||
hhea: Option<&Hhea>,
|
||||
os2: Option<&OS2>,
|
||||
post: Option<&Post>,
|
||||
ct: &CtMetrics,
|
||||
measured: Measurements,
|
||||
) -> FaceMetrics {
|
||||
let px_per_unit = px_per_em / units_per_em;
|
||||
|
||||
let (ascent, descent, line_gap) = vertical_metrics(hhea, os2, px_per_unit, ct);
|
||||
|
||||
let (underline_position, underline_thickness) = match post {
|
||||
None => (None, None),
|
||||
Some(post) => {
|
||||
// Some fonts have degenerate 'post' tables where the underline
|
||||
// thickness (and often position) are 0. We consider them null
|
||||
// if this is the case and use our own fallbacks when we calculate.
|
||||
let has_broken_underline = post.underlineThickness == 0;
|
||||
|
||||
// If the underline position isn't 0 then we do use it,
|
||||
// even if the thickness is't properly specified.
|
||||
let pos = if has_broken_underline && post.underlinePosition == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(f64::from(post.underlinePosition) * px_per_unit)
|
||||
};
|
||||
|
||||
let thick = if has_broken_underline {
|
||||
None
|
||||
} else {
|
||||
Some(f64::from(post.underlineThickness) * px_per_unit)
|
||||
};
|
||||
|
||||
(pos, thick)
|
||||
}
|
||||
};
|
||||
|
||||
// Similar logic to the underline above.
|
||||
let (strikethrough_position, strikethrough_thickness) = match os2 {
|
||||
None => (None, None),
|
||||
Some(os2) => {
|
||||
let has_broken_strikethrough = os2.yStrikeoutSize == 0;
|
||||
|
||||
let pos = if has_broken_strikethrough && os2.yStrikeoutPosition == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(f64::from(os2.yStrikeoutPosition) * px_per_unit)
|
||||
};
|
||||
|
||||
let thick = if has_broken_strikethrough {
|
||||
None
|
||||
} else {
|
||||
Some(f64::from(os2.yStrikeoutSize) * px_per_unit)
|
||||
};
|
||||
|
||||
(pos, thick)
|
||||
}
|
||||
};
|
||||
|
||||
// We fall back to whatever CoreText does if the
|
||||
// OS/2 table doesn't specify a cap or ex height.
|
||||
let (cap_height, ex_height) = match os2 {
|
||||
None => (ct.cap_height, ct.x_height),
|
||||
Some(os2) => (
|
||||
os2.sCapHeight
|
||||
.map_or(ct.cap_height, |v| f64::from(v) * px_per_unit),
|
||||
os2.sxHeight
|
||||
.map_or(ct.x_height, |v| f64::from(v) * px_per_unit),
|
||||
),
|
||||
};
|
||||
|
||||
FaceMetrics {
|
||||
px_per_em,
|
||||
|
||||
cell_width: measured.cell_width,
|
||||
|
||||
ascent,
|
||||
descent,
|
||||
line_gap,
|
||||
|
||||
underline_position,
|
||||
underline_thickness,
|
||||
|
||||
strikethrough_position,
|
||||
strikethrough_thickness,
|
||||
|
||||
cap_height: Some(cap_height),
|
||||
ex_height: Some(ex_height),
|
||||
ascii_height: Some(measured.ascii_height),
|
||||
ic_width: measured.ic_width,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the advance of the "水" glyph usable as the ic width? If the advance
|
||||
/// is less than the width of the actual glyph then it is probably wrong
|
||||
/// (e.g. a CJK font patched by the nerd fonts patcher) and using it for size
|
||||
/// normalization would make the font way too big.
|
||||
pub fn ic_width_is_valid(glyph_width: f64, advance: f64) -> bool {
|
||||
glyph_width <= advance || glyph_width.is_nan() || advance.is_nan()
|
||||
}
|
||||
|
||||
/// A CoreText bounding rect (origin is bottom-left, +Y up).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct Rect {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
}
|
||||
|
||||
/// The result of laying out a glyph for rasterization.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct GlyphLayout {
|
||||
/// Whole pixel bearings of the final glyph.
|
||||
pub px_x: i32,
|
||||
pub px_y: i32,
|
||||
/// Canvas size (includes padding and the fractional offset).
|
||||
pub px_width: u32,
|
||||
pub px_height: u32,
|
||||
/// Extra pixels around the glyph for font smoothing.
|
||||
pub canvas_padding: u32,
|
||||
/// Fractional part of the pixel bearings, added as an offset when
|
||||
/// rasterizing.
|
||||
pub frac_x: f64,
|
||||
pub frac_y: f64,
|
||||
/// Scale applied to the context so the glyph is stretched to the
|
||||
/// constrained size.
|
||||
pub scale_x: f64,
|
||||
pub scale_y: f64,
|
||||
/// The (possibly bold-adjusted) bounding rect origin; the glyph is drawn
|
||||
/// at the negated origin.
|
||||
pub rect_x: f64,
|
||||
pub rect_y: f64,
|
||||
}
|
||||
|
||||
impl GlyphLayout {
|
||||
/// `Glyph.offset_y`: distance from the bottom of the cell to the top of
|
||||
/// the glyph's bounding box.
|
||||
pub fn offset_y(&self) -> i32 {
|
||||
self.px_y + self.px_height as i32
|
||||
}
|
||||
|
||||
/// `Glyph.offset_x`: distance from the left of the cell to the left of
|
||||
/// the glyph's bounding box.
|
||||
pub fn offset_x(&self) -> i32 {
|
||||
self.px_x
|
||||
}
|
||||
}
|
||||
|
||||
/// The bearing / size computation from `renderGlyph`.
|
||||
///
|
||||
/// `rect` is `CTFontGetBoundingRectsForGlyphs` for the glyph. Returns `None`
|
||||
/// if the glyph is too small to render (the caller then returns a zero-sized
|
||||
/// `Glyph`).
|
||||
pub fn glyph_layout(
|
||||
mut rect: Rect,
|
||||
is_sbix: bool,
|
||||
synthetic_bold: Option<f64>,
|
||||
opts: &RenderOptions,
|
||||
) -> Option<GlyphLayout> {
|
||||
// If we're rendering a synthetic bold then we will gain 50% of
|
||||
// the line width on every edge, which means we should increase
|
||||
// our width and height by the line width and subtract half from
|
||||
// our origin points.
|
||||
//
|
||||
// We don't add extra size if it's a sbix color font though,
|
||||
// since bitmaps aren't affected by synthetic bold.
|
||||
if !is_sbix && let Some(line_width) = synthetic_bold {
|
||||
rect.width += line_width;
|
||||
rect.height += line_width;
|
||||
rect.x -= line_width / 2.0;
|
||||
rect.y -= line_width / 2.0;
|
||||
}
|
||||
|
||||
// If our rect is smaller than a quarter pixel in either axis
|
||||
// then it has no outlines or they're too small to render.
|
||||
if rect.width < 0.25 || rect.height < 0.25 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let metrics = &opts.grid_metrics;
|
||||
let cell_width = f64::from(metrics.cell_width);
|
||||
let cell_height = f64::from(metrics.cell_height);
|
||||
|
||||
// Next we apply any constraints to get the final size of the glyph.
|
||||
let constraint: &Constraint = &opts.constraint;
|
||||
|
||||
// We need to add the baseline position before passing to the constrain
|
||||
// function since it operates on cell-relative positions, not baseline.
|
||||
let cell_baseline = f64::from(metrics.cell_baseline);
|
||||
|
||||
let glyph_size = constraint.constrain(
|
||||
GlyphSize {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
x: rect.x,
|
||||
y: rect.y + cell_baseline,
|
||||
},
|
||||
metrics,
|
||||
opts.constraint_width,
|
||||
);
|
||||
|
||||
let mut x = glyph_size.x;
|
||||
let mut y = glyph_size.y;
|
||||
let mut width = glyph_size.width;
|
||||
let mut height = glyph_size.height;
|
||||
|
||||
// We center all glyphs within the pixel-rounded and adjusted
|
||||
// cell width if it's larger than the face width, so that they
|
||||
// aren't weirdly off to the left.
|
||||
//
|
||||
// We don't do this if the glyph has a stretch constraint,
|
||||
// since in that case the position was already calculated with the
|
||||
// new cell width in mind.
|
||||
if constraint.size != ConstraintSize::Stretch {
|
||||
// We add half the difference to re-center.
|
||||
let dx = (cell_width - metrics.face_width) / 2.0;
|
||||
x += dx;
|
||||
if dx < 0.0 {
|
||||
// For negative diff (cell narrower than advance), we remove the
|
||||
// integer part and only keep the fractional adjustment needed
|
||||
// for consistent subpixel positioning.
|
||||
x -= dx.trunc();
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a bitmap glyph, it will always render as full pixels,
|
||||
// not fractional pixels, so we need to quantize its position and
|
||||
// size accordingly to align to full pixels so we get good results.
|
||||
if is_sbix {
|
||||
width = cell_width - (cell_width - width - x).round() - x.round();
|
||||
height = cell_height - (cell_height - height - y).round() - y.round();
|
||||
x = x.round();
|
||||
y = y.round();
|
||||
}
|
||||
|
||||
// We make an assumption that font smoothing ("thicken")
|
||||
// adds no more than 1 extra pixel to any edge. We don't
|
||||
// add extra size if it's a sbix color font though, since
|
||||
// bitmaps aren't affected by smoothing.
|
||||
let canvas_padding: u32 = u32::from(opts.thicken && !is_sbix);
|
||||
|
||||
// Our whole-pixel bearings for the final glyph.
|
||||
// The fractional portion will be included in the rasterized position.
|
||||
let px_x = (x.floor() as i32) - canvas_padding as i32;
|
||||
let px_y = (y.floor() as i32) - canvas_padding as i32;
|
||||
|
||||
// We keep track of the fractional part of the pixel bearings, which
|
||||
// we will add as an offset when rasterizing to make sure we get the
|
||||
// correct sub-pixel position.
|
||||
let frac_x = x - x.floor();
|
||||
let frac_y = y - y.floor();
|
||||
|
||||
// Add the fractional pixel to the width and height and take
|
||||
// the ceiling to get a canvas size that will definitely fit
|
||||
// our drawn glyph, including the fractional offset and font smoothing.
|
||||
let px_width = ((width + frac_x).ceil() as u32) + (2 * canvas_padding);
|
||||
let px_height = ((height + frac_y).ceil() as u32) + (2 * canvas_padding);
|
||||
|
||||
Some(GlyphLayout {
|
||||
px_x,
|
||||
px_y,
|
||||
px_width,
|
||||
px_height,
|
||||
canvas_padding,
|
||||
frac_x,
|
||||
frac_y,
|
||||
scale_x: width / rect.width,
|
||||
scale_y: height / rect.height,
|
||||
rect_x: rect.x,
|
||||
rect_y: rect.y,
|
||||
})
|
||||
}
|
||||
|
||||
/// `kCTFontTraitBold` bit of the symbolic traits.
|
||||
pub const TRAIT_BOLD: u32 = 1 << 1;
|
||||
/// `kCTFontTraitItalic` bit of the symbolic traits.
|
||||
pub const TRAIT_ITALIC: u32 = 1;
|
||||
/// `kCTFontTraitMonoSpace` bit of the symbolic traits.
|
||||
pub const TRAIT_MONOSPACE: u32 = 1 << 10;
|
||||
/// `kCTFontTraitColorGlyphs` bit of the symbolic traits.
|
||||
pub const TRAIT_COLOR_GLYPHS: u32 = 1 << 13;
|
||||
|
||||
/// The symbolic traits value for a descriptor (0 if no trait is set, in
|
||||
/// which case the traits attribute is not added; see
|
||||
/// `Descriptor.toCoreTextDescriptor`).
|
||||
pub fn symbolic_traits_value(bold: bool, italic: bool, monospace: bool) -> u32 {
|
||||
(if bold { TRAIT_BOLD } else { 0 })
|
||||
| (if italic { TRAIT_ITALIC } else { 0 })
|
||||
| (if monospace { TRAIT_MONOSPACE } else { 0 })
|
||||
}
|
||||
|
||||
/// Number of bytes per pixel and whether the glyph is rendered in color,
|
||||
/// for the atlas format check in `renderGlyph`.
|
||||
pub fn bitmap_depth(is_color: bool) -> u32 {
|
||||
if is_color { 4 } else { 1 }
|
||||
}
|
||||
|
||||
/// Decode a UTF-32 codepoint into UTF-16 code units
|
||||
/// (`stringGetSurrogatePairForLongCharacter`). Returns the units and how many
|
||||
/// are valid (1 or 2). Codepoints outside the Unicode range yield a single
|
||||
/// replacement unit.
|
||||
pub fn utf16_units(cp: u32) -> ([u16; 2], usize) {
|
||||
match char::from_u32(cp) {
|
||||
Some(c) => {
|
||||
let mut buf = [0u16; 2];
|
||||
let n = c.encode_utf16(&mut buf).len();
|
||||
(buf, n)
|
||||
}
|
||||
None => ([0xFFFD, 0], 1),
|
||||
}
|
||||
}
|
||||
|
||||
/// The score used to sort CoreText discovery results, as a plain integer
|
||||
/// (`Score.int()` in discovery.zig). Fields from least to most significant:
|
||||
/// `glyph_count: u16`, `fuzzy_style: u8`, `bold`, `italic`, `exact_style`,
|
||||
/// `monospace`, `codepoint` (1 bit each). Higher is better.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Score {
|
||||
pub glyph_count: u16,
|
||||
pub fuzzy_style: u8,
|
||||
pub bold: bool,
|
||||
pub italic: bool,
|
||||
pub exact_style: bool,
|
||||
pub monospace: bool,
|
||||
pub codepoint: bool,
|
||||
}
|
||||
|
||||
impl Score {
|
||||
pub fn int(self) -> u32 {
|
||||
u32::from(self.glyph_count)
|
||||
| (u32::from(self.fuzzy_style) << 16)
|
||||
| (u32::from(self.bold) << 24)
|
||||
| (u32::from(self.italic) << 25)
|
||||
| (u32::from(self.exact_style) << 26)
|
||||
| (u32::from(self.monospace) << 27)
|
||||
| (u32::from(self.codepoint) << 28)
|
||||
}
|
||||
}
|
||||
|
||||
/// The style-name strings the discovery score matches against (the first
|
||||
/// is used for the exact match, all are used for the fuzzy match).
|
||||
pub fn desired_styles(style: Option<&str>, bold: bool, italic: bool) -> Vec<&str> {
|
||||
if let Some(s) = style {
|
||||
return vec![s];
|
||||
}
|
||||
|
||||
// If we don't have an explicitly desired style name, we base
|
||||
// it on the bold and italic properties, this isn't ideal since
|
||||
// fonts may use style names other than these, but it helps in
|
||||
// some edge cases.
|
||||
if bold {
|
||||
if italic {
|
||||
return vec!["bold italic", "bold", "italic", "oblique"];
|
||||
}
|
||||
return vec!["bold", "upright"];
|
||||
} else if italic {
|
||||
return vec!["italic", "regular", "oblique"];
|
||||
}
|
||||
vec!["regular", "upright"]
|
||||
}
|
||||
|
||||
/// Compute `(exact_style, fuzzy_style)` for a font's style string against
|
||||
/// the desired styles (see `Score.score`).
|
||||
pub fn style_scores(style_str: &str, desired: &[&str]) -> (bool, u8) {
|
||||
let exact = style_str.eq_ignore_ascii_case(desired[0]);
|
||||
|
||||
// Our "fuzzy match" score is 0 if the desired style isn't present
|
||||
// in the string, otherwise we give higher priority for styles that
|
||||
// have fewer characters not in the desired_styles list.
|
||||
let mut fuzzy: u8 = style_str.len().min(255) as u8;
|
||||
let lower = style_str.to_ascii_lowercase();
|
||||
for s in desired {
|
||||
if lower.contains(&s.to_ascii_lowercase()) {
|
||||
fuzzy = fuzzy.saturating_sub(s.len().min(255) as u8);
|
||||
}
|
||||
}
|
||||
(exact, u8::MAX.saturating_sub(fuzzy))
|
||||
}
|
||||
|
||||
/// Derive `(is_bold, is_italic)` for a variation axis list entry, following
|
||||
/// the thresholds in `Score.score`. `axes` yields `(name, value)`.
|
||||
pub fn bold_italic_from_axes(
|
||||
mut is_bold: bool,
|
||||
mut is_italic: bool,
|
||||
axes: impl IntoIterator<Item = (String, f64)>,
|
||||
) -> (bool, bool) {
|
||||
// If we see the 'ital' value then we ignore 'slnt'.
|
||||
let mut ital_seen = false;
|
||||
for (name, val) in axes {
|
||||
if name == "wght" {
|
||||
// Somewhat subjective threshold, we consider fonts
|
||||
// bold if they have a 'wght' set greater than 600.
|
||||
is_bold = val > 600.0;
|
||||
continue;
|
||||
}
|
||||
if name == "ital" {
|
||||
is_italic = val > 0.5;
|
||||
ital_seen = true;
|
||||
continue;
|
||||
}
|
||||
if !ital_seen && name == "slnt" {
|
||||
// Arbitrary threshold of anything more than a 5
|
||||
// degree clockwise slant is considered italic.
|
||||
is_italic = val <= -5.0;
|
||||
}
|
||||
}
|
||||
(is_bold, is_italic)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
face::{DesiredSize, RenderOptions},
|
||||
metrics::Metrics,
|
||||
opentype::{
|
||||
os2::FSSelection,
|
||||
sfnt::{Fixed, Version16Dot16},
|
||||
},
|
||||
};
|
||||
|
||||
fn hhea(ascender: i16, descender: i16, line_gap: i16) -> Hhea {
|
||||
Hhea {
|
||||
majorVersion: 1,
|
||||
minorVersion: 0,
|
||||
ascender,
|
||||
descender,
|
||||
lineGap: line_gap,
|
||||
advanceWidthMax: 0,
|
||||
minLeftSideBearing: 0,
|
||||
minRightSideBearing: 0,
|
||||
xMaxExtent: 0,
|
||||
caretSlopeRise: 0,
|
||||
caretSlopeRun: 0,
|
||||
caretOffset: 0,
|
||||
reserved0: 0,
|
||||
reserved1: 0,
|
||||
reserved2: 0,
|
||||
reserved3: 0,
|
||||
metricDataFormat: 0,
|
||||
numberOfHMetrics: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn os2(typo: (i16, i16, i16), win: (u16, u16), use_typo: bool) -> OS2 {
|
||||
OS2 {
|
||||
version: 4,
|
||||
xAvgCharWidth: 0,
|
||||
usWeightClass: 400,
|
||||
usWidthClass: 5,
|
||||
fsType: 0,
|
||||
ySubscriptXSize: 0,
|
||||
ySubscriptYSize: 0,
|
||||
ySubscriptXOffset: 0,
|
||||
ySubscriptYOffset: 0,
|
||||
ySuperscriptXSize: 0,
|
||||
ySuperscriptYSize: 0,
|
||||
ySuperscriptXOffset: 0,
|
||||
ySuperscriptYOffset: 0,
|
||||
yStrikeoutSize: 50,
|
||||
yStrikeoutPosition: 300,
|
||||
sFamilyClass: 0,
|
||||
panose: [0; 10],
|
||||
ulUnicodeRange1: 0,
|
||||
ulUnicodeRange2: 0,
|
||||
ulUnicodeRange3: 0,
|
||||
ulUnicodeRange4: 0,
|
||||
achVendID: [0; 4],
|
||||
fsSelection: FSSelection {
|
||||
use_typo_metrics: use_typo,
|
||||
..FSSelection::from_u16(0)
|
||||
},
|
||||
usFirstCharIndex: 0,
|
||||
usLastCharIndex: 0,
|
||||
sTypoAscender: typo.0,
|
||||
sTypoDescender: typo.1,
|
||||
sTypoLineGap: typo.2,
|
||||
usWinAscent: win.0,
|
||||
usWinDescent: win.1,
|
||||
ulCodePageRange1: None,
|
||||
ulCodePageRange2: None,
|
||||
sxHeight: Some(500),
|
||||
sCapHeight: Some(700),
|
||||
usDefaultChar: None,
|
||||
usBreakChar: None,
|
||||
usMaxContext: None,
|
||||
usLowerOpticalPointSize: None,
|
||||
usUpperOpticalPointSize: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn post(pos: i16, thick: i16) -> Post {
|
||||
Post {
|
||||
version: Version16Dot16::from_u32(0x0002_0000),
|
||||
italicAngle: Fixed(0),
|
||||
underlinePosition: pos,
|
||||
underlineThickness: thick,
|
||||
isFixedPitch: 1,
|
||||
minMemType42: 0,
|
||||
maxMemType42: 0,
|
||||
minMemType1: 0,
|
||||
maxMemType1: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbolic_traits() {
|
||||
assert_eq!(symbolic_traits_value(false, false, false), 0);
|
||||
assert_eq!(symbolic_traits_value(true, false, false), 2);
|
||||
assert_eq!(symbolic_traits_value(false, true, false), 1);
|
||||
assert_eq!(symbolic_traits_value(true, true, true), 2 | 1 | 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bold_line_width() {
|
||||
assert_eq!(synthetic_bold_line_width(12.0), 1.0);
|
||||
assert_eq!(synthetic_bold_line_width(14.0), 1.0);
|
||||
assert_eq!(synthetic_bold_line_width(28.0), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_no_hhea_uses_coretext() {
|
||||
let ct = CtMetrics {
|
||||
ascent: 10.0,
|
||||
descent: 3.0,
|
||||
leading: 1.0,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(vertical_metrics(None, None, 0.01, &ct), (10.0, -3.0, 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_hhea_only() {
|
||||
let h = hhea(800, -200, 100);
|
||||
let (a, d, g) = vertical_metrics(Some(&h), None, 0.01, &CtMetrics::default());
|
||||
assert_eq!((a, d, g), (8.0, -2.0, 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_typo_when_flagged() {
|
||||
let h = hhea(800, -200, 100);
|
||||
let o = os2((900, -300, 0), (1000, 400), true);
|
||||
let (a, d, g) = vertical_metrics(Some(&h), Some(&o), 0.01, &CtMetrics::default());
|
||||
assert_eq!((a, d, g), (9.0, -3.0, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_prefers_hhea_then_typo_then_win() {
|
||||
let o = os2((900, -300, 20), (1000, 400), false);
|
||||
// hhea non-zero wins over typo.
|
||||
let h = hhea(800, -200, 100);
|
||||
let (a, ..) = vertical_metrics(Some(&h), Some(&o), 0.01, &CtMetrics::default());
|
||||
assert_eq!(a, 8.0);
|
||||
// hhea zero -> typo.
|
||||
let h = hhea(0, 0, 0);
|
||||
let (a, d, g) = vertical_metrics(Some(&h), Some(&o), 0.01, &CtMetrics::default());
|
||||
assert_eq!((a, d, g), (9.0, -3.0, 0.2));
|
||||
// both zero -> win metrics with flipped descent sign.
|
||||
let o = os2((0, 0, 20), (1000, 400), false);
|
||||
let (a, d, g) = vertical_metrics(Some(&h), Some(&o), 0.01, &CtMetrics::default());
|
||||
assert_eq!((a, d, g), (10.0, -4.0, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_underline_and_strikeout() {
|
||||
let h = hhea(800, -200, 0);
|
||||
let o = os2((0, 0, 0), (0, 0), false);
|
||||
let p = post(-100, 50);
|
||||
let m = face_metrics(
|
||||
10.0,
|
||||
1000.0,
|
||||
Some(&h),
|
||||
Some(&o),
|
||||
Some(&p),
|
||||
&CtMetrics::default(),
|
||||
Measurements {
|
||||
cell_width: 6.0,
|
||||
ascii_height: 9.0,
|
||||
ic_width: Some(12.0),
|
||||
},
|
||||
);
|
||||
assert_eq!(m.underline_position, Some(-1.0));
|
||||
assert_eq!(m.underline_thickness, Some(0.5));
|
||||
assert_eq!(m.strikethrough_position, Some(3.0));
|
||||
assert_eq!(m.strikethrough_thickness, Some(0.5));
|
||||
assert_eq!(m.cap_height, Some(7.0));
|
||||
assert_eq!(m.ex_height, Some(5.0));
|
||||
assert_eq!(m.cell_width, 6.0);
|
||||
assert_eq!(m.ascii_height, Some(9.0));
|
||||
assert_eq!(m.ic_width, Some(12.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_broken_underline_is_none() {
|
||||
let p = post(0, 0);
|
||||
let m = face_metrics(
|
||||
10.0,
|
||||
1000.0,
|
||||
None,
|
||||
None,
|
||||
Some(&p),
|
||||
&CtMetrics {
|
||||
cap_height: 7.0,
|
||||
x_height: 5.0,
|
||||
..Default::default()
|
||||
},
|
||||
Measurements::default(),
|
||||
);
|
||||
assert_eq!(m.underline_position, None);
|
||||
assert_eq!(m.underline_thickness, None);
|
||||
// position set but thickness broken: keep position only
|
||||
let p = post(-100, 0);
|
||||
let m = face_metrics(
|
||||
10.0,
|
||||
1000.0,
|
||||
None,
|
||||
None,
|
||||
Some(&p),
|
||||
&CtMetrics::default(),
|
||||
Measurements::default(),
|
||||
);
|
||||
assert_eq!(m.underline_position, Some(-1.0));
|
||||
assert_eq!(m.underline_thickness, None);
|
||||
assert_eq!(m.strikethrough_position, None);
|
||||
// no OS/2: CoreText cap/ex heights.
|
||||
assert_eq!(m.cap_height, Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ic_width_validity() {
|
||||
assert!(ic_width_is_valid(10.0, 12.0));
|
||||
assert!(ic_width_is_valid(12.0, 12.0));
|
||||
assert!(!ic_width_is_valid(13.0, 12.0));
|
||||
}
|
||||
|
||||
fn opts() -> RenderOptions {
|
||||
let mut m = Metrics::init();
|
||||
m.cell_width = 10;
|
||||
m.cell_height = 20;
|
||||
m.cell_baseline = 4;
|
||||
m.face_width = 10.0;
|
||||
m.face_height = 20.0;
|
||||
let _ = DesiredSize::new(12.0);
|
||||
RenderOptions::new(m)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_tiny_glyph_is_none() {
|
||||
let r = Rect {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 0.2,
|
||||
height: 5.0,
|
||||
};
|
||||
assert!(glyph_layout(r, false, None, &opts()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_basic() {
|
||||
let r = Rect {
|
||||
x: 1.5,
|
||||
y: -2.25,
|
||||
width: 6.0,
|
||||
height: 10.0,
|
||||
};
|
||||
let l = glyph_layout(r, false, None, &opts()).unwrap();
|
||||
assert_eq!(l.px_x, 1);
|
||||
// y is cell-relative: -2.25 + cell_baseline (4) = 1.75
|
||||
assert_eq!(l.px_y, 1);
|
||||
assert!((l.frac_x - 0.5).abs() < 1e-9);
|
||||
assert!((l.frac_y - 0.75).abs() < 1e-9);
|
||||
assert_eq!(l.px_width, 7);
|
||||
assert_eq!(l.px_height, 11);
|
||||
assert_eq!(l.canvas_padding, 0);
|
||||
assert_eq!(l.scale_x, 1.0);
|
||||
assert_eq!(l.offset_y(), 12);
|
||||
assert_eq!(l.offset_x(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_thicken_pads_canvas() {
|
||||
let r = Rect {
|
||||
x: 1.0,
|
||||
y: 0.0,
|
||||
width: 5.0,
|
||||
height: 5.0,
|
||||
};
|
||||
let mut o = opts();
|
||||
o.thicken = true;
|
||||
let l = glyph_layout(r, false, None, &o).unwrap();
|
||||
assert_eq!(l.canvas_padding, 1);
|
||||
assert_eq!(l.px_x, 0);
|
||||
assert_eq!(l.px_width, 7);
|
||||
// sbix glyphs never pad
|
||||
let l = glyph_layout(r, true, None, &o).unwrap();
|
||||
assert_eq!(l.canvas_padding, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_synthetic_bold_grows_rect() {
|
||||
let r = Rect {
|
||||
x: 2.0,
|
||||
y: 2.0,
|
||||
width: 4.0,
|
||||
height: 4.0,
|
||||
};
|
||||
let l = glyph_layout(r, false, Some(2.0), &opts()).unwrap();
|
||||
assert_eq!(l.rect_x, 1.0);
|
||||
assert_eq!(l.rect_y, 1.0);
|
||||
assert_eq!(l.px_width, 6);
|
||||
// sbix ignores synthetic bold
|
||||
let l = glyph_layout(r, true, Some(2.0), &opts()).unwrap();
|
||||
assert_eq!(l.rect_x, 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_centers_in_wider_cell() {
|
||||
let mut o = opts();
|
||||
o.grid_metrics.cell_width = 12;
|
||||
o.grid_metrics.face_width = 10.0;
|
||||
let r = Rect {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 5.0,
|
||||
height: 5.0,
|
||||
};
|
||||
let l = glyph_layout(r, false, None, &o).unwrap();
|
||||
// dx = (12 - 10)/2 = 1
|
||||
assert_eq!(l.px_x, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16() {
|
||||
assert_eq!(utf16_units('A' as u32), (['A' as u16, 0], 1));
|
||||
let (u, n) = utf16_units(0x1F978);
|
||||
assert_eq!(n, 2);
|
||||
assert_eq!(u, [0xD83E, 0xDD78]);
|
||||
assert_eq!(utf16_units(0xD800).1, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn score_ordering() {
|
||||
let a = Score {
|
||||
codepoint: true,
|
||||
..Default::default()
|
||||
};
|
||||
let b = Score {
|
||||
monospace: true,
|
||||
glyph_count: u16::MAX,
|
||||
fuzzy_style: 255,
|
||||
bold: true,
|
||||
italic: true,
|
||||
exact_style: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(a.int() > b.int());
|
||||
let c = Score {
|
||||
glyph_count: 5,
|
||||
..Default::default()
|
||||
};
|
||||
let d = Score {
|
||||
glyph_count: 7,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(d.int() > c.int());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desired_style_lists() {
|
||||
assert_eq!(desired_styles(Some("Light"), true, true), vec!["Light"]);
|
||||
assert_eq!(
|
||||
desired_styles(None, true, true),
|
||||
vec!["bold italic", "bold", "italic", "oblique"]
|
||||
);
|
||||
assert_eq!(desired_styles(None, true, false), vec!["bold", "upright"]);
|
||||
assert_eq!(
|
||||
desired_styles(None, false, true),
|
||||
vec!["italic", "regular", "oblique"]
|
||||
);
|
||||
assert_eq!(
|
||||
desired_styles(None, false, false),
|
||||
vec!["regular", "upright"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_score_fuzzy_and_exact() {
|
||||
let d = desired_styles(None, false, false);
|
||||
let (exact, fuzzy) = style_scores("Regular", &d);
|
||||
assert!(exact);
|
||||
assert_eq!(fuzzy, 255);
|
||||
let (exact, fuzzy) = style_scores("Semi Regular Extra", &d);
|
||||
assert!(!exact);
|
||||
// len 18, "regular" (7) subtracted => 11 => 255 - 11
|
||||
assert_eq!(fuzzy, 255 - 11);
|
||||
let (_, fuzzy) = style_scores("Light", &d);
|
||||
assert_eq!(fuzzy, 255 - 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axes_bold_italic() {
|
||||
let r = bold_italic_from_axes(
|
||||
false,
|
||||
false,
|
||||
[("wght".to_owned(), 700.0), ("slnt".to_owned(), -10.0)],
|
||||
);
|
||||
assert_eq!(r, (true, true));
|
||||
// ital seen => slnt ignored
|
||||
let r = bold_italic_from_axes(
|
||||
true,
|
||||
true,
|
||||
[("ital".to_owned(), 0.0), ("slnt".to_owned(), -10.0)],
|
||||
);
|
||||
assert_eq!(r, (true, false));
|
||||
let r = bold_italic_from_axes(false, false, [("wght".to_owned(), 600.0)]);
|
||||
assert_eq!(r, (false, false));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user