1use crate::{
2 EmbeddedFontId, Font, ShapedGlyph, advance_units_to_pt, normalize::nfc_text, shape, text_width,
3};
4
5#[must_use]
29pub fn shape_text(font: Font, size: f32, text: &str) -> ShapedRun {
30 let text = nfc_text(text);
31 let text = text.as_ref();
32 match font {
33 Font::Base14(_) => ShapedRun {
34 glyphs: Vec::new(),
35 advance_pt: text_width(font, size, text),
36 },
37 Font::Embedded(id) => {
38 let ef = id.data();
39 let glyphs = shape(ef, text);
40 let upem = f32::from(ef.units_per_em);
41 let advance_pt: f32 = glyphs
42 .iter()
43 .map(|g| advance_units_to_pt(g.advance_units, size, upem))
44 .sum();
45 ShapedRun { glyphs, advance_pt }
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
66pub struct ShapedRun {
67 pub glyphs: Vec<ShapedGlyph>,
69 pub advance_pt: f32,
71}
72
73#[derive(Debug, Clone)]
98pub struct WordSubRun {
99 pub font: Font,
103 pub text: String,
107 pub glyphs: Vec<ShapedGlyph>,
112 pub advance_pt: f32,
116}
117
118#[must_use]
156pub fn shape_with_fallback(
157 primary: Font,
158 fallbacks: &[EmbeddedFontId],
159 size_pt: f32,
160 text: &str,
161) -> Vec<WordSubRun> {
162 let text = nfc_text(text);
163 let text = text.as_ref();
164 if text.is_empty() {
165 return Vec::new();
166 }
167
168 let primary_id = match primary {
169 Font::Base14(_) => {
170 return vec![WordSubRun {
172 font: primary,
173 text: text.to_owned(),
174 glyphs: Vec::new(),
175 advance_pt: text_width(primary, size_pt, text),
176 }];
177 }
178 Font::Embedded(id) => id,
179 };
180
181 let primary_ef = primary_id.data();
182 let primary_glyphs = shape(primary_ef, text);
183
184 if primary_glyphs.iter().all(|g| g.gid != 0) || fallbacks.is_empty() {
185 return vec![into_subrun(
187 primary,
188 text.to_owned(),
189 primary_glyphs,
190 primary_ef.units_per_em,
191 size_pt,
192 )];
193 }
194
195 let clusters = group_clusters(&primary_glyphs, text.len());
198
199 let mut resolutions: Vec<ClusterResolution> = Vec::with_capacity(clusters.len());
203 for cluster in &clusters {
204 let has_notdef = cluster.glyphs.iter().any(|g| g.gid == 0);
205 if !has_notdef {
206 resolutions.push(ClusterResolution {
207 font: primary,
208 byte_range: cluster.byte_range.clone(),
209 glyphs: cluster.glyphs.clone(),
210 });
211 continue;
212 }
213 let cluster_text = &text[cluster.byte_range.clone()];
216 let mut accepted: Option<(Font, Vec<ShapedGlyph>)> = None;
217 for &fb_id in fallbacks {
218 let fb_font = Font::Embedded(fb_id);
219 let fb_ef = fb_id.data();
220 let fb_glyphs = shape(fb_ef, cluster_text);
221 if !fb_glyphs.is_empty() && fb_glyphs.iter().all(|g| g.gid != 0) {
222 let shift = u32::try_from(cluster.byte_range.start).unwrap_or(u32::MAX);
230 let shifted: Vec<_> = fb_glyphs
231 .into_iter()
232 .map(|g| ShapedGlyph {
233 cluster: g.cluster + shift,
234 ..g
235 })
236 .collect();
237 accepted = Some((fb_font, shifted));
238 break;
239 }
240 }
241 match accepted {
242 Some((fb_font, fb_glyphs)) => resolutions.push(ClusterResolution {
243 font: fb_font,
244 byte_range: cluster.byte_range.clone(),
245 glyphs: fb_glyphs,
246 }),
247 None => resolutions.push(ClusterResolution {
248 font: primary,
249 byte_range: cluster.byte_range.clone(),
250 glyphs: cluster.glyphs.clone(),
251 }),
252 }
253 }
254
255 let mut subruns: Vec<WordSubRun> = Vec::new();
257 let mut current: Option<(Font, std::ops::Range<usize>, Vec<ShapedGlyph>)> = None;
258 for res in resolutions {
259 match current.take() {
260 Some((font, range, mut glyphs)) if font == res.font => {
261 let new_range = range.start..res.byte_range.end;
262 glyphs.extend(res.glyphs);
263 current = Some((font, new_range, glyphs));
264 }
265 Some((font, range, glyphs)) => {
266 subruns.push(finalize_subrun(font, range, glyphs, text, size_pt));
267 current = Some((res.font, res.byte_range, res.glyphs));
268 }
269 None => current = Some((res.font, res.byte_range, res.glyphs)),
270 }
271 }
272 if let Some((font, range, glyphs)) = current {
273 subruns.push(finalize_subrun(font, range, glyphs, text, size_pt));
274 }
275 subruns
276}
277
278struct ClusterGroup {
281 byte_range: std::ops::Range<usize>,
282 glyphs: Vec<ShapedGlyph>,
283}
284
285struct ClusterResolution {
288 font: Font,
289 byte_range: std::ops::Range<usize>,
290 glyphs: Vec<ShapedGlyph>,
291}
292
293fn group_clusters(glyphs: &[ShapedGlyph], text_len: usize) -> Vec<ClusterGroup> {
299 let mut groups: Vec<ClusterGroup> = Vec::new();
300 let mut i = 0;
301 while i < glyphs.len() {
302 let cluster = glyphs[i].cluster;
303 let mut j = i + 1;
304 while j < glyphs.len() && glyphs[j].cluster == cluster {
305 j += 1;
306 }
307 let end_byte = if j < glyphs.len() {
308 glyphs[j].cluster as usize
309 } else {
310 text_len
311 };
312 debug_assert!(end_byte >= cluster as usize);
313 groups.push(ClusterGroup {
314 byte_range: (cluster as usize)..end_byte,
315 glyphs: glyphs[i..j].to_vec(),
316 });
317 i = j;
318 }
319 groups
320}
321
322fn finalize_subrun(
327 font: Font,
328 byte_range: std::ops::Range<usize>,
329 glyphs: Vec<ShapedGlyph>,
330 parent_text: &str,
331 size_pt: f32,
332) -> WordSubRun {
333 let local_text = parent_text[byte_range.clone()].to_owned();
334 let shift = u32::try_from(byte_range.start).unwrap_or(u32::MAX);
339 let rebased: Vec<_> = glyphs
340 .into_iter()
341 .map(|g| ShapedGlyph {
342 cluster: g.cluster.saturating_sub(shift),
343 ..g
344 })
345 .collect();
346 let advance_pt: f32 = match font {
347 Font::Embedded(id) => {
348 let upem = embedded_upem(id);
349 rebased
350 .iter()
351 .map(|g| advance_units_to_pt(g.advance_units, size_pt, upem))
352 .sum()
353 }
354 Font::Base14(_) => text_width(font, size_pt, &local_text),
355 };
356 WordSubRun {
357 font,
358 text: local_text,
359 glyphs: rebased,
360 advance_pt,
361 }
362}
363
364fn into_subrun(
368 font: Font,
369 text: String,
370 glyphs: Vec<ShapedGlyph>,
371 upem: u16,
372 size_pt: f32,
373) -> WordSubRun {
374 let upem_f = f32::from(upem);
375 let advance_pt: f32 = glyphs
376 .iter()
377 .map(|g| advance_units_to_pt(g.advance_units, size_pt, upem_f))
378 .sum();
379 WordSubRun {
380 font,
381 text,
382 glyphs,
383 advance_pt,
384 }
385}
386
387fn embedded_upem(id: EmbeddedFontId) -> f32 {
389 f32::from(id.data().units_per_em)
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use crate::Base14Font;
396
397 #[test]
398 fn embedded_shape_is_empty_for_empty_string() {
399 let ef = EmbeddedFontId::Regular.data();
400 let glyphs = shape(ef, "");
401 assert!(glyphs.is_empty());
402 }
403
404 #[test]
405 fn embedded_shape_returns_clusters_in_byte_order() {
406 let ef = EmbeddedFontId::Regular.data();
407 let glyphs = shape(ef, "Привет");
408 assert!(!glyphs.is_empty());
409 let mut prev: u32 = 0;
412 for g in &glyphs {
413 assert!(
414 g.cluster >= prev,
415 "cluster regression: {prev} -> {}",
416 g.cluster
417 );
418 prev = g.cluster;
419 }
420 }
421
422 #[test]
423 fn embedded_shape_preserves_gpos_kerning() {
424 let ef = EmbeddedFontId::Regular.data();
425 let glyphs = shape(ef, "AV");
426 assert!(!glyphs.is_empty());
427 let nominal: i32 = glyphs
428 .iter()
429 .map(|g| i32::from(ef.advance_units(g.gid)))
430 .sum();
431 let shaped: i32 = glyphs.iter().map(|g| g.advance_units).sum();
432
433 assert!(
434 shaped < nominal,
435 "expected AV kerning to tighten advance: shaped={shaped} nominal={nominal}"
436 );
437 }
438
439 #[test]
440 fn embedded_shape_preserves_combining_mark_offsets() {
441 let ef = EmbeddedFontId::Regular.data();
442 let glyphs = shape(ef, "q\u{0302}\u{0301}");
443 assert!(
444 glyphs.len() >= 3,
445 "expected base 'q' + 2 combining marks, got {glyphs:?}"
446 );
447 assert!(
448 glyphs[1..]
449 .iter()
450 .any(|g| g.x_offset_units != 0 || g.y_offset_units != 0),
451 "expected at least one combining mark offset, got {glyphs:?}"
452 );
453 }
454
455 #[test]
456 fn shape_text_normalizes_decomposed_romanian() {
457 let font = Font::Embedded(EmbeddedFontId::Regular);
458 let decomposed = shape_text(font, 12.0, "S\u{0326}");
459 let precomposed = shape_text(font, 12.0, "\u{0218}");
460
461 let decomposed_gids: Vec<u16> = decomposed.glyphs.iter().map(|g| g.gid).collect();
462 let precomposed_gids: Vec<u16> = precomposed.glyphs.iter().map(|g| g.gid).collect();
463 assert_eq!(decomposed_gids, precomposed_gids);
464 assert!((decomposed.advance_pt - precomposed.advance_pt).abs() < f32::EPSILON);
465 }
466
467 #[test]
468 fn embedded_fi_ligature_collapses_glyphs() {
469 let ef = EmbeddedFontId::Regular.data();
476 let fi = shape(ef, "fi");
477 let f = shape(ef, "f");
478 let i = shape(ef, "i");
479 assert_eq!(fi.len(), 1, "expected fi ligature, got glyphs {fi:?}");
480 assert_ne!(fi[0].gid, f[0].gid);
481 assert_ne!(fi[0].gid, i[0].gid);
482 }
483
484 #[test]
485 fn fallback_empty_text_returns_empty() {
486 let primary = Font::Embedded(EmbeddedFontId::Regular);
487 let fallbacks = &[EmbeddedFontId::Math];
488 assert!(shape_with_fallback(primary, fallbacks, 11.0, "").is_empty());
489 }
490
491 #[test]
492 fn fallback_pure_primary_returns_single_subrun() {
493 let primary = Font::Embedded(EmbeddedFontId::Regular);
496 let fallbacks = &[EmbeddedFontId::Math];
497 let subs = shape_with_fallback(primary, fallbacks, 11.0, "Hello");
498 assert_eq!(subs.len(), 1, "expected one sub-run, got {}", subs.len());
499 assert_eq!(subs[0].font, primary);
500 assert_eq!(subs[0].text, "Hello");
501 assert!(!subs[0].glyphs.is_empty());
502 assert!(subs[0].glyphs.iter().all(|g| g.gid != 0));
503 assert!(subs[0].advance_pt > 0.0);
504 }
505
506 #[test]
507 fn fallback_shape_normalizes_subrun_text() {
508 let primary = Font::Embedded(EmbeddedFontId::Regular);
509 let fallbacks = &[EmbeddedFontId::Math];
510 let subs = shape_with_fallback(primary, fallbacks, 11.0, "S\u{0326}");
511
512 assert_eq!(subs.len(), 1);
513 assert_eq!(subs[0].text, "\u{0218}");
514 }
515
516 #[test]
517 fn fallback_mixed_latin_and_math_produces_alternating_subruns() {
518 let primary = Font::Embedded(EmbeddedFontId::Regular);
523 let fallbacks = &[EmbeddedFontId::Math];
524 let subs = shape_with_fallback(primary, fallbacks, 11.0, "a\u{2264}b");
525 assert_eq!(subs.len(), 3, "expected 3 sub-runs, got {subs:?}");
526
527 assert_eq!(subs[0].font, primary);
528 assert_eq!(subs[0].text, "a");
529
530 assert_eq!(subs[1].font, Font::Embedded(EmbeddedFontId::Math));
531 assert_eq!(subs[1].text, "\u{2264}");
532 assert!(
535 subs[1]
536 .glyphs
537 .iter()
538 .all(|g| (g.cluster as usize) < subs[1].text.len()),
539 "math sub-run glyph clusters not rebased: {:?}",
540 subs[1].glyphs,
541 );
542
543 assert_eq!(subs[2].font, primary);
544 assert_eq!(subs[2].text, "b");
545 }
546
547 #[test]
548 fn fallback_all_fail_keeps_primary_notdef() {
549 let primary = Font::Embedded(EmbeddedFontId::Regular);
554 let fallbacks = &[EmbeddedFontId::Math];
555 let subs = shape_with_fallback(primary, fallbacks, 11.0, "\u{1F389}");
556 assert_eq!(subs.len(), 1);
557 assert_eq!(subs[0].font, primary);
558 assert!(
559 subs[0].glyphs.iter().any(|g| g.gid == 0),
560 "expected .notdef for unsupported emoji, got {:?}",
561 subs[0].glyphs,
562 );
563 }
564
565 #[test]
566 fn fallback_base14_primary_returns_empty_glyphs_subrun() {
567 let primary = Font::Base14(Base14Font::Helvetica);
571 let fallbacks = &[EmbeddedFontId::Math];
572 let subs = shape_with_fallback(primary, fallbacks, 11.0, "Hello");
573 assert_eq!(subs.len(), 1);
574 assert_eq!(subs[0].font, primary);
575 assert!(subs[0].glyphs.is_empty());
576 assert!(subs[0].advance_pt > 0.0);
577 }
578
579 #[test]
580 fn fallback_no_fallbacks_configured_returns_single_subrun_even_with_notdef() {
581 let primary = Font::Embedded(EmbeddedFontId::Regular);
585 let subs = shape_with_fallback(primary, &[], 11.0, "\u{2264}");
586 assert_eq!(subs.len(), 1);
587 assert_eq!(subs[0].font, primary);
588 }
589
590 #[test]
591 fn fallback_math_subrun_advance_uses_math_face_upem() {
592 let primary = Font::Embedded(EmbeddedFontId::Regular);
596 let fallbacks = &[EmbeddedFontId::Math];
597 let subs = shape_with_fallback(primary, fallbacks, 11.0, "\u{2264}");
598 assert_eq!(subs.len(), 1);
599 assert_eq!(subs[0].font, Font::Embedded(EmbeddedFontId::Math));
600 assert!(subs[0].advance_pt > 0.0);
601 }
602}