#[cfg(not(feature = "std"))]
use alloc::boxed::Box;
#[cfg(feature = "no_std")]
use core_maths::CoreFloat;
use core::fmt;
use swash::scale::{image::Content, ScaleContext};
use swash::scale::{Render, Source, StrikeWith};
use swash::zeno::{Format, Vector};
use crate::{CacheKey, CacheKeyFlags, Color, FontSystem, HashMap};
pub use swash::scale::image::{Content as SwashContent, Image as SwashImage};
pub use swash::zeno::{Angle, Command, Placement, Transform};
fn swash_image(
font_system: &mut FontSystem,
context: &mut ScaleContext,
cache_key: CacheKey,
) -> Option<SwashImage> {
let Some(font) = font_system.get_font(cache_key.font_id, cache_key.font_weight) else {
log::warn!("did not find font {:?}", cache_key.font_id);
return None;
};
let variable_width = font
.as_swash()
.variations()
.find_by_tag(swash::Tag::from_be_bytes(*b"wght"));
let mut scaler = context
.builder(font.as_swash())
.size(f32::from_bits(cache_key.font_size_bits))
.hint(!cache_key.flags.contains(CacheKeyFlags::DISABLE_HINTING));
if let Some(variation) = variable_width {
scaler = scaler.normalized_coords(font.as_swash().variations().normalized_coords([(
swash::Tag::from_be_bytes(*b"wght"),
f32::from(cache_key.font_weight.0).clamp(variation.min_value(), variation.max_value()),
)]));
}
let mut scaler = scaler.build();
let offset = if cache_key.flags.contains(CacheKeyFlags::PIXEL_FONT) {
Vector::new(
cache_key.x_bin.as_float().round(),
cache_key.y_bin.as_float().round(),
)
} else {
Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float())
};
Render::new(&[
Source::ColorOutline(0),
Source::ColorBitmap(StrikeWith::BestFit),
Source::Outline,
])
.format(Format::Alpha)
.offset(offset)
.transform(if cache_key.flags.contains(CacheKeyFlags::FAKE_ITALIC) {
Some(Transform::skew(
Angle::from_degrees(14.0),
Angle::from_degrees(0.0),
))
} else {
None
})
.render(&mut scaler, cache_key.glyph_id)
}
fn swash_outline_commands(
font_system: &mut FontSystem,
context: &mut ScaleContext,
cache_key: CacheKey,
) -> Option<Box<[swash::zeno::Command]>> {
use swash::zeno::PathData as _;
let Some(font) = font_system.get_font(cache_key.font_id, cache_key.font_weight) else {
log::warn!("did not find font {:?}", cache_key.font_id);
return None;
};
let variable_width = font
.as_swash()
.variations()
.find_by_tag(swash::Tag::from_be_bytes(*b"wght"));
let mut scaler = context
.builder(font.as_swash())
.size(f32::from_bits(cache_key.font_size_bits))
.hint(!cache_key.flags.contains(CacheKeyFlags::DISABLE_HINTING));
if let Some(variation) = variable_width {
scaler = scaler.normalized_coords(font.as_swash().variations().normalized_coords([(
swash::Tag::from_be_bytes(*b"wght"),
f32::from(cache_key.font_weight.0).clamp(variation.min_value(), variation.max_value()),
)]));
}
let mut scaler = scaler.build();
let mut outline = scaler
.scale_outline(cache_key.glyph_id)
.or_else(|| scaler.scale_color_outline(cache_key.glyph_id))?;
if cache_key.flags.contains(CacheKeyFlags::FAKE_ITALIC) {
outline.transform(&Transform::skew(
Angle::from_degrees(14.0),
Angle::from_degrees(0.0),
));
}
let path = outline.path();
Some(path.commands().collect())
}
pub struct SwashCache {
context: ScaleContext,
pub image_cache: HashMap<CacheKey, Option<SwashImage>>,
pub outline_command_cache: HashMap<CacheKey, Option<Box<[swash::zeno::Command]>>>,
}
impl fmt::Debug for SwashCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad("SwashCache { .. }")
}
}
impl SwashCache {
pub fn new() -> Self {
Self {
context: ScaleContext::new(),
image_cache: HashMap::default(),
outline_command_cache: HashMap::default(),
}
}
pub fn get_image_uncached(
&mut self,
font_system: &mut FontSystem,
cache_key: CacheKey,
) -> Option<SwashImage> {
swash_image(font_system, &mut self.context, cache_key)
}
pub fn get_image(
&mut self,
font_system: &mut FontSystem,
cache_key: CacheKey,
) -> &Option<SwashImage> {
self.image_cache
.entry(cache_key)
.or_insert_with(|| swash_image(font_system, &mut self.context, cache_key))
}
pub fn get_outline_commands(
&mut self,
font_system: &mut FontSystem,
cache_key: CacheKey,
) -> Option<&[swash::zeno::Command]> {
self.outline_command_cache
.entry(cache_key)
.or_insert_with(|| swash_outline_commands(font_system, &mut self.context, cache_key))
.as_deref()
}
pub fn get_outline_commands_uncached(
&mut self,
font_system: &mut FontSystem,
cache_key: CacheKey,
) -> Option<Box<[swash::zeno::Command]>> {
swash_outline_commands(font_system, &mut self.context, cache_key)
}
pub fn with_pixels<F: FnMut(i32, i32, Color)>(
&mut self,
font_system: &mut FontSystem,
cache_key: CacheKey,
base: Color,
mut f: F,
) {
if let Some(image) = self.get_image(font_system, cache_key) {
let x = image.placement.left;
let y = -image.placement.top;
match image.content {
Content::Mask => {
let mut i = 0;
for off_y in 0..image.placement.height as i32 {
for off_x in 0..image.placement.width as i32 {
f(
x + off_x,
y + off_y,
Color((u32::from(image.data[i]) << 24) | base.0 & 0xFF_FF_FF),
);
i += 1;
}
}
}
Content::Color => {
let mut i = 0;
for off_y in 0..image.placement.height as i32 {
for off_x in 0..image.placement.width as i32 {
f(
x + off_x,
y + off_y,
Color::rgba(
image.data[i],
image.data[i + 1],
image.data[i + 2],
image.data[i + 3],
),
);
i += 4;
}
}
}
Content::SubpixelMask => {
log::warn!("TODO: SubpixelMask");
}
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
use swash::{FontRef, Setting, Tag};
#[test]
fn no_coord_leakage_across_fonts() {
let [Ok(sfns), Ok(sfns_italic)] = [
"/System/Library/Fonts/SFNS.ttf",
"/System/Library/Fonts/SFNSItalic.ttf",
]
.map(std::fs::read) else {
return;
};
let regular = FontRef::from_index(&sfns, 0).unwrap();
let italic = FontRef::from_index(&sfns_italic, 0).unwrap();
let wght = Tag::from_be_bytes(*b"wght");
let render = |ctx: &mut ScaleContext, font: FontRef, weight: f32, use_normalized| {
let mut b = ctx.builder(font).size(16.0).hint(true);
if use_normalized {
b = b.normalized_coords(font.variations().normalized_coords([(wght, weight)]));
} else {
b = b.variations(std::iter::once(Setting {
tag: wght,
value: weight,
}));
}
Render::new(&[Source::Outline])
.format(Format::Alpha)
.render(&mut b.build(), 36)
};
let mut ctx = ScaleContext::new();
let reference = render(&mut ctx, regular, 400.0, false).map(|i| i.data);
let mut ctx = ScaleContext::new();
render(&mut ctx, italic, 700.0, false);
let not_normalized = render(&mut ctx, regular, 400.0, false).map(|i| i.data);
let mut ctx = ScaleContext::new();
render(&mut ctx, italic, 700.0, true);
let normalized = render(&mut ctx, regular, 400.0, true).map(|i| i.data);
assert_ne!(not_normalized, reference, "variations leak across fonts");
assert_eq!(
normalized, reference,
"normalized_coords match clean render"
);
}
}