1use std::{
2 borrow::Borrow,
3 cell::RefCell,
4 collections::HashMap,
5 ffi::OsStr,
6 fs,
7 hash::{Hash, Hasher},
8 ops::Range,
9 path::Path as FilePath,
10 rc::Rc,
11};
12
13use fnv::{FnvBuildHasher, FnvHashMap, FnvHasher};
14use lru::LruCache;
15use rustybuzz::ttf_parser;
16use slotmap::{DefaultKey, SlotMap};
17
18use unicode_bidi::BidiInfo;
19use unicode_segmentation::UnicodeSegmentation;
20
21use crate::{
22 paint::{PaintFlavor, StrokeSettings, TextSettings},
23 Canvas, Color, ErrorKind, FillRule, ImageFlags, ImageId, ImageInfo, Paint, PixelFormat, RenderTarget, Renderer,
24};
25
26mod atlas;
27pub use atlas::Atlas;
28
29mod font;
30pub use font::FontMetrics;
31use font::{Font, GlyphRendering};
32
33// This padding is an empty border around the glyph’s pixels but inside the
34// sampled area (texture coordinates) for the quad in render_atlas().
35const GLYPH_PADDING: u32 = 1;
36// We add an additional margin of 1 pixel outside of the sampled area,
37// to deal with the linear interpolation of texels at the edge of that area
38// which mixes in the texels just outside of the edge.
39// This manifests as noise around the glyph, outside of the padding.
40const GLYPH_MARGIN: u32 = 1;
41
42const TEXTURE_SIZE: usize = 512;
43const DEFAULT_LRU_CACHE_CAPACITY: usize = 1000;
44
45/// A font handle.
46#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
47pub struct FontId(DefaultKey);
48
49/// Represents the vertical alignment of a text baseline.
50///
51/// The default value is `Alphabetic`.
52#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default)]
53#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
54pub enum Baseline {
55 /// The text baseline is the top of the em square.
56 Top,
57 /// The text baseline is the middle of the em square.
58 Middle,
59 /// The text baseline is the normal alphabetic baseline.
60 #[default]
61 Alphabetic,
62 /// The text baseline is the bottom of the bounding box.
63 Bottom,
64}
65
66/// Represents the horizontal alignment of text.
67///
68/// The default value is `Left`.
69#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default)]
70#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
71pub enum Align {
72 /// The text is left-aligned.
73 #[default]
74 Left,
75 /// The text is centered.
76 Center,
77 /// The text is right-aligned.
78 Right,
79}
80
81/// Represents the rendering mode for a path.
82///
83/// The default value is `Fill`.
84#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default)]
85pub enum RenderMode {
86 /// The path is filled.
87 #[default]
88 Fill,
89 /// The path is stroked.
90 Stroke,
91}
92
93#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
94pub struct RenderedGlyphId {
95 glyph_index: u32,
96 font_id: FontId,
97 size: u32,
98 line_width: u32,
99 render_mode: RenderMode,
100 subpixel_location: u8,
101}
102
103impl RenderedGlyphId {
104 fn new(
105 glyph_index: u32,
106 font_id: FontId,
107 font_size: f32,
108 line_width: f32,
109 mode: RenderMode,
110 subpixel_location: u8,
111 ) -> Self {
112 Self {
113 glyph_index,
114 font_id,
115 size: (font_size * 10.0).trunc() as u32,
116 line_width: (line_width * 10.0).trunc() as u32,
117 render_mode: mode,
118 subpixel_location,
119 }
120 }
121}
122
123#[derive(Copy, Clone, Debug)]
124pub struct RenderedGlyph {
125 texture_index: usize,
126 width: u32,
127 height: u32,
128 bearing_y: i32,
129 atlas_x: u32,
130 atlas_y: u32,
131 color_glyph: bool,
132}
133
134#[derive(Copy, Clone, Debug)]
135pub struct ShapedGlyph {
136 pub x: f32,
137 pub y: f32,
138 pub c: char,
139 pub byte_index: usize,
140 pub font_id: FontId,
141 pub codepoint: u32,
142 pub width: f32,
143 pub height: f32,
144 pub advance_x: f32,
145 pub advance_y: f32,
146 pub offset_x: f32,
147 pub offset_y: f32,
148 pub bearing_x: f32,
149 pub bearing_y: f32,
150 pub bitmap_glyph: bool,
151}
152
153#[derive(Clone, Debug, Default)]
154struct ShapedWord {
155 glyphs: Vec<ShapedGlyph>,
156 width: f32,
157}
158
159#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
160struct ShapingId {
161 size: u32,
162 word_hash: u64,
163 font_ids: [Option<FontId>; 8],
164}
165
166impl ShapingId {
167 fn new(font_size: f32, font_ids: [Option<FontId>; 8], word: &str, max_width: Option<f32>) -> Self {
168 let mut hasher: FnvHasher = FnvHasher::default();
169 word.hash(&mut hasher);
170 if let Some(max_width: f32) = max_width {
171 (max_width.trunc() as i32).hash(&mut hasher);
172 }
173
174 Self {
175 size: (font_size * 10.0).trunc() as u32,
176 word_hash: hasher.finish(),
177 font_ids,
178 }
179 }
180}
181
182type ShapedWordsCache<H> = LruCache<ShapingId, Result<ShapedWord, ErrorKind>, H>;
183type ShapingRunCache<H> = LruCache<ShapingId, TextMetrics, H>;
184
185pub struct FontTexture {
186 pub atlas: Atlas,
187 pub(crate) image_id: ImageId,
188}
189
190/// `TextContext` provides functionality for text processing in femtovg. You can
191/// add fonts using the [`Self::add_font_file()`], [`Self::add_font_mem()`] and
192/// [`Self::add_font_dir()`] functions. For each registered font a [`FontId`] is
193/// returned.
194///
195/// The [`FontId`] can be supplied to [`crate::Paint`] along with additional parameters
196/// such as the font size.
197///
198/// The paint is needed when using `TextContext`'s measurement functions such as
199/// [`Self::measure_text()`].
200///
201/// Note that the measurements are done entirely with the supplied sizes in the paint
202/// parameter. If you need measurements that take a [`crate::Canvas`]'s transform or dpi into
203/// account (see [`crate::Canvas::set_size()`]), you need to use the measurement functions
204/// on the canvas.
205#[derive(Clone, Default)]
206pub struct TextContext(pub(crate) Rc<RefCell<TextContextImpl>>);
207
208impl TextContext {
209 /// Registers all .ttf files from a directory with this text context. If successful, the
210 /// font ids of all registered fonts are returned.
211 pub fn add_font_dir<T: AsRef<FilePath>>(&self, path: T) -> Result<Vec<FontId>, ErrorKind> {
212 self.0.borrow_mut().add_font_dir(path)
213 }
214
215 /// Registers the .ttf file from the specified path with this text context. If successful,
216 /// the font id is returned.
217 pub fn add_font_file<T: AsRef<FilePath>>(&self, path: T) -> Result<FontId, ErrorKind> {
218 self.0.borrow_mut().add_font_file(path)
219 }
220
221 /// Registers the in-memory representation of a TrueType font pointed to by the data
222 /// parameter with this text context. If successful, the font id is returned.
223 pub fn add_font_mem(&self, data: &[u8]) -> Result<FontId, ErrorKind> {
224 self.0.borrow_mut().add_font_mem(data)
225 }
226
227 /// Registers the in-memory representation of a TrueType font pointed to by the shared data
228 /// parameter with this text context. If successful, the font id is returned. The `face_index`
229 /// specifies the face index if the font data is a true type font collection. For plain true
230 /// type fonts, use 0 as index.
231 pub fn add_shared_font_with_index<T: AsRef<[u8]> + 'static>(
232 &self,
233 data: T,
234 face_index: u32,
235 ) -> Result<FontId, ErrorKind> {
236 self.0.borrow_mut().add_shared_font_with_index(data, face_index)
237 }
238
239 /// Returns information on how the provided text will be drawn with the specified paint.
240 pub fn measure_text<S: AsRef<str>>(
241 &self,
242 x: f32,
243 y: f32,
244 text: S,
245 paint: &Paint,
246 ) -> Result<TextMetrics, ErrorKind> {
247 self.0.borrow_mut().measure_text(x, y, text, &paint.text)
248 }
249
250 /// Returns the maximum index-th byte of text that will fit inside `max_width`.
251 ///
252 /// The retuned index will always lie at the start and/or end of a UTF-8 code point sequence or at the start or end of the text
253 pub fn break_text<S: AsRef<str>>(&self, max_width: f32, text: S, paint: &Paint) -> Result<usize, ErrorKind> {
254 self.0.borrow_mut().break_text(max_width, text, &paint.text)
255 }
256
257 /// Returnes a list of ranges representing each line of text that will fit inside `max_width`
258 pub fn break_text_vec<S: AsRef<str>>(
259 &self,
260 max_width: f32,
261 text: S,
262 paint: &Paint,
263 ) -> Result<Vec<Range<usize>>, ErrorKind> {
264 self.0.borrow_mut().break_text_vec(max_width, text, &paint.text)
265 }
266
267 /// Returns font metrics for a particular Paint.
268 pub fn measure_font(&self, paint: &Paint) -> Result<FontMetrics, ErrorKind> {
269 self.0
270 .borrow_mut()
271 .measure_font(paint.text.font_size, &paint.text.font_ids)
272 }
273
274 /// Adjusts the capacity of the shaping run cache. This is a cache for measurements of whole
275 /// strings.
276 pub fn resize_shaping_run_cache(&self, capacity: std::num::NonZeroUsize) {
277 self.0.borrow_mut().resize_shaping_run_cache(capacity)
278 }
279
280 /// Adjusts the capacity of the shaped words cache. This is a cache for measurements of
281 /// individual words. Words are separated by
282 /// [UAX#29 word boundaries](http://www.unicode.org/reports/tr29/#Word_Boundaries).
283 pub fn resize_shaped_words_cache(&self, capacity: std::num::NonZeroUsize) {
284 self.0.borrow_mut().resize_shaped_words_cache(capacity)
285 }
286}
287
288pub struct TextContextImpl {
289 fonts: SlotMap<DefaultKey, Font>,
290 shaping_run_cache: ShapingRunCache<FnvBuildHasher>,
291 shaped_words_cache: ShapedWordsCache<FnvBuildHasher>,
292}
293
294impl Default for TextContextImpl {
295 fn default() -> Self {
296 let fnv_run: BuildHasherDefault = FnvBuildHasher::default();
297 let fnv_words: BuildHasherDefault = FnvBuildHasher::default();
298
299 Self {
300 fonts: Default::default(),
301 shaping_run_cache: LruCache::with_hasher(
302 cap:std::num::NonZeroUsize::new(DEFAULT_LRU_CACHE_CAPACITY).unwrap(),
303 hash_builder:fnv_run,
304 ),
305 shaped_words_cache: LruCache::with_hasher(
306 cap:std::num::NonZeroUsize::new(DEFAULT_LRU_CACHE_CAPACITY).unwrap(),
307 hash_builder:fnv_words,
308 ),
309 }
310 }
311}
312
313impl TextContextImpl {
314 pub fn resize_shaping_run_cache(&mut self, capacity: std::num::NonZeroUsize) {
315 self.shaping_run_cache.resize(capacity);
316 }
317
318 pub fn resize_shaped_words_cache(&mut self, capacity: std::num::NonZeroUsize) {
319 self.shaped_words_cache.resize(capacity);
320 }
321
322 pub fn add_font_dir<T: AsRef<FilePath>>(&mut self, path: T) -> Result<Vec<FontId>, ErrorKind> {
323 let path = path.as_ref();
324 let mut fonts = Vec::new();
325
326 if path.is_dir() {
327 for entry in fs::read_dir(path)? {
328 let entry = entry?;
329 let path = entry.path();
330
331 if path.is_dir() {
332 self.add_font_dir(&path)?;
333 } else if Some("ttf") == path.extension().and_then(OsStr::to_str) {
334 fonts.push(self.add_font_file(path)?);
335 } else if Some("ttc") == path.extension().and_then(OsStr::to_str) {
336 fonts.extend(self.add_font_file_collection(path)?);
337 }
338 }
339 }
340
341 Ok(fonts)
342 }
343
344 pub fn add_font_file<T: AsRef<FilePath>>(&mut self, path: T) -> Result<FontId, ErrorKind> {
345 let data = std::fs::read(path)?;
346
347 self.add_font_mem(&data)
348 }
349
350 pub fn add_font_file_collection<T: AsRef<FilePath>>(
351 &mut self,
352 path: T,
353 ) -> Result<impl Iterator<Item = FontId> + '_, ErrorKind> {
354 let data = std::fs::read(path)?;
355
356 let count = ttf_parser::fonts_in_collection(&data).unwrap_or(1);
357 Ok((0..count).filter_map(move |index| self.add_font_mem_with_index(&data, index).ok()))
358 }
359
360 pub fn add_font_mem(&mut self, data: &[u8]) -> Result<FontId, ErrorKind> {
361 self.add_font_mem_with_index(data, 0)
362 }
363
364 pub fn add_font_mem_with_index(&mut self, data: &[u8], face_index: u32) -> Result<FontId, ErrorKind> {
365 self.clear_caches();
366
367 let data_copy = data.to_owned();
368 let font = Font::new_with_data(data_copy, face_index)?;
369 Ok(FontId(self.fonts.insert(font)))
370 }
371
372 pub fn add_shared_font_with_index<T: AsRef<[u8]> + 'static>(
373 &mut self,
374 data: T,
375 face_index: u32,
376 ) -> Result<FontId, ErrorKind> {
377 self.clear_caches();
378
379 let font = Font::new_with_data(data, face_index)?;
380 Ok(FontId(self.fonts.insert(font)))
381 }
382
383 pub fn font(&self, id: FontId) -> Option<&Font> {
384 self.fonts.get(id.0)
385 }
386
387 pub fn font_mut(&mut self, id: FontId) -> Option<&mut Font> {
388 self.fonts.get_mut(id.0)
389 }
390
391 pub fn find_font<F, T>(&mut self, font_ids: &[Option<FontId>; 8], mut callback: F) -> Result<T, ErrorKind>
392 where
393 F: FnMut((FontId, &mut Font)) -> (bool, T),
394 {
395 // Try each font in the paint
396 for maybe_font_id in font_ids {
397 if let &Some(font_id) = maybe_font_id {
398 if let Some(font) = self.fonts.get_mut(font_id.0) {
399 let (has_missing, result) = callback((font_id, font));
400
401 if !has_missing {
402 return Ok(result);
403 }
404 }
405 } else {
406 break;
407 }
408 }
409
410 // Try each registered font
411 // An optimisation here would be to skip fonts that were tried by the paint
412 for (id, font) in &mut self.fonts {
413 let (has_missing, result) = callback((FontId(id), font));
414
415 if !has_missing {
416 return Ok(result);
417 }
418 }
419
420 // Just return the first font at this point and let it render .nodef glyphs
421 if let Some((id, font)) = self.fonts.iter_mut().next() {
422 return Ok(callback((FontId(id), font)).1);
423 }
424
425 Err(ErrorKind::NoFontFound)
426 }
427
428 fn clear_caches(&mut self) {
429 self.shaped_words_cache.clear();
430 }
431
432 pub fn measure_text<S: AsRef<str>>(
433 &mut self,
434 x: f32,
435 y: f32,
436 text: S,
437 text_settings: &TextSettings,
438 ) -> Result<TextMetrics, ErrorKind> {
439 shape(x, y, self, text_settings, text.as_ref(), None)
440 }
441
442 pub fn break_text<S: AsRef<str>>(
443 &mut self,
444 max_width: f32,
445 text: S,
446 text_settings: &TextSettings,
447 ) -> Result<usize, ErrorKind> {
448 let layout = shape(0.0, 0.0, self, text_settings, text.as_ref(), Some(max_width))?;
449
450 Ok(layout.final_byte_index)
451 }
452
453 pub fn break_text_vec<S: AsRef<str>>(
454 &mut self,
455 max_width: f32,
456 text: S,
457 text_settings: &TextSettings,
458 ) -> Result<Vec<Range<usize>>, ErrorKind> {
459 let text = text.as_ref();
460
461 let mut res = Vec::new();
462 let mut start = 0;
463
464 while start < text.len() {
465 if let Ok(index) = self.break_text(max_width, &text[start..], text_settings) {
466 if index == 0 {
467 break;
468 }
469
470 let index = start + index;
471 res.push(start..index);
472 start += &text[start..index].len();
473 } else {
474 break;
475 }
476 }
477
478 Ok(res)
479 }
480
481 pub fn measure_font(&mut self, font_size: f32, font_ids: &[Option<FontId>; 8]) -> Result<FontMetrics, ErrorKind> {
482 if let Some(Some(id)) = font_ids.first() {
483 if let Some(font) = self.font(*id) {
484 return Ok(font.metrics(font_size));
485 }
486 }
487
488 Err(ErrorKind::NoFontFound)
489 }
490}
491
492/// Represents the result of a text shaping run.
493#[derive(Clone, Default, Debug)]
494pub struct TextMetrics {
495 /// X-coordinate of the starting position for the shaped text.
496 pub x: f32,
497 /// Y-coordinate of the starting position for the shaped text.
498 pub y: f32,
499 width: f32,
500 height: f32,
501 /// Vector of shaped glyphs resulting from the text shaping run.
502 pub glyphs: Vec<ShapedGlyph>,
503 pub(crate) final_byte_index: usize,
504}
505
506impl TextMetrics {
507 pub(crate) fn scale(&mut self, scale: f32) {
508 self.x *= scale;
509 self.y *= scale;
510 self.width *= scale;
511 self.height *= scale;
512
513 for glyph in &mut self.glyphs {
514 glyph.x *= scale;
515 glyph.y *= scale;
516 glyph.width *= scale;
517 glyph.height *= scale;
518 }
519 }
520
521 /// width of the glyphs as drawn
522 pub fn width(&self) -> f32 {
523 self.width
524 }
525
526 /// height of the glyphs as drawn
527 pub fn height(&self) -> f32 {
528 self.height
529 }
530
531 pub(crate) fn has_bitmap_glyphs(&self) -> bool {
532 self.glyphs.iter().any(|g| g.bitmap_glyph)
533 }
534}
535
536// Shaper
537
538pub fn shape(
539 x: f32,
540 y: f32,
541 context: &mut TextContextImpl,
542 text_settings: &TextSettings,
543 text: &str,
544 max_width: Option<f32>,
545) -> Result<TextMetrics, ErrorKind> {
546 let id: ShapingId = ShapingId::new(text_settings.font_size, text_settings.font_ids, word:text, max_width);
547
548 if !context.shaping_run_cache.contains(&id) {
549 let metrics: TextMetrics = shape_run(
550 context,
551 text_settings.font_size,
552 text_settings.font_ids,
553 text_settings.letter_spacing,
554 text,
555 max_width,
556 )?;
557 context.shaping_run_cache.put(k:id, v:metrics);
558 }
559
560 if let Some(mut metrics: TextMetrics) = context.shaping_run_cache.get(&id).cloned() {
561 layout(x, y, context, &mut metrics, text_settings)?;
562
563 return Ok(metrics);
564 }
565
566 Err(ErrorKind::UnknownError)
567}
568
569fn shape_run(
570 context: &mut TextContextImpl,
571 font_size: f32,
572 font_ids: [Option<FontId>; 8],
573 letter_spacing: f32,
574 text: &str,
575 max_width: Option<f32>,
576) -> Result<TextMetrics, ErrorKind> {
577 let mut result = TextMetrics {
578 x: 0.0,
579 y: 0.0,
580 width: 0.0,
581 height: 0.0,
582 glyphs: Vec::with_capacity(text.len()),
583 final_byte_index: 0,
584 };
585
586 let bidi_info = BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
587
588 // this controls whether we should break within words
589 let mut first_word_in_paragraph = true;
590
591 if let Some(paragraph) = bidi_info.paragraphs.first() {
592 let line = paragraph.range.clone();
593
594 let (levels, runs) = bidi_info.visual_runs(paragraph, line);
595
596 for run in runs {
597 let sub_text = &text[run.clone()];
598
599 if sub_text.is_empty() {
600 continue;
601 }
602
603 let hb_direction = if levels[run.start].is_rtl() {
604 rustybuzz::Direction::RightToLeft
605 } else {
606 rustybuzz::Direction::LeftToRight
607 };
608
609 let mut words = Vec::new();
610 let mut word_break_reached = false;
611 let mut byte_index = run.start;
612
613 for mut word_txt in sub_text.split_word_bounds() {
614 let id = ShapingId::new(font_size, font_ids, word_txt, max_width);
615
616 if !context.shaped_words_cache.contains(&id) {
617 let word = shape_word(word_txt, hb_direction, context, font_size, &font_ids, letter_spacing);
618 context.shaped_words_cache.put(id, word);
619 }
620
621 if let Some(Ok(word)) = context.shaped_words_cache.get(&id) {
622 let mut word = word.clone();
623
624 if let Some(max_width) = max_width {
625 if result.width + word.width >= max_width {
626 word_break_reached = true;
627 if first_word_in_paragraph {
628 // search for the largest prefix of the word that can fit
629 let mut bytes_included = 0;
630 let mut subword_width = 0.0;
631 let target_width = max_width - result.width;
632 for glyph in word.glyphs {
633 bytes_included = glyph.byte_index;
634 let glyph_width = glyph.advance_x + letter_spacing;
635
636 // nuance: we want to include the first glyph even if it breaks
637 // the bounds. this is to allow pathologically small bounds to
638 // at least complete rendering
639 if subword_width + glyph_width >= target_width && bytes_included != 0 {
640 break;
641 }
642
643 subword_width += glyph_width;
644 }
645
646 if bytes_included == 0 {
647 // just in case - never mind!
648 break;
649 }
650
651 let subword_txt = &word_txt[..bytes_included];
652 let id = ShapingId::new(font_size, font_ids, subword_txt, Some(max_width));
653 if !context.shaped_words_cache.contains(&id) {
654 let subword = shape_word(
655 subword_txt,
656 hb_direction,
657 context,
658 font_size,
659 &font_ids,
660 letter_spacing,
661 );
662 context.shaped_words_cache.put(id, subword);
663 }
664
665 if let Some(Ok(subword)) = context.shaped_words_cache.get(&id) {
666 // replace the outer variables so we can continue normally
667 word = subword.clone();
668 word_txt = subword_txt;
669 } else {
670 break;
671 }
672 } else if word.glyphs.iter().all(|g| g.c.is_whitespace()) {
673 // the last word we've broken in the middle of is whitespace.
674 // include this word for now, but we will discard its metrics in a moment.
675 } else {
676 // we are not breaking up words - discard this word
677 break;
678 }
679 }
680 }
681
682 // if we have broken in the middle of whitespace, do not include this word in metrics
683 if !word_break_reached || !word.glyphs.iter().all(|g| g.c.is_whitespace()) {
684 result.width += word.width;
685 }
686
687 for glyph in &mut word.glyphs {
688 glyph.byte_index += byte_index;
689 debug_assert!(text.get(glyph.byte_index..).is_some());
690 }
691 words.push(word);
692 first_word_in_paragraph = false;
693 }
694
695 byte_index += word_txt.len();
696
697 if word_break_reached {
698 break;
699 }
700 }
701
702 if levels[run.start].is_rtl() {
703 words.reverse();
704 }
705
706 for word in words {
707 result.glyphs.extend(word.glyphs.clone());
708 }
709
710 result.final_byte_index = byte_index;
711
712 if word_break_reached {
713 break;
714 }
715 }
716 }
717
718 Ok(result)
719}
720
721fn shape_word(
722 word: &str,
723 hb_direction: rustybuzz::Direction,
724 context: &mut TextContextImpl,
725 font_size: f32,
726 font_ids: &[Option<FontId>; 8],
727 letter_spacing: f32,
728) -> Result<ShapedWord, ErrorKind> {
729 // find_font will call the closure with each font matching the provided style
730 // until a font capable of shaping the word is found
731 context.find_font(font_ids, |(font_id, font)| {
732 let face = font.face_ref();
733 // Call harfbuzz
734 let output = {
735 let mut buffer = rustybuzz::UnicodeBuffer::new();
736 buffer.push_str(word);
737 buffer.set_direction(hb_direction);
738
739 rustybuzz::shape(&face, &[], buffer)
740 };
741
742 let positions = output.glyph_positions();
743 let infos = output.glyph_infos();
744
745 let mut shaped_word = ShapedWord {
746 glyphs: Vec::with_capacity(positions.len()),
747 width: 0.0,
748 };
749
750 let mut has_missing = false;
751
752 for (position, (info, c)) in positions.iter().zip(infos.iter().zip(word.chars())) {
753 if info.glyph_id == 0 {
754 has_missing = true;
755 }
756
757 let scale = font.scale(font_size);
758
759 let mut g = ShapedGlyph {
760 x: 0.0,
761 y: 0.0,
762 c,
763 byte_index: info.cluster as usize,
764 font_id,
765 codepoint: info.glyph_id,
766 width: 0.0,
767 height: 0.0,
768 advance_x: position.x_advance as f32 * scale,
769 advance_y: position.y_advance as f32 * scale,
770 offset_x: position.x_offset as f32 * scale,
771 offset_y: position.y_offset as f32 * scale,
772 bearing_x: 0.0,
773 bearing_y: 0.0,
774 bitmap_glyph: false,
775 };
776
777 if let Some(glyph) = font.glyph(&face, info.glyph_id as u16) {
778 g.width = glyph.metrics.width * scale;
779 g.height = glyph.metrics.height * scale;
780 g.bearing_x = glyph.metrics.bearing_x * scale;
781 g.bearing_y = glyph.metrics.bearing_y * scale;
782 g.bitmap_glyph = glyph.path.is_none();
783 }
784
785 shaped_word.width += g.advance_x + letter_spacing;
786 shaped_word.glyphs.push(g);
787 }
788
789 (has_missing, shaped_word)
790 })
791}
792
793// Calculates the x,y coordinates for each glyph based on their advances. Calculates total width and height of the shaped text run
794fn layout(
795 x: f32,
796 y: f32,
797 context: &mut TextContextImpl,
798 res: &mut TextMetrics,
799 text_settings: &TextSettings,
800) -> Result<(), ErrorKind> {
801 let mut cursor_x = x;
802 let mut cursor_y = y;
803
804 // Horizontal alignment
805 match text_settings.text_align {
806 Align::Center => cursor_x -= res.width / 2.0,
807 Align::Right => cursor_x -= res.width,
808 Align::Left => (),
809 }
810
811 res.x = cursor_x;
812
813 let mut min_y = cursor_y;
814 let mut max_y = cursor_y;
815
816 let mut ascender: f32 = 0.;
817 let mut descender: f32 = 0.;
818
819 for glyph in &mut res.glyphs {
820 let font = context.font_mut(glyph.font_id).ok_or(ErrorKind::NoFontFound)?;
821 let metrics = font.metrics(text_settings.font_size);
822 ascender = ascender.max(metrics.ascender());
823 descender = descender.min(metrics.descender());
824 }
825
826 let primary_metrics = context.find_font(&text_settings.font_ids, |(_, font)| {
827 (false, font.metrics(text_settings.font_size))
828 })?;
829 if ascender.abs() < f32::EPSILON {
830 ascender = primary_metrics.ascender();
831 }
832 if descender.abs() < f32::EPSILON {
833 descender = primary_metrics.descender();
834 }
835
836 // Baseline alignment
837 let alignment_offset_y = match text_settings.text_baseline {
838 Baseline::Top => ascender,
839 Baseline::Middle => (ascender + descender) / 2.0,
840 Baseline::Alphabetic => 0.0,
841 Baseline::Bottom => descender,
842 };
843
844 for glyph in &mut res.glyphs {
845 glyph.x = cursor_x + glyph.offset_x + glyph.bearing_x;
846 glyph.y = (cursor_y + alignment_offset_y).round() + glyph.offset_y - glyph.bearing_y;
847
848 min_y = min_y.min(glyph.y);
849 max_y = max_y.max(glyph.y + glyph.height);
850
851 cursor_x += glyph.advance_x + text_settings.letter_spacing;
852 cursor_y += glyph.advance_y;
853 }
854
855 res.y = min_y;
856 res.height = max_y - min_y;
857
858 Ok(())
859}
860
861// Renderer
862
863/// Represents a command to draw an image with a set of quads.
864#[derive(Clone, Debug)]
865pub struct DrawCommand {
866 /// The ID of the image to draw.
867 pub image_id: ImageId,
868 /// The quads defining the positions and texture coordinates for drawing the image.
869 pub quads: Vec<Quad>,
870}
871
872/// Represents a quad with position and texture coordinates.
873#[derive(Copy, Clone, Default, Debug)]
874pub struct Quad {
875 /// X-coordinate of the top-left corner of the quad.
876 pub x0: f32,
877 /// Y-coordinate of the top-left corner of the quad.
878 pub y0: f32,
879 /// U-coordinate (horizontal texture coordinate) of the top-left corner of the quad.
880 pub s0: f32,
881 /// V-coordinate (vertical texture coordinate) of the top-left corner of the quad.
882 pub t0: f32,
883 /// X-coordinate of the bottom-right corner of the quad.
884 pub x1: f32,
885 /// Y-coordinate of the bottom-right corner of the quad.
886 pub y1: f32,
887 /// U-coordinate (horizontal texture coordinate) of the bottom-right corner of the quad.
888 pub s1: f32,
889 /// V-coordinate (vertical texture coordinate) of the bottom-right corner of the quad.
890 pub t1: f32,
891}
892
893/// Represents the drawing commands for glyphs, separated into alpha and color glyphs.
894pub struct GlyphDrawCommands {
895 /// Drawing commands for alpha (opacity) glyphs.
896 pub alpha_glyphs: Vec<DrawCommand>,
897 /// Drawing commands for color glyphs.
898 pub color_glyphs: Vec<DrawCommand>,
899}
900
901#[derive(Default)]
902pub struct GlyphAtlas {
903 pub rendered_glyphs: RefCell<FnvHashMap<RenderedGlyphId, RenderedGlyph>>,
904 pub glyph_textures: RefCell<Vec<FontTexture>>,
905}
906
907impl GlyphAtlas {
908 pub(crate) fn render_atlas<T: Renderer>(
909 &self,
910 canvas: &mut Canvas<T>,
911 text_layout: &TextMetrics,
912 font_size: f32,
913 line_width: f32,
914 mode: RenderMode,
915 ) -> Result<GlyphDrawCommands, ErrorKind> {
916 let mut alpha_cmd_map = FnvHashMap::default();
917 let mut color_cmd_map = FnvHashMap::default();
918
919 let line_width_offset = if mode == RenderMode::Stroke {
920 (line_width / 2.0).ceil()
921 } else {
922 0.0
923 };
924
925 let initial_render_target = canvas.current_render_target;
926
927 for glyph in &text_layout.glyphs {
928 let subpixel_location = crate::geometry::quantize(glyph.x.fract(), 0.1) * 10.0;
929
930 let id = RenderedGlyphId::new(
931 glyph.codepoint,
932 glyph.font_id,
933 font_size,
934 line_width,
935 mode,
936 subpixel_location as u8,
937 );
938
939 if !self.rendered_glyphs.borrow().contains_key(&id) {
940 let glyph = self.render_glyph(canvas, font_size, line_width, mode, glyph)?;
941
942 self.rendered_glyphs.borrow_mut().insert(id, glyph);
943 }
944
945 let rendered_glyphs = self.rendered_glyphs.borrow();
946 let rendered = rendered_glyphs.get(&id).unwrap();
947
948 if let Some(texture) = self.glyph_textures.borrow().get(rendered.texture_index) {
949 let image_id = texture.image_id;
950 let size = texture.atlas.size();
951 let itw = 1.0 / size.0 as f32;
952 let ith = 1.0 / size.1 as f32;
953
954 let cmd_map = if rendered.color_glyph {
955 &mut color_cmd_map
956 } else {
957 &mut alpha_cmd_map
958 };
959
960 let cmd = cmd_map.entry(rendered.texture_index).or_insert_with(|| DrawCommand {
961 image_id,
962 quads: Vec::new(),
963 });
964
965 let mut q = Quad::default();
966
967 let line_width_offset = if rendered.color_glyph { 0. } else { line_width_offset };
968
969 q.x0 = glyph.x.trunc() - line_width_offset - GLYPH_PADDING as f32;
970 q.y0 = (glyph.y + glyph.bearing_y).round()
971 - rendered.bearing_y as f32
972 - line_width_offset
973 - GLYPH_PADDING as f32;
974 q.x1 = q.x0 + rendered.width as f32;
975 q.y1 = q.y0 + rendered.height as f32;
976
977 q.s0 = rendered.atlas_x as f32 * itw;
978 q.t0 = rendered.atlas_y as f32 * ith;
979 q.s1 = (rendered.atlas_x + rendered.width) as f32 * itw;
980 q.t1 = (rendered.atlas_y + rendered.height) as f32 * ith;
981
982 cmd.quads.push(q);
983 }
984 }
985
986 canvas.set_render_target(initial_render_target);
987
988 Ok(GlyphDrawCommands {
989 alpha_glyphs: alpha_cmd_map.drain().map(|(_, cmd)| cmd).collect(),
990 color_glyphs: color_cmd_map.drain().map(|(_, cmd)| cmd).collect(),
991 })
992 }
993
994 fn render_glyph<T: Renderer>(
995 &self,
996 canvas: &mut Canvas<T>,
997 font_size: f32,
998 line_width: f32,
999 mode: RenderMode,
1000 glyph: &ShapedGlyph,
1001 ) -> Result<RenderedGlyph, ErrorKind> {
1002 let padding = GLYPH_PADDING + GLYPH_MARGIN;
1003
1004 let text_context = canvas.text_context.clone();
1005 let mut text_context = text_context.borrow_mut();
1006
1007 let (mut maybe_glyph_representation, scale) = {
1008 let font = text_context.font_mut(glyph.font_id).ok_or(ErrorKind::NoFontFound)?;
1009 let face = font.face_ref();
1010 let scale = font.scale(font_size);
1011
1012 let maybe_glyph_representation =
1013 font.glyph_rendering_representation(&face, glyph.codepoint as u16, font_size as u16);
1014 (maybe_glyph_representation, scale)
1015 };
1016
1017 #[cfg(feature = "image-loading")]
1018 let color_glyph = matches!(maybe_glyph_representation, Some(GlyphRendering::RenderAsImage(..)));
1019 #[cfg(not(feature = "image-loading"))]
1020 let color_glyph = false;
1021
1022 let line_width = if color_glyph || mode != RenderMode::Stroke {
1023 0.0
1024 } else {
1025 line_width
1026 };
1027
1028 let line_width_offset = (line_width / 2.0).ceil();
1029
1030 let width = glyph.width.ceil() as u32 + (line_width_offset * 2.0) as u32 + padding * 2;
1031 let height = glyph.height.ceil() as u32 + (line_width_offset * 2.0) as u32 + padding * 2;
1032
1033 let (dst_index, dst_image_id, (dst_x, dst_y)) =
1034 self.find_texture_or_alloc(canvas, width as usize, height as usize)?;
1035
1036 // render glyph to image
1037 canvas.save();
1038 canvas.reset();
1039
1040 let rendered_bearing_y = glyph.bearing_y.round();
1041 let x_quant = crate::geometry::quantize(glyph.x.fract(), 0.1);
1042 let x = dst_x as f32 - glyph.bearing_x + line_width_offset + padding as f32 + x_quant;
1043 let y = TEXTURE_SIZE as f32 - dst_y as f32 - rendered_bearing_y - line_width_offset - padding as f32;
1044
1045 let rendered_glyph = RenderedGlyph {
1046 width: width - 2 * GLYPH_MARGIN,
1047 height: height - 2 * GLYPH_MARGIN,
1048 bearing_y: rendered_bearing_y as i32,
1049 atlas_x: dst_x as u32 + GLYPH_MARGIN,
1050 atlas_y: dst_y as u32 + GLYPH_MARGIN,
1051 texture_index: dst_index,
1052 color_glyph,
1053 };
1054
1055 match maybe_glyph_representation.as_mut() {
1056 Some(GlyphRendering::RenderAsPath(ref mut path)) => {
1057 canvas.translate(x, y);
1058
1059 canvas.set_render_target(RenderTarget::Image(dst_image_id));
1060 canvas.clear_rect(
1061 dst_x as u32,
1062 TEXTURE_SIZE as u32 - dst_y as u32 - height,
1063 width,
1064 height,
1065 Color::black(),
1066 );
1067 let factor = 1.0 / 8.0;
1068
1069 let mask_color = Color::rgbf(factor, factor, factor);
1070
1071 let mut line_width = line_width;
1072
1073 if mode == RenderMode::Stroke {
1074 line_width /= scale;
1075 }
1076
1077 canvas.global_composite_blend_func(crate::BlendFactor::SrcAlpha, crate::BlendFactor::One);
1078
1079 // 4x
1080 // let points = [
1081 // (-3.0/8.0, 1.0/8.0),
1082 // (1.0/8.0, 3.0/8.0),
1083 // (3.0/8.0, -1.0/8.0),
1084 // (-1.0/8.0, -3.0/8.0),
1085 // ];
1086
1087 // 8x
1088 let points = [
1089 (-7.0 / 16.0, -1.0 / 16.0),
1090 (-1.0 / 16.0, -5.0 / 16.0),
1091 (3.0 / 16.0, -7.0 / 16.0),
1092 (5.0 / 16.0, -3.0 / 16.0),
1093 (7.0 / 16.0, 1.0 / 16.0),
1094 (1.0 / 16.0, 5.0 / 16.0),
1095 (-3.0 / 16.0, 7.0 / 16.0),
1096 (-5.0 / 16.0, 3.0 / 16.0),
1097 ];
1098
1099 for point in &points {
1100 canvas.save();
1101 canvas.translate(point.0, point.1);
1102
1103 canvas.scale(scale, scale);
1104
1105 if mode == RenderMode::Stroke {
1106 canvas.stroke_path_internal(
1107 path,
1108 &PaintFlavor::Color(mask_color),
1109 false,
1110 &StrokeSettings {
1111 line_width,
1112 ..Default::default()
1113 },
1114 );
1115 } else {
1116 canvas.fill_path_internal(path, &PaintFlavor::Color(mask_color), false, FillRule::NonZero);
1117 }
1118
1119 canvas.restore();
1120 }
1121 }
1122 #[cfg(feature = "image-loading")]
1123 Some(GlyphRendering::RenderAsImage(image_buffer)) => {
1124 let target_x = rendered_glyph.atlas_x as usize;
1125 let target_y = rendered_glyph.atlas_y as usize;
1126 let target_width = rendered_glyph.width;
1127 let target_height = rendered_glyph.height;
1128
1129 let image_buffer =
1130 image_buffer.resize(target_width, target_height, image::imageops::FilterType::Nearest);
1131 if let Ok(image) = crate::image::ImageSource::try_from(&image_buffer) {
1132 canvas.update_image(dst_image_id, image, target_x, target_y).unwrap();
1133 }
1134 }
1135 _ => {}
1136 }
1137
1138 canvas.restore();
1139
1140 Ok(rendered_glyph)
1141 }
1142
1143 // Returns (texture index, image id, glyph padding box)
1144 fn find_texture_or_alloc<T: Renderer>(
1145 &self,
1146 canvas: &mut Canvas<T>,
1147 width: usize,
1148 height: usize,
1149 ) -> Result<(usize, ImageId, (usize, usize)), ErrorKind> {
1150 // Find a free location in one of the atlases
1151 let mut texture_search_result = {
1152 let mut glyph_textures = self.glyph_textures.borrow_mut();
1153 let mut textures = glyph_textures.iter_mut().enumerate();
1154 textures.find_map(|(index, texture)| {
1155 texture
1156 .atlas
1157 .add_rect(width, height)
1158 .map(|loc| (index, texture.image_id, loc))
1159 })
1160 };
1161
1162 if texture_search_result.is_none() {
1163 // All atlases are exausted and a new one must be created
1164 let mut atlas = Atlas::new(TEXTURE_SIZE, TEXTURE_SIZE);
1165
1166 let loc = atlas
1167 .add_rect(width, height)
1168 .ok_or(ErrorKind::FontSizeTooLargeForAtlas)?;
1169
1170 // Using PixelFormat::Gray8 works perfectly and takes less VRAM.
1171 // We keep Rgba8 for now because it might be useful for sub-pixel
1172 // anti-aliasing (ClearType®), and the atlas debug display is much
1173 // clearer with different colors. Also, Rgba8 is required for color
1174 // fonts (typically used for emojis).
1175 let info = ImageInfo::new(ImageFlags::NEAREST, atlas.size().0, atlas.size().1, PixelFormat::Rgba8);
1176 let image_id = canvas.images.alloc(&mut canvas.renderer, info)?;
1177
1178 #[cfg(feature = "debug_inspector")]
1179 if cfg!(debug_assertions) {
1180 // Fill the texture with red pixels only in debug builds.
1181 if let Ok(size) = canvas.image_size(image_id) {
1182 // With image-loading we then subsequently support color fonts, where
1183 // the color glyphs are uploaded directly. Since that's immediately and
1184 // the clear_rect() is run much later, it would overwrite any uploaded
1185 // glyphs. So then when for the debug-inspector, use an image to clear.
1186 #[cfg(feature = "image-loading")]
1187 {
1188 use rgb::FromSlice;
1189 let clear_image = image::RgbaImage::from_pixel(
1190 size.0 as u32,
1191 size.1 as u32,
1192 image::Rgba::<u8>([255, 0, 0, 0]),
1193 );
1194 canvas
1195 .update_image(
1196 image_id,
1197 crate::image::ImageSource::from(imgref::Img::new(
1198 clear_image.as_rgba(),
1199 clear_image.width() as usize,
1200 clear_image.height() as usize,
1201 )),
1202 0,
1203 0,
1204 )
1205 .unwrap();
1206 }
1207 #[cfg(not(feature = "image-loading"))]
1208 {
1209 canvas.save();
1210 canvas.reset();
1211 canvas.set_render_target(RenderTarget::Image(image_id));
1212 canvas.clear_rect(
1213 0,
1214 0,
1215 size.0 as u32,
1216 size.1 as u32,
1217 Color::rgb(255, 0, 0), // Shown as white if using Gray8.,
1218 );
1219 canvas.restore();
1220 }
1221 }
1222 }
1223
1224 self.glyph_textures.borrow_mut().push(FontTexture { atlas, image_id });
1225
1226 let index = self.glyph_textures.borrow().len() - 1;
1227 texture_search_result = Some((index, image_id, loc));
1228 }
1229
1230 texture_search_result.ok_or(ErrorKind::UnknownError)
1231 }
1232
1233 pub(crate) fn clear<T: Renderer>(&self, canvas: &mut Canvas<T>) {
1234 let image_ids = std::mem::take(&mut *self.glyph_textures.borrow_mut())
1235 .into_iter()
1236 .map(|font_texture| font_texture.image_id);
1237 image_ids.for_each(|id| canvas.delete_image(id));
1238
1239 self.rendered_glyphs.borrow_mut().clear();
1240 }
1241}
1242
1243pub fn render_direct<T: Renderer>(
1244 canvas: &mut Canvas<T>,
1245 text_layout: &TextMetrics,
1246 paint_flavor: &PaintFlavor,
1247 anti_alias: bool,
1248 stroke: &StrokeSettings,
1249 font_size: f32,
1250 mode: RenderMode,
1251 invscale: f32,
1252) -> Result<(), ErrorKind> {
1253 let text_context = canvas.text_context.clone();
1254 let text_context = text_context.borrow_mut();
1255
1256 let mut face_cache: HashMap<FontId, rustybuzz::Face> = HashMap::default();
1257
1258 for glyph in &text_layout.glyphs {
1259 let (glyph_rendering, scale) = {
1260 let font = text_context.font(glyph.font_id).ok_or(ErrorKind::NoFontFound)?;
1261 let face = face_cache.entry(glyph.font_id).or_insert_with(|| font.face_ref());
1262
1263 let scale = font.scale(font_size);
1264
1265 let Some(glyph_rendering) =
1266 font.glyph_rendering_representation(face, glyph.codepoint as u16, font_size as u16)
1267 else {
1268 continue;
1269 };
1270
1271 (glyph_rendering, scale)
1272 };
1273
1274 canvas.save();
1275
1276 let line_width = match mode {
1277 RenderMode::Fill => stroke.line_width,
1278 RenderMode::Stroke => stroke.line_width / scale,
1279 };
1280
1281 canvas.translate(
1282 (glyph.x - glyph.bearing_x) * invscale,
1283 (glyph.y + glyph.bearing_y) * invscale,
1284 );
1285 canvas.scale(scale * invscale, -scale * invscale);
1286
1287 match glyph_rendering {
1288 GlyphRendering::RenderAsPath(path) => {
1289 if mode == RenderMode::Stroke {
1290 canvas.stroke_path_internal(
1291 path.borrow(),
1292 paint_flavor,
1293 anti_alias,
1294 &StrokeSettings {
1295 line_width,
1296 ..stroke.clone()
1297 },
1298 );
1299 } else {
1300 canvas.fill_path_internal(path.borrow(), paint_flavor, anti_alias, FillRule::NonZero);
1301 }
1302 }
1303 #[cfg(feature = "image-loading")]
1304 GlyphRendering::RenderAsImage(_) => unreachable!(),
1305 }
1306
1307 canvas.restore();
1308 }
1309
1310 Ok(())
1311}
1312