feat(math): bound layout cache and glyph rasterization
This commit is contained in:
1 parent
7f648f9567
commit
be2e9d76b7
3 files changed
+437
No files matched your search
@@ -0,0 +1,236 @@
|
||||
//! 固定字节预算的公式布局 LRU。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::mem::size_of;
|
||||
|
||||
use ahash::RandomState;
|
||||
|
||||
use super::layout::{MathGlyphOp, MathLayout, MathRuleOp};
|
||||
use super::{MathError, MathErrorKind};
|
||||
|
||||
pub(crate) const LAYOUT_CACHE_BUDGET: usize = 4 * 1024 * 1024;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub(crate) struct FormulaCacheKey {
|
||||
/// 文档生命周期内稳定的公式编号;缓存随 DocView 一起销毁,因而无需复制公式源码。
|
||||
pub(crate) formula_id: u64,
|
||||
pub(crate) pixel_size_bits: u32,
|
||||
pub(crate) pixels_per_point_bits: u32,
|
||||
pub(crate) display: bool,
|
||||
}
|
||||
|
||||
impl FormulaCacheKey {
|
||||
pub(crate) fn new(
|
||||
formula_id: u64,
|
||||
pixel_size: f32,
|
||||
pixels_per_point: f32,
|
||||
display: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
formula_id,
|
||||
pixel_size_bits: pixel_size.to_bits(),
|
||||
pixels_per_point_bits: pixels_per_point.to_bits(),
|
||||
display,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CacheEntry {
|
||||
key: FormulaCacheKey,
|
||||
layout: MathLayout,
|
||||
charge: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CacheNode {
|
||||
entry: Option<CacheEntry>,
|
||||
previous: Option<usize>,
|
||||
next: Option<usize>,
|
||||
next_free: Option<usize>,
|
||||
}
|
||||
|
||||
pub(crate) struct MathLayoutCache {
|
||||
budget: usize,
|
||||
used: usize,
|
||||
entries: HashMap<FormulaCacheKey, usize, RandomState>,
|
||||
nodes: Vec<CacheNode>,
|
||||
most_recent: Option<usize>,
|
||||
least_recent: Option<usize>,
|
||||
free: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for MathLayoutCache {
|
||||
fn default() -> Self {
|
||||
Self::with_budget(LAYOUT_CACHE_BUDGET)
|
||||
}
|
||||
}
|
||||
|
||||
impl MathLayoutCache {
|
||||
fn with_budget(budget: usize) -> Self {
|
||||
Self {
|
||||
budget,
|
||||
used: 0,
|
||||
entries: HashMap::with_hasher(RandomState::default()),
|
||||
nodes: Vec::new(),
|
||||
most_recent: None,
|
||||
least_recent: None,
|
||||
free: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get(&mut self, key: FormulaCacheKey) -> Option<&MathLayout> {
|
||||
let index = *self.entries.get(&key)?;
|
||||
self.touch(index);
|
||||
self.nodes[index].entry.as_ref().map(|entry| &entry.layout)
|
||||
}
|
||||
|
||||
pub(crate) fn get_or_insert_with(
|
||||
&mut self,
|
||||
key: FormulaCacheKey,
|
||||
build: impl FnOnce() -> Result<MathLayout, MathError>,
|
||||
) -> Result<&MathLayout, MathError> {
|
||||
if let Some(index) = self.entries.get(&key).copied() {
|
||||
self.touch(index);
|
||||
return self.nodes[index]
|
||||
.entry
|
||||
.as_ref()
|
||||
.map(|entry| &entry.layout)
|
||||
.ok_or_else(|| MathError::new(MathErrorKind::Parse, 0));
|
||||
}
|
||||
|
||||
let layout = build()?;
|
||||
let charge = layout_charge(&layout);
|
||||
if charge > self.budget {
|
||||
return Err(MathError::new(MathErrorKind::OpLimit, 0));
|
||||
}
|
||||
while self.used.saturating_add(charge) > self.budget {
|
||||
self.evict_least_recent()?;
|
||||
}
|
||||
|
||||
let index = if let Some(index) = self.free {
|
||||
self.free = self.nodes[index].next_free.take();
|
||||
index
|
||||
} else {
|
||||
self.nodes.push(CacheNode::default());
|
||||
self.nodes.len() - 1
|
||||
};
|
||||
self.nodes[index].entry = Some(CacheEntry { key, layout, charge });
|
||||
self.used += charge;
|
||||
self.entries.insert(key, index);
|
||||
self.attach_most_recent(index);
|
||||
Ok(&self.nodes[index].entry.as_ref().expect("inserted cache node").layout)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn used_bytes(&self) -> usize {
|
||||
self.used
|
||||
}
|
||||
|
||||
fn touch(&mut self, index: usize) {
|
||||
if self.most_recent == Some(index) {
|
||||
return;
|
||||
}
|
||||
self.detach(index);
|
||||
self.attach_most_recent(index);
|
||||
}
|
||||
|
||||
fn detach(&mut self, index: usize) {
|
||||
let previous = self.nodes[index].previous.take();
|
||||
let next = self.nodes[index].next.take();
|
||||
if let Some(previous) = previous {
|
||||
self.nodes[previous].next = next;
|
||||
} else if self.most_recent == Some(index) {
|
||||
self.most_recent = next;
|
||||
}
|
||||
if let Some(next) = next {
|
||||
self.nodes[next].previous = previous;
|
||||
} else if self.least_recent == Some(index) {
|
||||
self.least_recent = previous;
|
||||
}
|
||||
}
|
||||
|
||||
fn attach_most_recent(&mut self, index: usize) {
|
||||
let old_head = self.most_recent;
|
||||
self.nodes[index].previous = None;
|
||||
self.nodes[index].next = old_head;
|
||||
if let Some(old_head) = old_head {
|
||||
self.nodes[old_head].previous = Some(index);
|
||||
} else {
|
||||
self.least_recent = Some(index);
|
||||
}
|
||||
self.most_recent = Some(index);
|
||||
}
|
||||
|
||||
fn evict_least_recent(&mut self) -> Result<(), MathError> {
|
||||
let index = self.least_recent.ok_or_else(|| MathError::new(MathErrorKind::OpLimit, 0))?;
|
||||
self.detach(index);
|
||||
let entry = self.nodes[index]
|
||||
.entry
|
||||
.take()
|
||||
.ok_or_else(|| MathError::new(MathErrorKind::Parse, 0))?;
|
||||
self.entries.remove(&entry.key);
|
||||
self.used = self.used.saturating_sub(entry.charge);
|
||||
self.nodes[index].next_free = self.free;
|
||||
self.free = Some(index);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_charge(layout: &MathLayout) -> usize {
|
||||
// 将容器、索引节点与哈希桶的近似固定开销也计入预算,避免大量空公式绕过限制。
|
||||
const INDEX_OVERHEAD: usize = size_of::<CacheNode>()
|
||||
+ size_of::<(FormulaCacheKey, usize)>() * 2
|
||||
+ size_of::<CacheEntry>();
|
||||
INDEX_OVERHEAD
|
||||
.saturating_add(layout.glyphs.capacity() * size_of::<MathGlyphOp>())
|
||||
.saturating_add(layout.rules.capacity() * size_of::<MathRuleOp>())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::math::layout::{MathGlyphOp, MathMetrics};
|
||||
|
||||
fn key(id: u64) -> FormulaCacheKey {
|
||||
FormulaCacheKey::new(id, 18.0, 1.0, false)
|
||||
}
|
||||
|
||||
fn layout(glyphs: usize) -> MathLayout {
|
||||
MathLayout {
|
||||
metrics: MathMetrics::default(),
|
||||
glyphs: vec![MathGlyphOp::default(); glyphs],
|
||||
rules: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lru_stays_within_its_byte_budget_and_reuses_nodes() {
|
||||
let one_charge = layout_charge(&layout(8));
|
||||
let mut cache = MathLayoutCache::with_budget(one_charge * 2);
|
||||
cache.get_or_insert_with(key(1), || Ok(layout(8))).unwrap();
|
||||
cache.get_or_insert_with(key(2), || Ok(layout(8))).unwrap();
|
||||
assert!(cache.get(key(1)).is_some());
|
||||
cache.get_or_insert_with(key(3), || Ok(layout(8))).unwrap();
|
||||
|
||||
assert_eq!(cache.len(), 2);
|
||||
assert!(cache.get(key(1)).is_some());
|
||||
assert!(cache.get(key(2)).is_none());
|
||||
assert!(cache.get(key(3)).is_some());
|
||||
assert!(cache.used_bytes() <= one_charge * 2);
|
||||
assert_eq!(cache.nodes.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_hit_does_not_rebuild_layout() {
|
||||
let mut cache = MathLayoutCache::default();
|
||||
cache.get_or_insert_with(key(7), || Ok(layout(1))).unwrap();
|
||||
let hit =
|
||||
cache.get_or_insert_with(key(7), || panic!("cache hit must not call builder")).unwrap();
|
||||
assert_eq!(hit.glyphs.len(), 1);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,12 @@
|
||||
//!
|
||||
//! 该模块不依赖窗口、OpenGL 或主题类型,保证同一份布局可被不同渲染后端复用。
|
||||
|
||||
pub(crate) mod cache;
|
||||
pub(crate) mod font;
|
||||
pub(crate) mod ir;
|
||||
pub(crate) mod layout;
|
||||
pub(crate) mod parser;
|
||||
pub(crate) mod rasterizer;
|
||||
pub(crate) mod spacing;
|
||||
pub(crate) mod validate;
|
||||
|
||||
@@ -47,6 +49,7 @@ pub(crate) enum MathErrorKind {
|
||||
OpLimit,
|
||||
Parse,
|
||||
MissingGlyph,
|
||||
GlyphTooLarge,
|
||||
Font,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
//! 固定数学字体 glyph ID 的紧边界 CPU 栅格化。
|
||||
|
||||
use ab_glyph_rasterizer::{Point, Rasterizer, point};
|
||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||
|
||||
use super::font::MathFont;
|
||||
use super::{MathError, MathErrorKind};
|
||||
|
||||
const GLYPH_PADDING: f32 = 1.0;
|
||||
const MAX_GLYPH_DIMENSION: u32 = 512;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) struct RasterizedMathGlyph {
|
||||
pub(crate) width: u16,
|
||||
pub(crate) height: u16,
|
||||
/// glyph 原点到位图左边界的像素偏移。
|
||||
pub(crate) left: i16,
|
||||
/// glyph 基线到位图顶边界的像素偏移,向上为正。
|
||||
pub(crate) top: i16,
|
||||
pub(crate) rgba: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct MathGlyphRasterizer {
|
||||
font: MathFont,
|
||||
}
|
||||
|
||||
impl MathGlyphRasterizer {
|
||||
pub(crate) fn new() -> Result<Self, MathError> {
|
||||
let font = MathFont::load().map_err(|_| MathError::new(MathErrorKind::Font, 0))?;
|
||||
Ok(Self { font })
|
||||
}
|
||||
|
||||
pub(crate) fn rasterize(
|
||||
&self,
|
||||
glyph_id: u16,
|
||||
pixel_size: f32,
|
||||
) -> Result<RasterizedMathGlyph, MathError> {
|
||||
if !pixel_size.is_finite() || pixel_size <= 0.0 {
|
||||
return Err(MathError::new(MathErrorKind::Font, 0));
|
||||
}
|
||||
let glyph = GlyphId(glyph_id);
|
||||
let metrics = self
|
||||
.font
|
||||
.glyph_metrics(glyph, pixel_size)
|
||||
.map_err(|_| MathError::new(MathErrorKind::MissingGlyph, 0))?;
|
||||
let left = (metrics.x_min - GLYPH_PADDING).floor();
|
||||
let right = (metrics.x_max + GLYPH_PADDING).ceil();
|
||||
let top = (metrics.height + GLYPH_PADDING).ceil();
|
||||
let bottom = (-metrics.depth - GLYPH_PADDING).floor();
|
||||
let width = (right - left).max(0.0) as u32;
|
||||
let height = (top - bottom).max(0.0) as u32;
|
||||
if width == 0 || height == 0 {
|
||||
return Ok(RasterizedMathGlyph::default());
|
||||
}
|
||||
if width > MAX_GLYPH_DIMENSION || height > MAX_GLYPH_DIMENSION {
|
||||
return Err(MathError::new(MathErrorKind::GlyphTooLarge, 0));
|
||||
}
|
||||
let left_i16 = checked_i16(left)?;
|
||||
let top_i16 = checked_i16(top)?;
|
||||
let mut outline =
|
||||
RasterOutline::new(width as usize, height as usize, pixel_size, left, top, self.font)?;
|
||||
let bounds = self
|
||||
.font
|
||||
.outline(glyph, &mut outline)
|
||||
.map_err(|_| MathError::new(MathErrorKind::Font, 0))?;
|
||||
if bounds.is_none() {
|
||||
return Err(MathError::new(MathErrorKind::MissingGlyph, 0));
|
||||
}
|
||||
|
||||
let pixel_count = (width as usize)
|
||||
.checked_mul(height as usize)
|
||||
.ok_or_else(|| MathError::new(MathErrorKind::GlyphTooLarge, 0))?;
|
||||
let mut rgba = vec![0u8; pixel_count * 4];
|
||||
outline.rasterizer.for_each_pixel(|index, alpha| {
|
||||
let offset = index * 4;
|
||||
rgba[offset] = 255;
|
||||
rgba[offset + 1] = 255;
|
||||
rgba[offset + 2] = 255;
|
||||
rgba[offset + 3] = (alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
});
|
||||
Ok(RasterizedMathGlyph {
|
||||
width: width as u16,
|
||||
height: height as u16,
|
||||
left: left_i16,
|
||||
top: top_i16,
|
||||
rgba,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn checked_i16(value: f32) -> Result<i16, MathError> {
|
||||
if value < i16::MIN as f32 || value > i16::MAX as f32 {
|
||||
Err(MathError::new(MathErrorKind::GlyphTooLarge, 0))
|
||||
} else {
|
||||
Ok(value as i16)
|
||||
}
|
||||
}
|
||||
|
||||
struct RasterOutline {
|
||||
rasterizer: Rasterizer,
|
||||
scale: f32,
|
||||
left: f32,
|
||||
top: f32,
|
||||
current: Point,
|
||||
contour_start: Point,
|
||||
}
|
||||
|
||||
impl RasterOutline {
|
||||
fn new(
|
||||
width: usize,
|
||||
height: usize,
|
||||
pixel_size: f32,
|
||||
left: f32,
|
||||
top: f32,
|
||||
font: MathFont,
|
||||
) -> Result<Self, MathError> {
|
||||
let units_per_em =
|
||||
font.units_per_em().map_err(|_| MathError::new(MathErrorKind::Font, 0))?;
|
||||
Ok(Self {
|
||||
rasterizer: Rasterizer::new(width, height),
|
||||
scale: pixel_size / units_per_em as f32,
|
||||
left,
|
||||
top,
|
||||
current: point(0.0, 0.0),
|
||||
contour_start: point(0.0, 0.0),
|
||||
})
|
||||
}
|
||||
|
||||
fn transform(&self, x: f32, y: f32) -> Point {
|
||||
point(x * self.scale - self.left, self.top - y * self.scale)
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlineBuilder for RasterOutline {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
let point = self.transform(x, y);
|
||||
self.current = point;
|
||||
self.contour_start = point;
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
let next = self.transform(x, y);
|
||||
self.rasterizer.draw_line(self.current, next);
|
||||
self.current = next;
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
|
||||
let control = self.transform(x1, y1);
|
||||
let next = self.transform(x, y);
|
||||
self.rasterizer.draw_quad(self.current, control, next);
|
||||
self.current = next;
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
|
||||
let control_1 = self.transform(x1, y1);
|
||||
let control_2 = self.transform(x2, y2);
|
||||
let next = self.transform(x, y);
|
||||
self.rasterizer.draw_cubic(self.current, control_1, control_2, next);
|
||||
self.current = next;
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.rasterizer.draw_line(self.current, self.contour_start);
|
||||
self.current = self.contour_start;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn indexed_math_glyph_rasterizes_to_bounded_rgba() {
|
||||
let font = MathFont::load().unwrap();
|
||||
let glyph = font.glyph_id('∫').unwrap();
|
||||
let rasterizer = MathGlyphRasterizer::new().unwrap();
|
||||
let image = rasterizer.rasterize(glyph.0, 32.0).unwrap();
|
||||
assert!(image.width > 0 && image.height > 0);
|
||||
assert_eq!(image.rgba.len(), image.width as usize * image.height as usize * 4);
|
||||
assert!(image.rgba.chunks_exact(4).any(|pixel| pixel[3] > 0));
|
||||
assert!(image.rgba.chunks_exact(4).all(|pixel| pixel[..3] == [255, 255, 255]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rasterization_is_deterministic_and_rejects_oversized_requests() {
|
||||
let font = MathFont::load().unwrap();
|
||||
let glyph = font.glyph_id('x').unwrap();
|
||||
let rasterizer = MathGlyphRasterizer::new().unwrap();
|
||||
let first = rasterizer.rasterize(glyph.0, 18.0).unwrap();
|
||||
let second = rasterizer.rasterize(glyph.0, 18.0).unwrap();
|
||||
assert_eq!(first, second);
|
||||
assert_eq!(
|
||||
rasterizer.rasterize(glyph.0, 4096.0).unwrap_err().kind,
|
||||
MathErrorKind::GlyphTooLarge
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user