use super::font_assets::{active_faces, FaceBytes};
use super::glyph_source::{Cell, GlyphSource, InkKind, Painted};
const SUBSAMPLES: u32 = 4;
const MAX_POINTS: usize = 1024;
const FLATTEN_TOLERANCE: f32 = 0.2;
const MAX_SUBDIVIDE: u32 = 16;
const ASCENT_SHARE: f32 = 0.82;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
x: f32,
y: f32,
}
impl Point {
const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
fn mid(self, other: Self) -> Self {
Self::new((self.x + other.x) * 0.5, (self.y + other.y) * 0.5)
}
}
struct PointBuffer {
points: [Point; MAX_POINTS],
len: usize,
contours: [(usize, usize); MAX_CONTOURS],
contour_count: usize,
}
const MAX_CONTOURS: usize = 16;
impl PointBuffer {
fn new() -> Self {
Self {
points: [Point::new(0.0, 0.0); MAX_POINTS],
len: 0,
contours: [(0, 0); MAX_CONTOURS],
contour_count: 0,
}
}
fn push(&mut self, point: Point) -> bool {
if self.len == MAX_POINTS {
return false;
}
self.points[self.len] = point;
self.len += 1;
true
}
fn begin_contour(&mut self) -> bool {
if self.contour_count == MAX_CONTOURS {
return false;
}
self.contours[self.contour_count] = (self.len, self.len);
self.contour_count += 1;
true
}
fn end_contour(&mut self) {
if let Some(last) = self.contours.get_mut(self.contour_count.saturating_sub(1)) {
last.1 = self.len;
}
}
fn contours(&self) -> impl Iterator<Item = &[Point]> {
self.contours[..self.contour_count]
.iter()
.filter_map(|&(start, end)| self.points.get(start..end))
}
}
#[derive(Debug, Clone, Copy)]
struct Placement {
units_per_em: f32,
pixel_size: f32,
origin_x: f32,
baseline_y: f32,
x_direction: f32,
}
impl Placement {
fn map(&self, x: f32, y: f32) -> Point {
let scale = self.pixel_size / self.units_per_em;
Point::new(self.origin_x + x * scale * self.x_direction, self.baseline_y - y * scale)
}
}
struct Flattener<'a> {
placement: Placement,
points: &'a mut PointBuffer,
contour_start: Point,
pen: Point,
overflowed: bool,
}
impl<'a> Flattener<'a> {
fn new(placement: Placement, points: &'a mut PointBuffer) -> Self {
Self {
placement,
points,
contour_start: Point::new(0.0, 0.0),
pen: Point::new(0.0, 0.0),
overflowed: false,
}
}
fn finish(&mut self) {
self.points.end_contour();
}
fn push(&mut self, point: Point) {
if !self.points.push(point) {
self.overflowed = true;
}
}
fn quad_to(&mut self, p0: Point, ctrl: Point, p1: Point, depth: u32) {
if depth >= MAX_SUBDIVIDE || is_flat_quad(p0, ctrl, p1) {
self.push(p1);
return;
}
let p01 = p0.mid(ctrl);
let p12 = ctrl.mid(p1);
let mid = p01.mid(p12);
self.quad_to(p0, p01, mid, depth + 1);
self.quad_to(mid, p12, p1, depth + 1);
}
fn curve_to(&mut self, p0: Point, c1: Point, c2: Point, p1: Point, depth: u32) {
if depth >= MAX_SUBDIVIDE || is_flat_cubic(p0, c1, c2, p1) {
self.push(p1);
return;
}
let p01 = p0.mid(c1);
let p12 = c1.mid(c2);
let p23 = c2.mid(p1);
let p012 = p01.mid(p12);
let p123 = p12.mid(p23);
let mid = p012.mid(p123);
self.curve_to(p0, p01, p012, mid, depth + 1);
self.curve_to(mid, p123, p23, p1, depth + 1);
}
}
fn is_flat_quad(p0: Point, ctrl: Point, p1: Point) -> bool {
let dx = p1.x - p0.x;
let dy = p1.y - p0.y;
let mid_x = (p0.x + p1.x) * 0.5;
let mid_y = (p0.y + p1.y) * 0.5;
let dev_x = ctrl.x - mid_x;
let dev_y = ctrl.y - mid_y;
dev_x * dev_x + dev_y * dev_y
<= (FLATTEN_TOLERANCE * FLATTEN_TOLERANCE) * 0.25 * (dx * dx + dy * dy)
|| (dev_x * dev_x + dev_y * dev_y) <= FLATTEN_TOLERANCE * FLATTEN_TOLERANCE
}
fn is_flat_cubic(p0: Point, c1: Point, c2: Point, p1: Point) -> bool {
let d1 = distance_to_line(c1, p0, p1);
let d2 = distance_to_line(c2, p0, p1);
(d1 + d2) * (d1 + d2) <= FLATTEN_TOLERANCE * FLATTEN_TOLERANCE
}
fn distance_to_line(p: Point, a: Point, b: Point) -> f32 {
let dx = b.x - a.x;
let dy = b.y - a.y;
let len_sq = dx * dx + dy * dy;
if len_sq <= f32::EPSILON {
return ((p.x - a.x).powi(2) + (p.y - a.y).powi(2)).sqrt();
}
((p.x - a.x) * dy - (p.y - a.y) * dx).abs() / len_sq.sqrt()
}
struct Accumulator<'a> {
rows: &'a mut [i32],
width: usize,
height: usize,
}
impl Accumulator<'_> {
fn add_edge(&mut self, x0: f32, y0: f32, x1: f32, y1: f32) {
let (x0s, y0s) = (x0 * SUBSAMPLES as f32, y0 * SUBSAMPLES as f32);
let (x1s, y1s) = (x1 * SUBSAMPLES as f32, y1 * SUBSAMPLES as f32);
let dy_total = y1s - y0s;
if dy_total.abs() <= f32::EPSILON {
return;
}
let going_down = dy_total > 0.0;
let (start_y, end_y) = if going_down { (y0s, y1s) } else { (y1s, y0s) };
let delta = if going_down { 1 } else { -1 };
let dx_total = x1s - x0s;
let first_row = (start_y - 0.5).ceil() as i64;
let last_row = (end_y - 0.5).ceil() as i64;
for row in first_row..last_row {
if row < 0 || row as usize >= self.height {
continue;
}
let row_center = row as f32 + 0.5;
let t = (row_center - y0s) / dy_total;
if !(0.0..=1.0).contains(&t) {
continue;
}
let xs = x0s + dx_total * t;
let column = xs.floor() as i64;
if column < 0 || column as usize >= self.width {
continue;
}
let index = row as usize * self.width + column as usize;
self.rows[index] += delta;
}
}
}
fn resolve_coverage(rows: &[i32], sub_width: usize, cell: Cell, out: &mut [u8]) {
let device_w = cell.width as usize;
let device_h = cell.height as usize;
let total = SUBSAMPLES * SUBSAMPLES;
for py in 0..device_h {
for px in 0..device_w {
let mut inside = 0u32;
for sy in 0..SUBSAMPLES as usize {
let row = py * SUBSAMPLES as usize + sy;
let Some(row_slice) = rows.get(row * sub_width..(row + 1) * sub_width) else {
continue;
};
let first = px * SUBSAMPLES as usize;
let mut winding = 0i32;
for (offset, value) in
row_slice.iter().take(first + SUBSAMPLES as usize).enumerate()
{
winding += *value;
if offset >= first && winding != 0 {
inside += 1;
}
}
}
out[py * device_w + px] = (inside * 255 / total) as u8;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OutlinePoint {
pub x: f32,
pub y: f32,
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex", feature = "fonts-cjk"))]
pub fn outline(
ch: char,
cell: Cell,
family: &str,
points: &mut [OutlinePoint],
contours: &mut [(usize, usize)],
) -> Option<usize> {
if cell.is_empty() || points.len() < MAX_POINTS || contours.len() < MAX_CONTOURS {
return None;
}
let face_bytes = super::shaping::face_for_family(family)?;
let face = ttf_parser::Face::parse(face_bytes.bytes, 0).ok()?;
let glyph = face.glyph_index(ch)?;
let units_per_em = face.units_per_em() as f32;
if units_per_em <= 0.0 {
return None;
}
let placement = Placement {
units_per_em,
pixel_size: cell.height as f32,
origin_x: 0.0,
baseline_y: cell.height as f32 * ASCENT_SHARE,
x_direction: 1.0,
};
let mut buffer = PointBuffer::new();
{
let mut flattener = Flattener::new(placement, &mut buffer);
face.outline_glyph(glyph, &mut flattener)?;
flattener.finish();
if flattener.overflowed {
return None;
}
}
let mut contour_count = 0usize;
let mut cursor = 0usize;
for (index, contour) in buffer.contours().enumerate() {
let end = cursor.checked_add(contour.len())?;
let destination = points.get_mut(cursor..end)?;
for (slot, source) in destination.iter_mut().zip(contour.iter()) {
*slot = OutlinePoint { x: source.x, y: source.y };
}
*contours.get_mut(index)? = (cursor, end);
cursor = end;
contour_count = index + 1;
}
if contour_count == 0 {
return None;
}
Some(contour_count)
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex", feature = "fonts-cjk"))]
pub const OUTLINE_MAX_POINTS: usize = MAX_POINTS;
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex", feature = "fonts-cjk"))]
pub const OUTLINE_MAX_CONTOURS: usize = MAX_CONTOURS;
pub struct VectorSource;
impl VectorSource {
pub const INSTANCE: Self = Self;
fn face_for(&self, ch: char) -> Option<FaceBytes> {
active_faces().iter().copied().find(|face| {
ttf_parser::Face::parse(face.bytes, 0)
.ok()
.and_then(|parsed| parsed.glyph_index(ch))
.is_some()
})
}
}
impl GlyphSource for VectorSource {
fn glyph(&self, ch: char) -> Option<super::GlyphBitmap> {
let _ = self.face_for(ch);
None
}
fn name(&self) -> &'static str {
"vector"
}
fn covers(&self, ch: char) -> bool {
self.face_for(ch).is_some()
}
fn paint(&self, ch: char, cell: Cell, out: &mut [u8]) -> Option<Painted> {
if cell.is_empty() || out.len() < cell.area() {
return None;
}
let face_bytes = self.face_for(ch)?;
let face = ttf_parser::Face::parse(face_bytes.bytes, 0).ok()?;
let glyph = face.glyph_index(ch)?;
let units_per_em = face.units_per_em() as f32;
if units_per_em <= 0.0 {
return None;
}
let pixel_size = cell.height as f32;
let placement = Placement {
units_per_em,
pixel_size,
origin_x: 0.0,
baseline_y: cell.height as f32 * ASCENT_SHARE,
x_direction: 1.0,
};
let mut points = PointBuffer::new();
{
let mut flattener = Flattener::new(placement, &mut points);
face.outline_glyph(glyph, &mut flattener)?;
flattener.finish();
if flattener.overflowed {
return None;
}
}
let sub_width = cell.width as usize * SUBSAMPLES as usize;
let sub_height = cell.height as usize * SUBSAMPLES as usize;
let total = sub_width.checked_mul(sub_height)?;
let mut rows = crate::compat::vec![0i32; total];
{
let mut accumulator =
Accumulator { rows: &mut rows, width: sub_width, height: sub_height };
fill_outline(&points, &mut accumulator);
}
out[..cell.area()].fill(0);
resolve_coverage(&rows, sub_width, cell, out);
Some(Painted { source: face_bytes.name, source_cell: None, ink: InkKind::Coverage })
}
}
fn fill_outline(buffer: &PointBuffer, accumulator: &mut Accumulator) {
for contour in buffer.contours() {
if contour.len() < 3 {
continue;
}
for index in 0..contour.len() {
let a = contour[index];
let b = contour[(index + 1) % contour.len()];
accumulator.add_edge(a.x, a.y, b.x, b.y);
}
}
}
impl ttf_parser::OutlineBuilder for Flattener<'_> {
fn move_to(&mut self, x: f32, y: f32) {
let point = self.placement.map(x, y);
self.points.end_contour();
if !self.points.begin_contour() {
self.overflowed = true;
}
self.contour_start = point;
self.pen = point;
self.push(point);
}
fn line_to(&mut self, x: f32, y: f32) {
let point = self.placement.map(x, y);
self.pen = point;
self.push(point);
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
let ctrl = self.placement.map(x1, y1);
let end = self.placement.map(x, y);
let start = self.pen;
Flattener::quad_to(self, start, ctrl, end, 0);
self.pen = end;
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
let c1 = self.placement.map(x1, y1);
let c2 = self.placement.map(x2, y2);
let end = self.placement.map(x, y);
let start = self.pen;
Flattener::curve_to(self, start, c1, c2, end, 0);
self.pen = end;
}
fn close(&mut self) {
self.points.end_contour();
self.pen = self.contour_start;
}
}
#[cfg(all(
test,
any(feature = "fonts-vector-latin", feature = "fonts-complex", feature = "fonts-cjk")
))]
mod tests {
#[allow(unused_imports)]
use super::*;
#[allow(unused_imports)]
use crate::render::text::{paint_active, Cell, GlyphSource, InkKind, VectorSource};
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-cjk"))]
fn latin_family() -> &'static str {
#[cfg(feature = "fonts-vector-latin")]
{
"Open Sans"
}
#[cfg(all(not(feature = "fonts-vector-latin"), feature = "fonts-cjk"))]
{
"Noto Sans SC"
}
}
#[cfg(feature = "fonts-vector-latin")]
#[test]
fn a_vector_face_produces_antialiased_coverage() {
let cell = Cell::new(32, 32);
let mut out = vec![0u8; cell.area()];
let painted = paint_active('o', cell, &mut out).expect("the Latin face covers 'o'");
assert_eq!(painted.ink, InkKind::Coverage, "a vector face reports coverage, not 1-bit ink");
assert!(painted.is_coverage(), "so the rasteriser may blend it as alpha");
assert!(!painted.is_color(), "and it is not a colour glyph");
assert_eq!(painted.source_cell, None, "there is no source pixel grid to compress by");
assert!(out.contains(&0), "the glyph must not fill its whole cell");
assert!(out.contains(&255), "its interior must be solid");
assert!(
out.iter().any(|v| *v > 0 && *v < 255),
"a curved outline must produce partial coverage, or there is no antialiasing"
);
}
#[cfg(feature = "fonts-vector-latin")]
#[test]
fn the_counter_of_a_glyph_is_a_hole() {
let (w, h) = (40u32, 40u32);
let cell = Cell::new(w, h);
let mut out = vec![0u8; cell.area()];
paint_active('o', cell, &mut out).expect("the Latin face covers 'o'");
let painted = |x: u32, y: u32| out[(y * w + x) as usize] > 0;
let mut top = None;
let mut bottom = 0u32;
for y in 0..h {
if (0..w).any(|x| painted(x, y)) {
top.get_or_insert(y);
bottom = y;
}
}
let (top, bottom) = (top.expect("something was painted"), bottom);
let mid_y = (top + bottom) / 2;
let columns: Vec<u32> = (0..w).filter(|x| painted(*x, mid_y)).collect();
let first = *columns.first().expect("the left wall of the bowl");
let last = *columns.last().expect("the right wall of the bowl");
assert!(last > first + 1, "the row must cross two walls, not one blob");
let hole = ((first + 1)..last).filter(|x| !painted(*x, mid_y)).count();
assert!(
hole > 0,
"'o' must have an unpainted counter on its middle row: walls at {first} and {last}, \
nothing empty between them"
);
assert!(
out[(mid_y * w + (first + last) / 2) as usize] == 0,
"and the middle of that gap must be exactly unpainted, not merely dim"
);
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-cjk"))]
#[test]
fn a_character_outside_the_face_is_refused() {
let source = VectorSource::INSTANCE;
assert!(source.covers('A'), "the Latin and CJK subsets both carry ASCII letters");
let cell = Cell::new(16, 16);
let mut out = vec![0u8; cell.area()];
assert!(
source.paint('\u{e000}', cell, &mut out).is_none(),
"a private-use codepoint is in no shipped subset, so the face must say so"
);
}
#[cfg(feature = "fonts-vector-latin")]
#[test]
fn a_short_buffer_is_refused() {
let cell = Cell::new(32, 32);
let mut out = vec![0u8; cell.area() - 1];
assert!(
VectorSource::INSTANCE.paint('A', cell, &mut out).is_none(),
"one byte short is short"
);
}
#[cfg(feature = "fonts-vector-latin")]
#[test]
fn an_empty_cell_is_refused() {
let mut out = vec![0u8; 0];
assert!(VectorSource::INSTANCE.paint('A', Cell::new(0, 0), &mut out).is_none());
assert!(VectorSource::INSTANCE.paint('A', Cell::new(8, 0), &mut out).is_none());
}
#[cfg(feature = "fonts-vector-latin")]
#[test]
fn the_outline_is_rasterised_at_the_requested_size() {
let mut small = vec![0u8; 8 * 8];
let mut large = vec![0u8; 64 * 64];
paint_active('l', Cell::new(8, 8), &mut small).expect("8x8");
paint_active('l', Cell::new(64, 64), &mut large).expect("64x64");
let coverage = |px: &[u8]| px.iter().map(|v| u32::from(*v)).sum::<u32>() as f32 / 255.0;
let small_ink = coverage(&small);
let large_ink = coverage(&large);
let ratio = large_ink / small_ink.max(1.0);
assert!(
(30.0..120.0).contains(&ratio),
"ink must scale with the cell's area: 8x8 -> {small_ink}, 64x64 -> {large_ink}, \
ratio {ratio}"
);
}
#[cfg(feature = "fonts-cjk")]
#[test]
fn the_scalable_cjk_face_draws_ideographs_and_kana() {
let cell = Cell::new(32, 32);
let mut out = vec![0u8; cell.area()];
for (ch, label) in [
('\u{4E2D}', "U+4E2D a Han ideograph"),
('\u{3042}', "U+3042 hiragana A"),
('\u{30AB}', "U+30AB katakana KA"),
('\u{FF01}', "U+FF01 fullwidth exclamation"),
] {
let painted = VectorSource::INSTANCE
.paint(ch, cell, &mut out)
.unwrap_or_else(|| panic!("{label} must be covered by the CJK vector face"));
assert_eq!(painted.ink, InkKind::Coverage, "{label} is outline ink");
assert_eq!(painted.source, "Noto Sans SC", "{label} came from the CJK face");
let lit = out.iter().filter(|v| **v > 0).count();
let interior = out.iter().filter(|v| **v > 0 && **v < 255).count();
assert!(lit > 0, "{label} must draw something");
let area = cell.area();
assert!(
lit < area,
"{label} must not fill the whole {area}-pixel cell (that is a solid block, not a glyph)"
);
assert!(
lit > area / 40,
"{label} must cover more than a speck: {lit} of {area} pixels"
);
assert!(
interior > 0,
"{label} must have antialiased edge pixels, got {interior} interior values"
);
}
}
#[cfg(feature = "fonts-cjk")]
#[test]
fn an_ideograph_outside_the_cjk_subset_is_refused() {
let cell = Cell::new(16, 16);
let mut out = vec![0u8; cell.area()];
assert!(
VectorSource::INSTANCE.paint('\u{9000}', cell, &mut out).is_none(),
"U+9000 is in the Han block but past the subset's cap, so it must be a miss"
);
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-cjk"))]
#[test]
fn the_outline_geometry_has_closed_contours_with_subpixel_points() {
let mut points = [OutlinePoint { x: 0.0, y: 0.0 }; OUTLINE_MAX_POINTS];
let mut contours = [(0usize, 0usize); OUTLINE_MAX_CONTOURS];
let cell = Cell::new(32, 32);
let count = outline('A', cell, latin_family(), &mut points, &mut contours)
.expect("the Latin subset covers 'A'");
assert_eq!(count, 2, "'A' has an outer contour and a counter");
let mut cursor = 0usize;
for (index, (start, end)) in contours.iter().take(count).enumerate() {
assert_eq!(*start, cursor, "contour {index} must start where the previous one ended");
assert!(end > start, "contour {index} must have at least one vertex");
cursor = *end;
}
assert!(cursor <= points.len(), "the contours must fit the slice");
let has_fraction =
points[..cursor].iter().any(|p| p.x.fract() != 0.0 || p.y.fract() != 0.0);
assert!(has_fraction, "outline vertices must keep their sub-pixel precision");
for point in &points[..cursor] {
assert!(
(-1.0..=33.0).contains(&point.x) && (-1.0..=33.0).contains(&point.y),
"a 32x32 cell's glyph must land near the cell, got ({}, {})",
point.x,
point.y
);
}
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-cjk"))]
#[test]
fn a_glyph_with_a_counter_reports_two_contours() {
let mut points = [OutlinePoint { x: 0.0, y: 0.0 }; OUTLINE_MAX_POINTS];
let mut contours = [(0usize, 0usize); OUTLINE_MAX_CONTOURS];
let count = outline('o', Cell::new(32, 32), latin_family(), &mut points, &mut contours)
.expect("the Latin subset covers 'o'");
assert_eq!(count, 2, "'o' is a ring, so it needs an outer contour and an inner one");
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-cjk"))]
#[test]
fn an_uncovered_character_has_no_outline() {
let mut points = [OutlinePoint { x: 0.0, y: 0.0 }; OUTLINE_MAX_POINTS];
let mut contours = [(0usize, 0usize); OUTLINE_MAX_CONTOURS];
assert!(outline('\u{e000}', Cell::new(32, 32), latin_family(), &mut points, &mut contours)
.is_none());
assert!(outline('A', Cell::new(0, 0), latin_family(), &mut points, &mut contours).is_none());
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-cjk"))]
#[test]
fn an_undersized_scratch_is_refused() {
let mut contours = [(0usize, 0usize); OUTLINE_MAX_CONTOURS];
let mut small_points = [OutlinePoint { x: 0.0, y: 0.0 }; OUTLINE_MAX_POINTS - 1];
assert!(outline('A', Cell::new(32, 32), latin_family(), &mut small_points, &mut contours)
.is_none());
let mut points = [OutlinePoint { x: 0.0, y: 0.0 }; OUTLINE_MAX_POINTS];
let mut small_contours = [(0usize, 0usize); OUTLINE_MAX_CONTOURS - 1];
assert!(outline('A', Cell::new(32, 32), latin_family(), &mut points, &mut small_contours)
.is_none());
}
}