Skip to content

Commit 38f8bbb

Browse files
committed
feat(ttf2sig): find ZWJ ligatures
1 parent 28ea334 commit 38f8bbb

File tree

4 files changed

+193
-58
lines changed

4 files changed

+193
-58
lines changed

crates/ttf2sig/src/bin/ttf-info.rs

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ use signum::{
77
metrics::{FontMetrics, DEFAULT_FONT_SIZE},
88
printer::PrinterKind,
99
},
10-
image::{GrayImage, ImageFormat},
10+
image::{GenericImage, GrayImage, ImageFormat},
1111
};
12-
use ttf2sig::{glyph_index_vec, LigatureInfo};
12+
use ttf2sig::{glyph_index_vec, KerningInfo, LigatureInfo};
1313
use ttf_parser::{Face, GlyphId};
1414

1515
#[derive(Parser)]
@@ -33,13 +33,31 @@ fn main() -> color_eyre::Result<()> {
3333

3434
let face = Face::parse(&data, opt.index)?;
3535
let l = LigatureInfo::new(&face);
36+
let gpos = KerningInfo::new(&face);
37+
38+
debug_kern(&face);
3639

3740
let lig = opt.ligature.chars().collect::<Vec<_>>();
3841

3942
let glyphs = glyph_index_vec(&face, &lig).ok_or_eyre("Not all glyphs in font")?;
43+
eprintln!("{lig:?} => {glyphs:?}");
4044
let lig_glyph = l.find(&glyphs);
4145
println!("{:?} => {:?}", lig, lig_glyph);
4246

47+
for pair in glyphs.windows(2) {
48+
let (first, second) = (pair[0], pair[1]);
49+
// kern ttf-parser
50+
if let Some((v1, v2)) = gpos.find(first, second) {
51+
println!("v1: {v1:?}");
52+
println!("v2: {v2:?}");
53+
}
54+
}
55+
56+
let font_size = DEFAULT_FONT_SIZE;
57+
let pk = PrinterKind::Needle24;
58+
let fm = FontMetrics::new(pk, font_size);
59+
let px = fm.em_square_pixels() as f32;
60+
4361
let font = fontdue::Font::from_bytes(
4462
&data[..],
4563
fontdue::FontSettings {
@@ -49,21 +67,63 @@ fn main() -> color_eyre::Result<()> {
4967
},
5068
)
5169
.expect("already ttf parsed");
70+
71+
for pair in lig.windows(2) {
72+
// kern fontdue
73+
let (a, b) = (pair[0], pair[1]);
74+
let kern = font.horizontal_kern(a, b, px);
75+
println!("kern {pair:?} => {:?}", kern);
76+
}
77+
5278
if let Some(gl) = lig_glyph {
53-
let img = _raster(&font, gl)?;
79+
let img = _raster(&font, gl, px)?;
5480
_show(&img)?;
81+
} else {
82+
let mut ymin = i32::MAX;
83+
let mut ymax = i32::MIN;
84+
let mut x = 0;
85+
let mut width = 0;
86+
let mut pos = vec![];
87+
for g in &glyphs {
88+
let m = font.metrics_indexed(g.0, px);
89+
println!("{m:?}");
90+
ymin = m.ymin.min(ymin);
91+
let y = m.ymin + (m.height as i32);
92+
ymax = y.max(ymax);
93+
width = (x + m.width).max(width);
94+
pos.push((x as u32, y));
95+
x += m.advance_width as usize;
96+
}
97+
let height = (ymax - ymin) as usize;
98+
let mut canvas = GrayImage::new(width as u32, height as u32);
99+
canvas.fill(0xFF);
100+
101+
let mut pos_iter = pos.into_iter().map(|(x, y)| (x, (ymax - y) as u32));
102+
for g in glyphs {
103+
let img = _raster(&font, g, px)?;
104+
let (x, y) = pos_iter.next().expect("pos vec");
105+
canvas.copy_from(&img, x, y).unwrap();
106+
}
107+
_show(&canvas)?;
55108
}
56109

57110
Ok(())
58111
}
59112

60-
fn _raster(font: &fontdue::Font, g: GlyphId) -> color_eyre::Result<GrayImage> {
61-
let font_size = DEFAULT_FONT_SIZE;
62-
let pk = PrinterKind::Needle24;
63-
let fm = FontMetrics::new(pk, font_size);
64-
let px_per_em = fm.em_square_pixels();
113+
fn debug_kern(face: &Face<'_>) {
114+
if let Some(_kern) = face.tables().kern {
115+
} else {
116+
eprintln!("No kerning info (kern)");
117+
}
118+
119+
if let Some(_kerx) = face.tables().kerx {
120+
} else {
121+
eprintln!("No extended kerning info (kerx)");
122+
}
123+
}
65124

66-
let (metrics, bitmap) = font.rasterize_indexed(g.0, px_per_em as f32);
125+
fn _raster(font: &fontdue::Font, g: GlyphId, px: f32) -> color_eyre::Result<GrayImage> {
126+
let (metrics, bitmap) = font.rasterize_indexed(g.0, px);
67127
let inverted = bitmap.iter().copied().map(|c| 255 - c).collect();
68128
let img = GrayImage::from_vec(metrics.width as u32, metrics.height as u32, inverted)
69129
.context("image creation")?;

crates/ttf2sig/src/kerning.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use ttf_parser::{
2+
gpos::{PairAdjustment, PositioningSubtable, ValueRecord},
3+
Face, GlyphId,
4+
};
5+
6+
#[derive(Debug)]
7+
pub struct KerningInfo<'a> {
8+
pair_adjustments: Vec<PairAdjustment<'a>>,
9+
}
10+
11+
impl<'a> KerningInfo<'a> {
12+
pub fn new(face: &Face<'a>) -> Self {
13+
Self {
14+
pair_adjustments: face
15+
.tables()
16+
.gpos
17+
.map(|gpos| {
18+
gpos.lookups
19+
.into_iter()
20+
.flat_map(|lookup| {
21+
lookup
22+
.subtables
23+
.into_iter::<PositioningSubtable>()
24+
.filter_map(|subtable| match subtable {
25+
PositioningSubtable::Pair(pair) => Some(pair),
26+
_ => None,
27+
})
28+
})
29+
.collect()
30+
})
31+
.unwrap_or_default(),
32+
}
33+
}
34+
35+
pub fn find(
36+
&self,
37+
first: GlyphId,
38+
second: GlyphId,
39+
) -> Option<(ValueRecord<'a>, ValueRecord<'a>)> {
40+
for p in &self.pair_adjustments {
41+
if let Some(c) = p.coverage().get(first) {
42+
match p {
43+
PairAdjustment::Format1 { coverage: _, sets } => {
44+
let set = sets.get(c).expect("coverage");
45+
return set.get(second);
46+
}
47+
PairAdjustment::Format2 {
48+
coverage: _,
49+
classes: _,
50+
matrix: _,
51+
} => unimplemented!(),
52+
}
53+
}
54+
}
55+
None
56+
}
57+
}

crates/ttf2sig/src/lib.rs

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,9 @@
1-
use ttf_parser::{
2-
gsub::{LigatureSubstitution, SubstitutionSubtable},
3-
Face, GlyphId,
4-
};
1+
mod kerning;
2+
mod ligature;
53

6-
pub struct LigatureInfo<'a> {
7-
ligature_substitutions: Vec<LigatureSubstitution<'a>>,
8-
}
9-
10-
impl<'a> LigatureInfo<'a> {
11-
pub fn new(face: &Face<'a>) -> Self {
12-
Self {
13-
ligature_substitutions: face
14-
.tables()
15-
.gsub
16-
.map(|gsub| {
17-
gsub.lookups
18-
.into_iter()
19-
.flat_map(|lookup| {
20-
lookup
21-
.subtables
22-
.into_iter::<SubstitutionSubtable>()
23-
.filter_map(|subtable| match subtable {
24-
SubstitutionSubtable::Ligature(lig) => Some(lig),
25-
_ => None,
26-
})
27-
})
28-
.collect()
29-
})
30-
.unwrap_or_default(),
31-
}
32-
}
33-
34-
pub fn find(&self, glyphs: &[GlyphId]) -> Option<GlyphId> {
35-
let (&first, rest) = glyphs.split_first()?;
36-
37-
for lig in &self.ligature_substitutions {
38-
if let Some(coverage) = lig.coverage.get(first) {
39-
let l = lig.ligature_sets.get(coverage)?;
40-
for ligature in l {
41-
if ligature.components.into_iter().eq(rest.iter().copied()) {
42-
return Some(ligature.glyph);
43-
}
44-
}
45-
return None;
46-
}
47-
}
48-
None
49-
}
50-
}
4+
pub use kerning::KerningInfo;
5+
pub use ligature::LigatureInfo;
6+
use ttf_parser::{Face, GlyphId};
517

528
pub fn glyph_index_vec(face: &Face<'_>, lig: &[char]) -> Option<Vec<GlyphId>> {
539
let mut v = Vec::new();

crates/ttf2sig/src/ligature.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use ttf_parser::{
2+
gsub::{LigatureSubstitution, SubstitutionSubtable},
3+
Face, GlyphId,
4+
};
5+
6+
pub struct LigatureInfo<'a> {
7+
ligature_substitutions: Vec<LigatureSubstitution<'a>>,
8+
zwj: Option<GlyphId>,
9+
}
10+
11+
const ZERO_WIDTH_JOINER: char = '\u{200D}';
12+
13+
impl<'a> LigatureInfo<'a> {
14+
pub fn new(face: &Face<'a>) -> Self {
15+
Self {
16+
zwj: face.glyph_index(ZERO_WIDTH_JOINER),
17+
ligature_substitutions: face
18+
.tables()
19+
.gsub
20+
.map(|gsub| {
21+
gsub.lookups
22+
.into_iter()
23+
.flat_map(|lookup| {
24+
lookup
25+
.subtables
26+
.into_iter::<SubstitutionSubtable>()
27+
.filter_map(|subtable| match subtable {
28+
SubstitutionSubtable::Ligature(lig) => Some(lig),
29+
_ => None,
30+
})
31+
})
32+
.collect()
33+
})
34+
.unwrap_or_default(),
35+
}
36+
}
37+
38+
pub fn find(&self, glyphs: &[GlyphId]) -> Option<GlyphId> {
39+
let (&first, rest) = glyphs.split_first()?;
40+
41+
for lig in &self.ligature_substitutions {
42+
if let Some(coverage) = lig.coverage.get(first) {
43+
let l = lig.ligature_sets.get(coverage)?;
44+
for ligature in l {
45+
if ligature.components.into_iter().eq(rest.iter().copied()) {
46+
return Some(ligature.glyph);
47+
} else if let Some(zwj) = self.zwj {
48+
if ligature
49+
.components
50+
.into_iter()
51+
.eq(rest.iter().copied().flat_map(|i| [zwj, i].into_iter()))
52+
{
53+
return Some(ligature.glyph);
54+
}
55+
}
56+
}
57+
return None;
58+
}
59+
}
60+
None
61+
}
62+
}

0 commit comments

Comments
 (0)