@@ -8,7 +8,7 @@ use geo::{
88 orient:: Direction ,
99} ;
1010use std:: fmt:: Debug ;
11- use ttf_parser:: OutlineBuilder ;
11+ use ttf_parser:: { Face , GlyphId , OutlineBuilder } ;
1212use ttf_utils:: Outline ;
1313
1414// For flattening curves, how many segments per quad/cubic
@@ -21,7 +21,7 @@ impl<S: Clone + Debug + Send + Sync> Sketch<S> {
2121 /// and any open contours become `LineString`s.
2222 ///
2323 /// # Arguments
24- /// - `text`: the text string (no multiline logic here)
24+ /// - `text`: the text string
2525 /// - `font_data`: raw bytes of a TTF file
2626 /// - `scale`: a uniform scale factor for glyphs
2727 /// - `metadata`: optional metadata for the resulting `Sketch`
@@ -42,29 +42,62 @@ impl<S: Clone + Debug + Send + Sync> Sketch<S> {
4242 } ,
4343 } ;
4444
45- // 1 font unit, 2048 font units / em, scale points / em, 0.352777 points / mm
46- let font_scale = 1.0 / 2048.0 * scale * 0.3527777 ;
45+ // Treat `scale` as points-per-em and convert points to millimeters.
46+ let units_per_em = face. units_per_em ( ) as Real ;
47+ if units_per_em <= 0.0 || !scale. is_finite ( ) {
48+ return Sketch :: new ( ) ;
49+ }
50+ let font_scale = scale * 0.3527777 / units_per_em;
51+ let default_advance = default_advance ( & face, font_scale) ;
52+ let line_advance = line_advance ( & face, font_scale) ;
53+ let tab_advance = default_advance * 4.0 ;
4754
4855 // 2) We'll collect all glyph geometry into one GeometryCollection
4956 let mut geo_coll = GeometryCollection :: default ( ) ;
5057
5158 // 3) A simple "pen" cursor for horizontal text layout
5259 let mut cursor_x = 0.0 as Real ;
60+ let mut cursor_y = 0.0 as Real ;
61+ let mut previous_glyph = None ;
5362
5463 for ch in text. chars ( ) {
55- // Skip control chars:
56- if ch. is_control ( ) {
57- continue ;
64+ match ch {
65+ '\n' => {
66+ cursor_x = 0.0 ;
67+ cursor_y -= line_advance;
68+ previous_glyph = None ;
69+ continue ;
70+ } ,
71+ '\r' => {
72+ cursor_x = 0.0 ;
73+ previous_glyph = None ;
74+ continue ;
75+ } ,
76+ '\t' => {
77+ cursor_x += tab_advance;
78+ previous_glyph = None ;
79+ continue ;
80+ } ,
81+ ch if ch. is_control ( ) => {
82+ previous_glyph = None ;
83+ continue ;
84+ } ,
85+ _ => { } ,
5886 }
5987
6088 // Find glyph index in the font
6189 if let Some ( gid) = face. glyph_index ( ch) {
90+ if let Some ( previous) = previous_glyph {
91+ cursor_x += glyph_pair_kerning ( & face, previous, gid) * font_scale;
92+ }
93+
6294 // Extract the glyph outline (if any)
6395 if let Some ( outline) = Outline :: new ( & face, gid) {
6496 // Flatten the outline into line segments
6597 let mut collector =
66- OutlineFlattener :: new ( font_scale as Real , cursor_x as Real , 0.0 ) ;
98+ OutlineFlattener :: new ( font_scale as Real , cursor_x as Real , cursor_y ) ;
6799 outline. emit ( & mut collector) ;
100+ collector. finish_open_subpath ( ) ;
68101
69102 // Now `collector.contours` holds closed subpaths,
70103 // and `collector.open_contours` holds open polylines.
@@ -132,17 +165,14 @@ impl<S: Clone + Debug + Send + Sync> Sketch<S> {
132165 }
133166 }
134167
135- // Finally, advance our pen by the glyph's bounding-box width
136- let bbox = outline. bbox ( ) ;
137- let glyph_width = bbox. width ( ) as Real * font_scale;
138- cursor_x += glyph_width;
139- } else {
140- // If there's no outline (e.g., space), just move a bit
141- cursor_x += font_scale as Real * 0.3 ;
142168 }
169+
170+ cursor_x += glyph_advance ( & face, gid, font_scale, default_advance) ;
171+ previous_glyph = Some ( gid) ;
143172 } else {
144173 // Missing glyph => small blank advance
145- cursor_x += font_scale as Real * 0.3 ;
174+ cursor_x += default_advance;
175+ previous_glyph = None ;
146176 }
147177 }
148178
@@ -151,6 +181,104 @@ impl<S: Clone + Debug + Send + Sync> Sketch<S> {
151181 }
152182}
153183
184+ fn glyph_advance (
185+ face : & Face < ' _ > ,
186+ glyph_id : GlyphId ,
187+ font_scale : Real ,
188+ default_advance : Real ,
189+ ) -> Real {
190+ face. glyph_hor_advance ( glyph_id)
191+ . map ( |advance| advance as Real * font_scale)
192+ . filter ( |advance| advance. is_finite ( ) && * advance >= 0.0 )
193+ . unwrap_or ( default_advance)
194+ }
195+
196+ fn default_advance ( face : & Face < ' _ > , font_scale : Real ) -> Real {
197+ face. glyph_index ( ' ' )
198+ . and_then ( |glyph_id| face. glyph_hor_advance ( glyph_id) )
199+ . map ( |advance| advance as Real * font_scale)
200+ . filter ( |advance| advance. is_finite ( ) && * advance > 0.0 )
201+ . unwrap_or_else ( || face. units_per_em ( ) as Real * font_scale * 0.5 )
202+ }
203+
204+ fn line_advance ( face : & Face < ' _ > , font_scale : Real ) -> Real {
205+ let height = face. height ( ) as Real ;
206+ let line_gap = face. line_gap ( ) as Real ;
207+ let advance = ( height + line_gap) . max ( face. units_per_em ( ) as Real ) * font_scale;
208+ if advance. is_finite ( ) && advance > 0.0 {
209+ advance
210+ } else {
211+ face. units_per_em ( ) as Real * font_scale
212+ }
213+ }
214+
215+ fn glyph_pair_kerning ( face : & Face < ' _ > , left : GlyphId , right : GlyphId ) -> Real {
216+ let gpos_adjustment = gpos_pair_adjustment ( face, left, right) ;
217+ if gpos_adjustment != 0.0 {
218+ return gpos_adjustment;
219+ }
220+
221+ kern_pair_adjustment ( face, left, right)
222+ }
223+
224+ fn kern_pair_adjustment ( face : & Face < ' _ > , left : GlyphId , right : GlyphId ) -> Real {
225+ let Some ( kern) = face. tables ( ) . kern else {
226+ return 0.0 ;
227+ } ;
228+
229+ kern. subtables
230+ . into_iter ( )
231+ . filter ( |subtable| {
232+ subtable. horizontal && !subtable. has_cross_stream && !subtable. has_state_machine
233+ } )
234+ . filter_map ( |subtable| subtable. glyphs_kerning ( left, right) )
235+ . map ( |value| value as Real )
236+ . sum ( )
237+ }
238+
239+ fn gpos_pair_adjustment ( face : & Face < ' _ > , left : GlyphId , right : GlyphId ) -> Real {
240+ let Some ( gpos) = face. tables ( ) . gpos else {
241+ return 0.0 ;
242+ } ;
243+
244+ let mut adjustment = 0.0 ;
245+
246+ for lookup in gpos. lookups {
247+ for subtable in lookup
248+ . subtables
249+ . into_iter :: < ttf_parser:: gpos:: PositioningSubtable > ( )
250+ {
251+ let ttf_parser:: gpos:: PositioningSubtable :: Pair ( pair) = subtable else {
252+ continue ;
253+ } ;
254+
255+ adjustment += match pair {
256+ ttf_parser:: gpos:: PairAdjustment :: Format1 { coverage, sets } => coverage
257+ . get ( left)
258+ . and_then ( |index| sets. get ( index) )
259+ . and_then ( |set| set. get ( right) )
260+ . map ( |( left_value, right_value) | {
261+ left_value. x_advance as Real + right_value. x_placement as Real
262+ } )
263+ . unwrap_or ( 0.0 ) ,
264+ ttf_parser:: gpos:: PairAdjustment :: Format2 {
265+ classes, matrix, ..
266+ } => {
267+ let class_pair = ( classes. 0 . get ( left) , classes. 1 . get ( right) ) ;
268+ matrix
269+ . get ( class_pair)
270+ . map ( |( left_value, right_value) | {
271+ left_value. x_advance as Real + right_value. x_placement as Real
272+ } )
273+ . unwrap_or ( 0.0 )
274+ } ,
275+ } ;
276+ }
277+ }
278+
279+ adjustment
280+ }
281+
154282/// A helper that implements `ttf_parser::OutlineBuilder`.
155283/// It receives MoveTo/LineTo/QuadTo/CurveTo calls from `outline.emit(self)`.
156284/// We flatten curves and accumulate polylines.
@@ -209,7 +337,7 @@ impl OutlineFlattener {
209337
210338 /// Finish the current subpath as open (do not close).
211339 /// (We call this if a new `MoveTo` or the entire glyph ends.)
212- fn _finish_open_subpath ( & mut self ) {
340+ fn finish_open_subpath ( & mut self ) {
213341 if self . subpath_open && !self . current . is_empty ( ) {
214342 self . open_contours . push ( self . current . clone ( ) ) ;
215343 }
0 commit comments