use crate::Error;
use oxideav_core::{
FillRule, ImageRef, Node, Paint, Path, PathCommand, PathNode, Point, Rect, Rgba, Transform2D,
VideoFrame, VideoPlane,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FaceKind {
Ttf,
Otf,
}
fn next_face_id() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(1);
NEXT.fetch_add(1, Ordering::Relaxed)
}
#[derive(Debug)]
pub struct Face {
bytes: Box<[u8]>,
id: u64,
kind: FaceKind,
units_per_em: u16,
ascent: i16,
descent: i16,
line_gap: i16,
family: Option<String>,
italic_angle: f32,
weight_class: u16,
subfont_index: Option<u32>,
}
impl Face {
pub fn from_ttf_bytes(bytes: Vec<u8>) -> Result<Self, Error> {
let bytes: Box<[u8]> = bytes.into_boxed_slice();
let (units_per_em, ascent, descent, line_gap, family, italic_angle, weight_class) = {
let font = oxideav_ttf::Font::from_bytes(&bytes).map_err(Error::from)?;
(
font.units_per_em(),
font.ascent(),
font.descent(),
font.line_gap(),
font.family_name().map(|s| s.to_string()),
font.italic_angle(),
font.weight_class(),
)
};
Ok(Self {
bytes,
id: next_face_id(),
kind: FaceKind::Ttf,
units_per_em,
ascent,
descent,
line_gap,
family,
italic_angle,
weight_class,
subfont_index: None,
})
}
pub fn from_ttc_bytes(bytes: Vec<u8>, index: u32) -> Result<Self, Error> {
let bytes: Box<[u8]> = bytes.into_boxed_slice();
let (units_per_em, ascent, descent, line_gap, family, italic_angle, weight_class) = {
let font =
oxideav_ttf::Font::from_collection_bytes(&bytes, index).map_err(Error::from)?;
(
font.units_per_em(),
font.ascent(),
font.descent(),
font.line_gap(),
font.family_name().map(|s| s.to_string()),
font.italic_angle(),
font.weight_class(),
)
};
Ok(Self {
bytes,
id: next_face_id(),
kind: FaceKind::Ttf,
units_per_em,
ascent,
descent,
line_gap,
family,
italic_angle,
weight_class,
subfont_index: Some(index),
})
}
pub fn from_otf_bytes(bytes: Vec<u8>) -> Result<Self, Error> {
let bytes: Box<[u8]> = bytes.into_boxed_slice();
let (units_per_em, ascent, descent, line_gap, family) = {
let font = oxideav_otf::Font::from_bytes(&bytes).map_err(Error::from)?;
(
font.units_per_em(),
font.ascent(),
font.descent(),
font.line_gap(),
font.family_name().map(|s| s.to_string()),
)
};
Ok(Self {
bytes,
id: next_face_id(),
kind: FaceKind::Otf,
units_per_em,
ascent,
descent,
line_gap,
family,
italic_angle: 0.0,
weight_class: 400,
subfont_index: None,
})
}
pub fn kind(&self) -> FaceKind {
self.kind
}
pub fn id(&self) -> u64 {
self.id
}
pub fn stable_id(&self) -> u64 {
use std::hash::{DefaultHasher, Hash, Hasher};
let mut h = DefaultHasher::new();
(self.kind as u8).hash(&mut h);
self.subfont_index.hash(&mut h);
(self.bytes.len() as u64).hash(&mut h);
let prefix = &self.bytes[..self.bytes.len().min(256)];
prefix.hash(&mut h);
h.finish()
}
pub fn family_name(&self) -> Option<&str> {
self.family.as_deref()
}
pub fn units_per_em(&self) -> u16 {
self.units_per_em
}
pub fn ascent_px(&self, size_px: f32) -> f32 {
self.ascent as f32 * size_px / self.units_per_em as f32
}
pub fn descent_px(&self, size_px: f32) -> f32 {
self.descent as f32 * size_px / self.units_per_em as f32
}
pub fn line_height_px(&self, size_px: f32) -> f32 {
let units = self.ascent as i32 - self.descent as i32 + self.line_gap as i32;
units as f32 * size_px / self.units_per_em as f32
}
pub fn italic_angle(&self) -> f32 {
self.italic_angle
}
pub fn weight_class(&self) -> u16 {
self.weight_class
}
pub fn with_font<R>(&self, f: impl FnOnce(&oxideav_ttf::Font<'_>) -> R) -> Result<R, Error> {
if self.kind != FaceKind::Ttf {
return Err(Error::WrongFaceKind {
expected: FaceKind::Ttf,
actual: self.kind,
});
}
let font = match self.subfont_index {
Some(i) => {
oxideav_ttf::Font::from_collection_bytes(&self.bytes, i).map_err(Error::from)?
}
None => oxideav_ttf::Font::from_bytes(&self.bytes).map_err(Error::from)?,
};
Ok(f(&font))
}
pub fn subfont_index(&self) -> Option<u32> {
self.subfont_index
}
pub fn with_otf_font<R>(
&self,
f: impl FnOnce(&oxideav_otf::Font<'_>) -> R,
) -> Result<R, Error> {
if self.kind != FaceKind::Otf {
return Err(Error::WrongFaceKind {
expected: FaceKind::Otf,
actual: self.kind,
});
}
let font = oxideav_otf::Font::from_bytes(&self.bytes).map_err(Error::from)?;
Ok(f(&font))
}
pub fn glyph_path(&self, glyph_id: u16) -> Option<Path> {
match self.kind {
FaceKind::Ttf => {
let outline = self.with_font(|f| f.glyph_outline(glyph_id)).ok()?.ok()?;
if outline.contours.is_empty() {
return None;
}
Some(tt_outline_to_path(&outline))
}
FaceKind::Otf => {
let outline = self
.with_otf_font(|f| f.glyph_outline(glyph_id))
.ok()?
.ok()?;
if outline.contours.is_empty() {
return None;
}
Some(cff_outline_to_path(&outline))
}
}
}
pub fn glyph_node(&self, glyph_id: u16, size_px: f32) -> Option<Node> {
if size_px <= 0.0 || !size_px.is_finite() {
return None;
}
if matches!(self.kind, FaceKind::Ttf) && self.has_color_bitmaps() {
if let Ok(Some(cgb)) = self.raster_color_glyph(glyph_id, size_px) {
if !cgb.bitmap.is_empty() {
let w = cgb.bitmap.width;
let h = cgb.bitmap.height;
let stride = (w as usize) * 4;
let frame = VideoFrame {
pts: None,
planes: vec![VideoPlane {
stride,
data: cgb.bitmap.data.clone(),
}],
};
let strike_scale = if cgb.ppem > 0 {
size_px / cgb.ppem as f32
} else {
1.0
};
let bx = cgb.bearing_x as f32 * strike_scale;
let by = -(cgb.bearing_y as f32) * strike_scale;
let bw = w as f32 * strike_scale;
let bh = h as f32 * strike_scale;
return Some(Node::Image(ImageRef {
frame: Box::new(frame),
bounds: Rect {
x: bx,
y: by,
width: bw,
height: bh,
},
transform: Transform2D::identity(),
}));
}
}
}
let raw = self.glyph_path(glyph_id)?;
let upem = self.units_per_em.max(1) as f32;
let scale = size_px / upem;
let path = scale_and_flip_path(&raw, scale);
Some(Node::Path(PathNode {
path,
fill: Some(Paint::Solid(Rgba::opaque(0, 0, 0))),
stroke: None,
fill_rule: FillRule::NonZero,
}))
}
}
fn scale_and_flip_path(src: &Path, scale: f32) -> Path {
let mut out = Path {
commands: Vec::with_capacity(src.commands.len()),
};
let map = |p: Point| Point::new(p.x * scale, -p.y * scale);
for cmd in &src.commands {
let new = match *cmd {
PathCommand::MoveTo(p) => PathCommand::MoveTo(map(p)),
PathCommand::LineTo(p) => PathCommand::LineTo(map(p)),
PathCommand::QuadCurveTo { control, end } => PathCommand::QuadCurveTo {
control: map(control),
end: map(end),
},
PathCommand::CubicCurveTo { c1, c2, end } => PathCommand::CubicCurveTo {
c1: map(c1),
c2: map(c2),
end: map(end),
},
PathCommand::Close => PathCommand::Close,
other => other,
};
out.commands.push(new);
}
out
}
fn tt_outline_to_path(outline: &oxideav_ttf::TtOutline) -> Path {
let mut out = Path::new();
for contour in &outline.contours {
let pts = &contour.points;
if pts.is_empty() {
continue;
}
let n = pts.len();
let start_idx = pts.iter().position(|p| p.on_curve);
let (start_xy, ordered): (Point, Vec<(Point, bool)>) = if let Some(s) = start_idx {
let mut ord: Vec<(Point, bool)> = Vec::with_capacity(n);
for i in 0..n {
let p = pts[(s + i) % n];
ord.push((Point::new(p.x as f32, p.y as f32), p.on_curve));
}
(ord[0].0, ord)
} else {
let p0 = pts[0];
let p1 = pts[1 % n];
let mid = Point::new(
(p0.x as f32 + p1.x as f32) * 0.5,
(p0.y as f32 + p1.y as f32) * 0.5,
);
let mut ord: Vec<(Point, bool)> = Vec::with_capacity(n + 1);
ord.push((mid, true));
for p in pts.iter().take(n) {
ord.push((Point::new(p.x as f32, p.y as f32), p.on_curve));
}
(mid, ord)
};
out.commands.push(PathCommand::MoveTo(start_xy));
let mut prev_off: Option<Point> = None;
for &(xy, on) in ordered.iter().skip(1) {
if on {
if let Some(c) = prev_off.take() {
out.commands.push(PathCommand::QuadCurveTo {
control: c,
end: xy,
});
} else {
out.commands.push(PathCommand::LineTo(xy));
}
} else if let Some(c) = prev_off {
let mid = Point::new((c.x + xy.x) * 0.5, (c.y + xy.y) * 0.5);
out.commands.push(PathCommand::QuadCurveTo {
control: c,
end: mid,
});
prev_off = Some(xy);
} else {
prev_off = Some(xy);
}
}
if let Some(c) = prev_off.take() {
out.commands.push(PathCommand::QuadCurveTo {
control: c,
end: start_xy,
});
}
out.commands.push(PathCommand::Close);
}
out
}
fn cff_outline_to_path(outline: &oxideav_otf::CubicOutline) -> Path {
let mut out = Path::new();
for contour in &outline.contours {
for seg in &contour.segments {
match *seg {
oxideav_otf::CubicSegment::MoveTo(p) => {
out.commands.push(PathCommand::MoveTo(Point::new(p.x, p.y)));
}
oxideav_otf::CubicSegment::LineTo(p) => {
out.commands.push(PathCommand::LineTo(Point::new(p.x, p.y)));
}
oxideav_otf::CubicSegment::CurveTo { c1, c2, end } => {
out.commands.push(PathCommand::CubicCurveTo {
c1: Point::new(c1.x, c1.y),
c2: Point::new(c2.x, c2.y),
end: Point::new(end.x, end.y),
});
}
oxideav_otf::CubicSegment::ClosePath => {
out.commands.push(PathCommand::Close);
}
}
}
}
out
}