425 lines
13 KiB
Rust
425 lines
13 KiB
Rust
//! Support for accessing glyph names.
|
|
|
|
use core::ops::Range;
|
|
use raw::{
|
|
tables::{
|
|
cff::Cff,
|
|
post::Post,
|
|
postscript::{Charset, CharsetIter, StringId as Sid},
|
|
},
|
|
types::GlyphId,
|
|
FontRef, TableProvider,
|
|
};
|
|
|
|
/// "Names must be no longer than 63 characters; some older implementations
|
|
/// can assume a length limit of 31 characters."
|
|
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/post#version-20>
|
|
const MAX_GLYPH_NAME_LEN: usize = 63;
|
|
|
|
/// Mapping from glyph identifiers to names.
|
|
///
|
|
/// This sources glyph names from the `post` and `CFF` tables in that order.
|
|
/// If glyph names are not available in either, then they are synthesized
|
|
/// as `gidDDD` where `DDD` is the glyph identifier in decimal. Use the
|
|
/// [`source`](Self::source) to determine which source was chosen.
|
|
#[derive(Clone)]
|
|
pub struct GlyphNames<'a> {
|
|
inner: Inner<'a>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum Inner<'a> {
|
|
// Second field is num_glyphs
|
|
Post(Post<'a>, u32),
|
|
Cff(Cff<'a>, Charset<'a>),
|
|
Synthesized(u32),
|
|
}
|
|
|
|
impl<'a> GlyphNames<'a> {
|
|
/// Creates a new object for accessing glyph names from the given font.
|
|
pub fn new(font: &FontRef<'a>) -> Self {
|
|
let num_glyphs = font
|
|
.maxp()
|
|
.map(|maxp| maxp.num_glyphs() as u32)
|
|
.unwrap_or_default();
|
|
if let Ok(post) = font.post() {
|
|
if post.num_names() != 0 {
|
|
return Self {
|
|
inner: Inner::Post(post, num_glyphs),
|
|
};
|
|
}
|
|
}
|
|
if let Some((cff, charset)) = font
|
|
.cff()
|
|
.ok()
|
|
.and_then(|cff| Some((cff.clone(), cff.charset(0).ok()??)))
|
|
{
|
|
return Self {
|
|
inner: Inner::Cff(cff, charset),
|
|
};
|
|
}
|
|
Self {
|
|
inner: Inner::Synthesized(num_glyphs),
|
|
}
|
|
}
|
|
|
|
/// Returns the chosen source for glyph names.
|
|
pub fn source(&self) -> GlyphNameSource {
|
|
match &self.inner {
|
|
Inner::Post(..) => GlyphNameSource::Post,
|
|
Inner::Cff(..) => GlyphNameSource::Cff,
|
|
Inner::Synthesized(..) => GlyphNameSource::Synthesized,
|
|
}
|
|
}
|
|
|
|
/// Returns the number of glyphs in the font.
|
|
pub fn num_glyphs(&self) -> u32 {
|
|
match &self.inner {
|
|
Inner::Post(_, n) | Inner::Synthesized(n) => *n,
|
|
Inner::Cff(_, charset) => charset.num_glyphs(),
|
|
}
|
|
}
|
|
|
|
/// Returns the name for the given glyph identifier.
|
|
pub fn get(&self, glyph_id: GlyphId) -> Option<GlyphName> {
|
|
if glyph_id.to_u32() >= self.num_glyphs() {
|
|
return None;
|
|
}
|
|
let name = match &self.inner {
|
|
Inner::Post(post, _) => GlyphName::from_post(post, glyph_id),
|
|
Inner::Cff(cff, charset) => charset
|
|
.string_id(glyph_id)
|
|
.ok()
|
|
.and_then(|sid| GlyphName::from_cff_sid(cff, sid)),
|
|
_ => None,
|
|
};
|
|
// If name is empty string, synthesize it
|
|
if !name.as_ref().is_some_and(|s| !s.is_empty()) {
|
|
return Some(GlyphName::synthesize(glyph_id));
|
|
}
|
|
Some(name.unwrap_or_else(|| GlyphName::synthesize(glyph_id)))
|
|
}
|
|
|
|
/// Returns an iterator yielding the identifier and name for all glyphs in
|
|
/// the font.
|
|
pub fn iter(&self) -> impl Iterator<Item = (GlyphId, GlyphName)> + 'a + Clone {
|
|
match &self.inner {
|
|
Inner::Post(post, n) => Iter::Post(0..*n, post.clone()),
|
|
Inner::Cff(cff, charset) => Iter::Cff(cff.clone(), charset.iter()),
|
|
Inner::Synthesized(n) => Iter::Synthesized(0..*n),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Specifies the chosen source for glyph names.
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
pub enum GlyphNameSource {
|
|
/// Glyph names are sourced from the `post` table.
|
|
Post,
|
|
/// Glyph names are sourced from the `CFF` table.
|
|
Cff,
|
|
/// Glyph names are synthesized in the format `gidDDD` where `DDD` is
|
|
/// the glyph identifier in decimal.
|
|
Synthesized,
|
|
}
|
|
|
|
/// The name of a glyph.
|
|
#[derive(Clone)]
|
|
pub struct GlyphName {
|
|
name: [u8; MAX_GLYPH_NAME_LEN],
|
|
len: u8,
|
|
is_synthesized: bool,
|
|
}
|
|
|
|
impl GlyphName {
|
|
/// Returns the underlying name as a string.
|
|
pub fn as_str(&self) -> &str {
|
|
let bytes = &self.name[..self.len as usize];
|
|
core::str::from_utf8(bytes).unwrap_or_default()
|
|
}
|
|
|
|
/// Returns true if the glyph name was synthesized, i.e. not found in any
|
|
/// source.
|
|
pub fn is_synthesized(&self) -> bool {
|
|
self.is_synthesized
|
|
}
|
|
|
|
fn from_bytes(bytes: &[u8]) -> Self {
|
|
let mut name = Self::default();
|
|
name.append(bytes);
|
|
name
|
|
}
|
|
|
|
fn from_post(post: &Post, glyph_id: GlyphId) -> Option<Self> {
|
|
glyph_id
|
|
.try_into()
|
|
.ok()
|
|
.and_then(|id| post.glyph_name(id))
|
|
.map(|s| s.as_bytes())
|
|
.map(Self::from_bytes)
|
|
}
|
|
|
|
fn from_cff_sid(cff: &Cff, sid: Sid) -> Option<Self> {
|
|
cff.string(sid)
|
|
.and_then(|s| core::str::from_utf8(s.bytes()).ok())
|
|
.map(|s| s.as_bytes())
|
|
.map(Self::from_bytes)
|
|
}
|
|
|
|
fn synthesize(glyph_id: GlyphId) -> Self {
|
|
use core::fmt::Write;
|
|
let mut name = Self {
|
|
is_synthesized: true,
|
|
..Self::default()
|
|
};
|
|
let _ = write!(GlyphNameWrite(&mut name), "gid{}", glyph_id.to_u32());
|
|
name
|
|
}
|
|
|
|
/// Appends the given bytes to `self` while keeping the maximum length
|
|
/// at 63 bytes.
|
|
///
|
|
/// This exists primarily to support the [`core::fmt::Write`] impl
|
|
/// (which is used for generating synthesized glyph names) because
|
|
/// we have no guarantee of how many times `write_str` might be called
|
|
/// for a given format.
|
|
fn append(&mut self, bytes: &[u8]) {
|
|
// We simply truncate when length exceeds the max since glyph names
|
|
// are expected to be <= 63 chars
|
|
let start = self.len as usize;
|
|
let available = MAX_GLYPH_NAME_LEN - start;
|
|
let copy_len = available.min(bytes.len());
|
|
self.name[start..start + copy_len].copy_from_slice(&bytes[..copy_len]);
|
|
self.len = (start + copy_len) as u8;
|
|
}
|
|
}
|
|
|
|
impl Default for GlyphName {
|
|
fn default() -> Self {
|
|
Self {
|
|
name: [0; MAX_GLYPH_NAME_LEN],
|
|
len: 0,
|
|
is_synthesized: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl core::fmt::Debug for GlyphName {
|
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
f.debug_struct("GlyphName")
|
|
.field("name", &self.as_str())
|
|
.field("is_synthesized", &self.is_synthesized)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl core::fmt::Display for GlyphName {
|
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
write!(f, "{}", self.as_str())
|
|
}
|
|
}
|
|
|
|
impl core::ops::Deref for GlyphName {
|
|
type Target = str;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
self.as_str()
|
|
}
|
|
}
|
|
|
|
impl PartialEq<&str> for GlyphName {
|
|
fn eq(&self, other: &&str) -> bool {
|
|
self.as_str() == *other
|
|
}
|
|
}
|
|
|
|
struct GlyphNameWrite<'a>(&'a mut GlyphName);
|
|
|
|
impl core::fmt::Write for GlyphNameWrite<'_> {
|
|
fn write_str(&mut self, s: &str) -> core::fmt::Result {
|
|
self.0.append(s.as_bytes());
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum Iter<'a> {
|
|
Post(Range<u32>, Post<'a>),
|
|
Cff(Cff<'a>, CharsetIter<'a>),
|
|
Synthesized(Range<u32>),
|
|
}
|
|
|
|
impl Iter<'_> {
|
|
fn next_name(&mut self) -> Option<Result<(GlyphId, GlyphName), GlyphId>> {
|
|
match self {
|
|
Self::Post(range, post) => {
|
|
let gid = GlyphId::new(range.next()?);
|
|
Some(
|
|
GlyphName::from_post(post, gid)
|
|
.map(|name| (gid, name))
|
|
.ok_or(gid),
|
|
)
|
|
}
|
|
Self::Cff(cff, iter) => {
|
|
let (gid, sid) = iter.next()?;
|
|
Some(
|
|
GlyphName::from_cff_sid(cff, sid)
|
|
.map(|name| (gid, name))
|
|
.ok_or(gid),
|
|
)
|
|
}
|
|
Self::Synthesized(range) => {
|
|
let gid = GlyphId::new(range.next()?);
|
|
Some(Ok((gid, GlyphName::synthesize(gid))))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Iterator for Iter<'_> {
|
|
type Item = (GlyphId, GlyphName);
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
match self.next_name()? {
|
|
Ok((gid, name)) if name.is_empty() => Some((gid, GlyphName::synthesize(gid))),
|
|
Ok(gid_name) => Some(gid_name),
|
|
Err(gid) => Some((gid, GlyphName::synthesize(gid))),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use raw::{FontData, FontRead};
|
|
|
|
#[test]
|
|
fn synthesized_glyph_names() {
|
|
let count = 58;
|
|
let names = GlyphNames {
|
|
inner: Inner::Synthesized(58),
|
|
};
|
|
let names_buf = (0..count).map(|i| format!("gid{i}")).collect::<Vec<_>>();
|
|
let expected_names = names_buf.iter().map(|s| s.as_str()).collect::<Vec<_>>();
|
|
for (_, name) in names.iter() {
|
|
assert!(name.is_synthesized())
|
|
}
|
|
check_names(&names, &expected_names, GlyphNameSource::Synthesized);
|
|
}
|
|
|
|
#[test]
|
|
fn synthesize_for_empty_names() {
|
|
let mut post_data = font_test_data::post::SIMPLE.to_vec();
|
|
// last name in this post data is "hola" so pop 5 bytes and then
|
|
// push a 0 to simulate an empty name
|
|
post_data.truncate(post_data.len() - 5);
|
|
post_data.push(0);
|
|
let post = Post::read(FontData::new(&post_data)).unwrap();
|
|
let gid = GlyphId::new(9);
|
|
assert!(post.glyph_name(gid.try_into().unwrap()).unwrap().is_empty());
|
|
let names = GlyphNames {
|
|
inner: Inner::Post(post, 10),
|
|
};
|
|
assert_eq!(names.get(gid).unwrap(), "gid9");
|
|
assert_eq!(names.iter().last().unwrap().1, "gid9");
|
|
}
|
|
|
|
#[test]
|
|
fn cff_glyph_names() {
|
|
let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
|
|
let names = GlyphNames::new(&font);
|
|
assert_eq!(names.source(), GlyphNameSource::Cff);
|
|
let expected_names = [".notdef", "i", "j", "k", "l"];
|
|
check_names(&names, &expected_names, GlyphNameSource::Cff);
|
|
}
|
|
|
|
#[test]
|
|
fn post_glyph_names() {
|
|
let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
|
|
let names = GlyphNames::new(&font);
|
|
let expected_names = [
|
|
".notdef",
|
|
"space",
|
|
"A",
|
|
"I",
|
|
"T",
|
|
"Aacute",
|
|
"Agrave",
|
|
"Iacute",
|
|
"Igrave",
|
|
"Amacron",
|
|
"Imacron",
|
|
"acutecomb",
|
|
"gravecomb",
|
|
"macroncomb",
|
|
"A.001",
|
|
"A.002",
|
|
"A.003",
|
|
"A.004",
|
|
"A.005",
|
|
"A.006",
|
|
"A.007",
|
|
"A.008",
|
|
"A.009",
|
|
"A.010",
|
|
];
|
|
check_names(&names, &expected_names, GlyphNameSource::Post);
|
|
}
|
|
|
|
#[test]
|
|
fn post_glyph_names_partial() {
|
|
let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
|
|
let mut names = GlyphNames::new(&font);
|
|
let Inner::Post(_, len) = &mut names.inner else {
|
|
panic!("it's a post table!");
|
|
};
|
|
// Increase count by 4 so we synthesize the remaining names
|
|
*len += 4;
|
|
let expected_names = [
|
|
".notdef",
|
|
"space",
|
|
"A",
|
|
"I",
|
|
"T",
|
|
"Aacute",
|
|
"Agrave",
|
|
"Iacute",
|
|
"Igrave",
|
|
"Amacron",
|
|
"Imacron",
|
|
"acutecomb",
|
|
"gravecomb",
|
|
"macroncomb",
|
|
"A.001",
|
|
"A.002",
|
|
"A.003",
|
|
"A.004",
|
|
"A.005",
|
|
"A.006",
|
|
"A.007",
|
|
"A.008",
|
|
"A.009",
|
|
"A.010",
|
|
// synthesized names...
|
|
"gid24",
|
|
"gid25",
|
|
"gid26",
|
|
"gid27",
|
|
];
|
|
check_names(&names, &expected_names, GlyphNameSource::Post);
|
|
}
|
|
|
|
fn check_names(names: &GlyphNames, expected_names: &[&str], expected_source: GlyphNameSource) {
|
|
assert_eq!(names.source(), expected_source);
|
|
let iter_names = names.iter().collect::<Vec<_>>();
|
|
assert_eq!(iter_names.len(), expected_names.len());
|
|
for (i, expected) in expected_names.iter().enumerate() {
|
|
let gid = GlyphId::new(i as u32);
|
|
let name = names.get(gid).unwrap();
|
|
assert_eq!(name, expected);
|
|
assert_eq!(iter_names[i].0, gid);
|
|
assert_eq!(iter_names[i].1, expected);
|
|
}
|
|
}
|
|
}
|