Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/STABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ signatures only change in a major version.
cluster levels. A Rust `Buffer` and the C API both default to HarfBuzz's
`MonotoneGraphemes` (`MONOTONE_GRAPHEMES`); before 0.24.0 a Rust `Buffer`
defaulted to `MonotoneCharacters`. Both settings survive `Buffer::clear`.
- Added in 0.24.0:
- `Font::with_variations`: bind a variable instance from user-space axis values, as
HarfBuzz's `hb_font_set_variations` does. `Font::coords` now returns a slice that
borrows the font, since such a font owns its coords.
- `Face::glyph_outlines` and `GlyphOutlines::{outline, draw}`: draw many glyphs at
one instance with the outline tables read once.

`sigilbuzz-render`:

Expand Down
6 changes: 6 additions & 0 deletions fuzz/fuzz_targets/face.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,10 @@ fn exercise(face: &Face<'_>, knobs: &mut Knobs<'_>) {
let _ = face.svg_document(gid);
let _ = face.colr_paint(gid);
}
// A run of outlines with the tables read once draws each glyph as
// the face draws it, errors included.
let outlines = face.glyph_outlines(&coords);
for gid in glyph_ids(face, knobs) {
assert_eq!(outlines.outline(gid), face.glyph_outline_at_coords(gid, &coords));
}
}
15 changes: 14 additions & 1 deletion fuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,20 @@ pub fn axis_coords(face: &Face<'_>, knobs: &mut Knobs<'_>) -> Vec<f32> {
pub fn shape_samples(face: Face<'_>, knobs: &mut Knobs<'_>, extra: Option<&str>) {
let coords = axis_coords(&face, knobs);
let size = f32::from(knobs.byte()) + 1.0;
let font = Font::new(face, size).with_coords(&coords);
// Half the time the instance comes from user-space values.
let font = if knobs.byte() & 1 == 1 {
let variations: Vec<([u8; 4], f32)> = match face.fvar() {
Ok(Some(fvar)) => fvar
.axes()
.iter()
.map(|a| (a.tag, a.default_value + knobs.coord() * 500.0))
.collect(),
_ => Vec::new(),
};
Font::new(face, size).with_variations(&variations)
} else {
Font::new(face, size).with_coords(&coords)
};
let mut features = Vec::new();
for _ in 0..(knobs.byte() % 6) {
let i = usize::from(knobs.byte()) % FEATURE_TAGS.len();
Expand Down
2 changes: 2 additions & 0 deletions src/face.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
//! with [`crate::fonts_in_collection`].

mod color;
mod glyph_outlines;
mod layout;
mod outlines;
mod variations;
Expand All @@ -45,6 +46,7 @@ use crate::tables::parse::Reader;
use crate::tables::{tag, Cmap, Head, Hhea, Hmtx, Maxp, Name, Vhea, Vmtx, Vorg};

pub use color::GlyphBitmapEntry;
pub use glyph_outlines::GlyphOutlines;
pub(crate) use outlines::varied_glyph_bounds;

/// One entry in the SFNT table directory.
Expand Down
219 changes: 219 additions & 0 deletions src/face/glyph_outlines.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
//! [`GlyphOutlines`]: many glyphs of one face drawn at one instance,
//! with the outline tables read once.

use core::cell::OnceCell;
use core::fmt;

use alloc::vec::Vec;

use super::Face;
use crate::error::Result;
use crate::font::f2dot14_coords;
use crate::tables::cff2::Cff2Shared;
use crate::tables::glyf::PhantomMetrics;
use crate::tables::{
tag, Cff, Cff2, Glyf, Gvar, Hmtx, Loca, Outline, OutlineSink, PathOp, Varc, Vmtx,
};

/// Draws glyphs of one face at one set of variation coords, reading the
/// outline tables once for all of them.
///
/// [`Face::glyph_outline_at_coords`] reads the tables for every glyph,
/// which for a `CFF2` font means parsing the table, its variation store,
/// its Private DICTs, and the region scalars of the coords again and
/// again. A run of text draws many glyphs at one instance, so this keeps
/// them: the `CFF2` or `CFF ` table, or `glyf` with `loca`, `gvar` and
/// the metrics its phantom points read, and `VARC`. Each glyph comes out
/// exactly as [`Face::glyph_outline_at_coords`] draws it, errors
/// included: a table that does not read fails every glyph that needs it.
///
/// It caches through cells that are not thread-safe, so it is `Send` but
/// not `Sync`: build one per thread.
///
/// ```
/// use sigilbuzz::Face;
///
/// let data = include_bytes!("../../tests/fixtures/noto_sans_kr_vf_cff2_subset.otf");
/// let face = Face::parse_bytes(data, 0)?;
/// let coords = [0.5];
/// let outlines = face.glyph_outlines(&coords);
/// for gid in 0..face.maxp()?.num_glyphs {
/// assert_eq!(
/// outlines.outline(gid)?,
/// face.glyph_outline_at_coords(gid, &coords)?,
/// );
/// }
/// # Ok::<(), sigilbuzz::Error>(())
/// ```
pub struct GlyphOutlines<'a> {
face: Face<'a>,
/// The coords as shaping reads them, rounded to F2DOT14.
coords: Vec<f32>,
varc: OnceCell<Result<Option<Varc<'a>>>>,
tables: OnceCell<Result<Tables<'a>>>,
}

/// The table the glyphs are drawn from, chosen as
/// [`Face::glyph_outline_at_coords`] chooses it.
enum Tables<'a> {
/// With what its glyphs share at the coords.
Cff2(Cff2<'a>, Cff2Shared<'a>),
Cff(Cff<'a>),
Glyf {
glyf: Glyf<'a>,
loca: Loca<'a>,
hmtx: Hmtx<'a>,
/// The font's `vmtx`, or HarfBuzz's metrics for a font without.
vmtx: Vmtx<'a>,
/// `None` at the default instance.
gvar: Option<Gvar<'a>>,
},
}

impl<'a> Face<'a> {
/// Returns a [`GlyphOutlines`] that draws this face's glyphs at the
/// normalized variation coords `coords`, reading the outline tables
/// once. The coords are rounded to F2DOT14 as
/// [`Face::glyph_outline_at_coords`] rounds them; an empty slice
/// draws the default instance.
#[must_use]
pub fn glyph_outlines(&self, coords: &[f32]) -> GlyphOutlines<'a> {
GlyphOutlines {
face: self.clone(),
coords: f2dot14_coords(coords),
varc: OnceCell::new(),
tables: OnceCell::new(),
}
}
}

impl<'a> GlyphOutlines<'a> {
/// The outline of `glyph_id`, as [`Face::glyph_outline_at_coords`]
/// returns it: `Ok(None)` for a glyph with no outline or past the
/// end of the outline table.
///
/// # Errors
///
/// The errors [`Face::glyph_outline_at_coords`] returns for the
/// glyph.
pub fn outline(&self, glyph_id: u16) -> Result<Option<Outline>> {
let mut out = Outline::new();
let drew = self.draw(glyph_id, &mut out)?;
Ok(drew.then_some(out))
}

/// Draws `glyph_id` into `sink` and returns whether the glyph has an
/// outline, the ops [`GlyphOutlines::outline`] would return, without
/// collecting them. A glyph that fails partway may have sent some
/// ops to `sink` before the error.
///
/// # Errors
///
/// The errors [`Face::glyph_outline_at_coords`] returns for the
/// glyph.
pub fn draw<S: OutlineSink>(&self, glyph_id: u16, sink: &mut S) -> Result<bool> {
let varc = self
.varc
.get_or_init(|| self.face.varc())
.as_ref()
.map_err(Clone::clone)?;
// A VARC composite is drawn the way the face draws it: its
// components carry coords of their own.
if let Some(varc) = varc {
if varc.covers(glyph_id) && varc.composite(glyph_id, &self.coords).is_some() {
let Some(outline) = self.face.glyph_outline_at_coords(glyph_id, &self.coords)?
else {
return Ok(false);
};
replay(&outline, sink);
return Ok(true);
}
}
let tables = self
.tables
.get_or_init(|| self.read_tables())
.as_ref()
.map_err(Clone::clone)?;
match tables {
Tables::Cff2(cff2, shared) => cff2.outline_shared(glyph_id, &self.coords, shared, sink),
Tables::Cff(cff) => cff.outline(glyph_id, sink),
Tables::Glyf {
glyf,
loca,
hmtx,
vmtx,
gvar,
} => {
let metrics = PhantomMetrics {
hmtx,
vmtx: Some(vmtx),
};
glyf.outline_at_coords(
loca,
glyph_id,
gvar.as_ref(),
&self.coords,
Some(&metrics),
sink,
)
}
}
}

/// Reads the table the glyphs come from, in the order
/// [`Face::glyph_outline_at_coords`] reads them.
fn read_tables(&self) -> Result<Tables<'a>> {
let face = &self.face;
if face.record(tag::CFF2).is_some() {
return Ok(Tables::Cff2(face.cff2()?, Cff2Shared::default()));
}
if face.record(tag::CFF1).is_some() && face.record(tag::GLYF).is_none() {
return Ok(Tables::Cff(face.cff()?));
}
let loca = face.loca()?;
let glyf = face.glyf()?;
let hmtx = face.hmtx()?;
let vmtx = face.vmtx()?;
let vmtx = face.phantom_vmtx(vmtx)?;
let gvar = if self.coords.is_empty() {
None
} else {
face.gvar()?
};
Ok(Tables::Glyf {
glyf,
loca,
hmtx,
vmtx,
gvar,
})
}
}

/// Sends the ops of `outline` to `sink`.
fn replay<S: OutlineSink>(outline: &Outline, sink: &mut S) {
for op in outline.ops() {
match *op {
PathOp::MoveTo { x, y } => sink.move_to(x, y),
PathOp::LineTo { x, y } => sink.line_to(x, y),
PathOp::QuadTo { cx, cy, x, y } => sink.quad_to(cx, cy, x, y),
PathOp::CubicTo {
c1x,
c1y,
c2x,
c2y,
x,
y,
} => sink.curve_to(c1x, c1y, c2x, c2y, x, y),
PathOp::Close => sink.close(),
}
}
}

impl fmt::Debug for GlyphOutlines<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GlyphOutlines")
.field("coords", &self.coords)
.finish_non_exhaustive()
}
}
8 changes: 7 additions & 1 deletion src/face/outlines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ impl<'a> Face<'a> {
/// entirely (CFF-only); the caller should treat the missing
/// information as "drop the kern silently", the same conservative
/// posture sigilbuzz uses for fmt-4 fall-through everywhere else.
///
/// A font without `vmtx` gets HarfBuzz's vertical metrics for one
/// (see [`Vmtx::missing`]), as the outline and bounds methods do:
/// the top phantom point sits at the top of the glyph's box and the
/// bottom one an em below it.
pub fn glyph_points(&self, glyph_id: u16) -> Result<Option<Vec<(i16, i16)>>> {
if self.record(tag::GLYF).is_none() {
return Ok(None);
Expand All @@ -49,7 +54,8 @@ impl<'a> Face<'a> {
let glyf = self.glyf()?;
let hmtx = self.hmtx()?;
let vmtx = self.vmtx()?;
glyf.glyph_points(&loca, glyph_id, &hmtx, vmtx.as_ref())
let vmtx = self.phantom_vmtx(vmtx)?;
glyf.glyph_points(&loca, glyph_id, &hmtx, Some(&vmtx))
}

/// Returns the design-unit bounding box for `glyph_id`, or
Expand Down
Loading
Loading