From cf4724932b767e8827aae716a862318b274218f4 Mon Sep 17 00:00:00 2001 From: Lu Litao Date: Sun, 27 Nov 2022 02:45:34 +0800 Subject: [PATCH 1/2] use freetype for epaint text rendering --- Cargo.lock | 28 +---- Cargo.toml | 2 +- crates/epaint/Cargo.toml | 3 +- crates/epaint/src/image.rs | 25 ++-- crates/epaint/src/text/font.rs | 185 +++++++++++++++++++---------- crates/epaint/src/text/fonts.rs | 69 ++++++----- crates/epaint/src/texture_atlas.rs | 13 +- 7 files changed, 190 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4c7b627bd2..94aecc0f22c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,22 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "ab_glyph" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "846ffacb9d0c8b879ef9e565b59e18fb76d6a61013e5bd24ecc659864e6b1a1f" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e" - [[package]] name = "addr2line" version = "0.17.0" @@ -873,6 +857,7 @@ name = "custom_font" version = "0.1.0" dependencies = [ "eframe", + "rfd", ] [[package]] @@ -1364,7 +1349,6 @@ dependencies = [ name = "epaint" version = "0.19.0" dependencies = [ - "ab_glyph", "ahash 0.8.1", "atomic_refcell", "backtrace", @@ -1374,6 +1358,7 @@ dependencies = [ "criterion", "document-features", "emath", + "freetype-rs", "nohash-hasher", "parking_lot", "serde", @@ -2575,15 +2560,6 @@ dependencies = [ "shared_library", ] -[[package]] -name = "owned_ttf_parser" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ef1a404ae479dd6906f4fa2c88b3c94028f1284beb42a47c183a7c27ee9a3e" -dependencies = [ - "ttf-parser", -] - [[package]] name = "pango-sys" version = "0.15.10" diff --git a/Cargo.toml b/Cargo.toml index eed3b6fc310..c01bf34ee57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'` # debug = true # include debug symbols, useful when profiling wasm -[profile.dev] +[profile.dev.'cfg(!windows)'] split-debuginfo = "unpacked" # faster debug builds on mac # opt-level = 1 # Make debug builds run faster diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index dda4127ed82..823f3362764 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -57,7 +57,8 @@ serde = ["dep:serde", "ahash/serde", "emath/serde"] [dependencies] emath = { version = "0.19.0", path = "../emath" } -ab_glyph = "0.2.11" +freetype-rs = "0.26.0" +# ab_glyph = "0.2.11" ahash = { version = "0.8.1", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead "std", diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index 5fd4499b74b..4cfa582359d 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -187,14 +187,14 @@ pub struct FontImage { /// The coverage value. /// /// Often you want to use [`Self::srgba_pixels`] instead. - pub pixels: Vec, + pub pixels: Vec, } impl FontImage { pub fn new(size: [usize; 2]) -> Self { Self { size, - pixels: vec![0.0; size[0] * size[1]], + pixels: vec![Color32::TRANSPARENT; size[0] * size[1]], } } @@ -217,13 +217,14 @@ impl FontImage { &'_ self, gamma: Option, ) -> impl ExactSizeIterator + '_ { - let gamma = gamma.unwrap_or(0.55); // TODO(emilk): this default coverage gamma is a magic constant, chosen by eye. I don't even know why we need it. - self.pixels.iter().map(move |coverage| { - let alpha = coverage.powf(gamma); - // We want to multiply with `vec4(alpha)` in the fragment shader: - let a = fast_round(alpha * 255.0); - Color32::from_rgba_premultiplied(a, a, a, a) - }) + self.pixels.iter().copied() + // let gamma = gamma.unwrap_or(0.55); // TODO(emilk): this default coverage gamma is a magic constant, chosen by eye. I don't even know why we need it. + // self.pixels.iter().map(move |coverage| { + // let alpha = coverage.powf(gamma); + // // We want to multiply with `vec4(alpha)` in the fragment shader: + // let a = fast_round(alpha * 255.0); + // Color32::from_rgba_premultiplied(a, a, a, a) + // }) } /// Clone a sub-region as a new image. @@ -245,10 +246,10 @@ impl FontImage { } impl std::ops::Index<(usize, usize)> for FontImage { - type Output = f32; + type Output = Color32; #[inline] - fn index(&self, (x, y): (usize, usize)) -> &f32 { + fn index(&self, (x, y): (usize, usize)) -> &Color32 { let [w, h] = self.size; assert!(x < w && y < h); &self.pixels[y * w + x] @@ -257,7 +258,7 @@ impl std::ops::Index<(usize, usize)> for FontImage { impl std::ops::IndexMut<(usize, usize)> for FontImage { #[inline] - fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut f32 { + fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut Color32 { let [w, h] = self.size; assert!(x < w && y < h); &mut self.pixels[y * w + x] diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index e93356ab757..8904a9c2bac 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -1,12 +1,13 @@ use crate::{ mutex::{Mutex, RwLock}, - TextureAtlas, + Color32, TextureAtlas, }; use emath::{vec2, Vec2}; use std::collections::BTreeSet; use std::sync::Arc; // ---------------------------------------------------------------------------- +pub type GlyphId = u32; #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -33,8 +34,7 @@ impl UvRect { #[derive(Clone, Copy, Debug, PartialEq)] pub struct GlyphInfo { - pub(crate) id: ab_glyph::GlyphId, - + pub(crate) id: GlyphId, /// Unit: points. pub advance_width: f32, @@ -45,7 +45,7 @@ pub struct GlyphInfo { impl Default for GlyphInfo { fn default() -> Self { Self { - id: ab_glyph::GlyphId(0), + id: 0, advance_width: 0.0, uv_rect: Default::default(), } @@ -58,7 +58,7 @@ impl Default for GlyphInfo { /// The interface uses points as the unit for everything. pub struct FontImpl { name: String, - ab_glyph_font: ab_glyph::FontArc, + freetype_face: freetype::Face, /// Maximum character height scale_in_pixels: u32, height_in_points: f32, @@ -69,12 +69,53 @@ pub struct FontImpl { atlas: Arc>, } +struct CharacterIter<'a> { + freetype_font: &'a freetype::Face, + gindex: u32, + charcode: Option, +} + +impl<'a> CharacterIter<'a> { + pub fn new(freetype_face: &'a freetype::Face) -> Self { + Self { + freetype_font: freetype_face, + gindex: 0, + charcode: None, + } + } +} + +impl Iterator for CharacterIter<'_> { + type Item = char; + + fn next(&mut self) -> Option { + let raw_font = unsafe { std::mem::transmute(self.freetype_font.raw()) }; + match self.charcode { + Some(charcode) => { + self.charcode = Some(unsafe { + freetype::ffi::FT_Get_Next_Char(raw_font, charcode, &mut self.gindex) + }) + } + None => { + self.charcode = + Some(unsafe { freetype::ffi::FT_Get_First_Char(raw_font, &mut self.gindex) }) + } + }; + + match (self.gindex, self.charcode) { + (0, _) => None, + (_, None) => None, + (_, Some(charcode)) => char::from_u32(charcode), + } + } +} + impl FontImpl { pub fn new( atlas: Arc>, pixels_per_point: f32, name: String, - ab_glyph_font: ab_glyph::FontArc, + freetype_face: freetype::Face, scale_in_pixels: u32, y_offset_points: f32, ) -> FontImpl { @@ -95,7 +136,7 @@ impl FontImpl { Self { name, - ab_glyph_font, + freetype_face, scale_in_pixels, height_in_points, y_offset, @@ -126,11 +167,7 @@ impl FontImpl { /// An un-ordered iterator over all supported characters. fn characters(&self) -> impl Iterator + '_ { - use ab_glyph::Font as _; - self.ab_glyph_font - .codepoint_ids() - .map(|(_, chr)| chr) - .filter(|&chr| !self.ignore_character(chr)) + CharacterIter::new(&self.freetype_face) } /// `\n` will result in `None` @@ -174,10 +211,9 @@ impl FontImpl { } // Add new character: - use ab_glyph::Font as _; - let glyph_id = self.ab_glyph_font.glyph_id(c); + let glyph_id = self.freetype_face.get_char_index(c as usize); - if glyph_id.0 == 0 { + if glyph_id == 0 { if invisible_char(c) { // hack let glyph_info = GlyphInfo::default(); @@ -189,7 +225,7 @@ impl FontImpl { } else { let glyph_info = allocate_glyph( &mut self.atlas.lock(), - &self.ab_glyph_font, + &self.freetype_face, glyph_id, self.scale_in_pixels as f32, self.y_offset, @@ -202,16 +238,16 @@ impl FontImpl { } #[inline] - pub fn pair_kerning( - &self, - last_glyph_id: ab_glyph::GlyphId, - glyph_id: ab_glyph::GlyphId, - ) -> f32 { - use ab_glyph::{Font as _, ScaleFont}; - self.ab_glyph_font - .as_scaled(self.scale_in_pixels as f32) - .kern(last_glyph_id, glyph_id) - / self.pixels_per_point + pub fn pair_kerning(&self, last_glyph_id: GlyphId, glyph_id: GlyphId) -> f32 { + let result = self + .freetype_face + .get_kerning( + last_glyph_id, + glyph_id, + freetype::face::KerningMode::KerningUnfitted, + ) + .unwrap(); + result.x as f32 / (1 << 6) as f32 / self.pixels_per_point } /// Height of one row of text. In points @@ -386,51 +422,74 @@ fn invisible_char(c: char) -> bool { fn allocate_glyph( atlas: &mut TextureAtlas, - font: &ab_glyph::FontArc, - glyph_id: ab_glyph::GlyphId, + font: &freetype::Face, + glyph_id: GlyphId, scale_in_pixels: f32, y_offset: f32, pixels_per_point: f32, ) -> GlyphInfo { - assert!(glyph_id.0 != 0); - use ab_glyph::{Font as _, ScaleFont}; - - let glyph = - glyph_id.with_scale_and_position(scale_in_pixels, ab_glyph::Point { x: 0.0, y: 0.0 }); - - let uv_rect = font.outline_glyph(glyph).map(|glyph| { - let bb = glyph.px_bounds(); - let glyph_width = bb.width() as usize; - let glyph_height = bb.height() as usize; - if glyph_width == 0 || glyph_height == 0 { - UvRect::default() - } else { - let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height)); - glyph.draw(|x, y, v| { - if v > 0.0 { - let px = glyph_pos.0 + x as usize; - let py = glyph_pos.1 + y as usize; - image[(px, py)] = v; - } - }); + assert!(glyph_id != 0); + use freetype::face::LoadFlag; + + let mut advance_width_in_points = 0.0; + font.set_pixel_sizes(0, scale_in_pixels as u32).unwrap(); + let uv_rect = || -> Result { + font.load_glyph(glyph_id, LoadFlag::RENDER | LoadFlag::TARGET_LCD)?; + let glyph = font.glyph(); + let bitmap = glyph.bitmap(); + let glyph_width = bitmap.width() / 3; + let glyph_height = bitmap.rows(); + + // freetype-rs's Glyph type will call `FT_Done_Glyph` and `FT_Done_Library` when dropping, so we use a named variable to prevent it from dropping in this scope + let rendered_glyph = glyph.get_glyph()?; + advance_width_in_points = + rendered_glyph.advance_x() as f32 / (1 << 16) as f32 / pixels_per_point; + + if glyph_width == 0 + || glyph_height == 0 + || bitmap.pitch() < 3 + || bitmap.buffer().len() < (bitmap.pitch() as usize * glyph_height as usize) + { + return Ok(UvRect::default()); + } - let offset_in_pixels = vec2(bb.min.x, scale_in_pixels + bb.min.y); - let offset = offset_in_pixels / pixels_per_point + y_offset * Vec2::Y; - UvRect { - offset, - size: vec2(glyph_width as f32, glyph_height as f32) / pixels_per_point, - min: [glyph_pos.0 as u16, glyph_pos.1 as u16], - max: [ - (glyph_pos.0 + glyph_width) as u16, - (glyph_pos.1 + glyph_height) as u16, - ], + let (glyph_pos, image) = atlas.allocate((glyph_width as usize, glyph_height as usize)); + let mut buffer_cursor = 0; + for i in 0..glyph_height { + for j in 0..glyph_width { + let idx = (j * 3 + buffer_cursor) as usize; + let r = bitmap.buffer()[idx]; + let g = bitmap.buffer()[idx + 1]; + let b = bitmap.buffer()[idx + 2]; + let px = glyph_pos.0 + j as usize; + let py = glyph_pos.1 + i as usize; + + // Luminance Y is defined by the CIE 1931 XYZ color space. Linear RGB to Y is a weighted average based on factors from the color conversion matrix: + // Y = 0.2126*R + 0.7152*G + 0.0722*B. Computed on the integer pipe. + let a = (4732 * r as usize + 46871 * g as usize + 13933 * b as usize) >> 16; + image[(px, py)] = Color32::from_rgba_premultiplied(r, g, b, a as u8); } + buffer_cursor += bitmap.pitch(); } - }); - let uv_rect = uv_rect.unwrap_or_default(); - let advance_width_in_points = - font.as_scaled(scale_in_pixels).h_advance(glyph_id) / pixels_per_point; + // Note that bitmap_left is the horizontal distance from the current pen position to the left-most border of the glyph bitmap, while bitmap_top is the vertical distance from the pen position (on the baseline) to the top-most border of the glyph bitmap. It is positive to indicate an upwards distance. + let offset_in_pixels = vec2( + glyph.bitmap_left() as f32, + scale_in_pixels - glyph.bitmap_top() as f32, + ); + + let offset = offset_in_pixels / pixels_per_point + y_offset * Vec2::Y; + Ok(UvRect { + offset, + size: vec2(glyph_width as f32, glyph_height as f32) / pixels_per_point, + min: [glyph_pos.0 as u16, glyph_pos.1 as u16], + max: [ + (glyph_pos.0 + glyph_width as usize) as u16, + (glyph_pos.1 + glyph_height as usize) as u16, + ], + }) + }() + .unwrap_or_default(); GlyphInfo { id: glyph_id, diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 4857213c21f..926c397a409 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -184,18 +184,22 @@ impl Default for FontTweak { // ---------------------------------------------------------------------------- -fn ab_glyph_font_from_font_data(name: &str, data: &FontData) -> ab_glyph::FontArc { - match &data.font { - std::borrow::Cow::Borrowed(bytes) => { - ab_glyph::FontRef::try_from_slice_and_index(bytes, data.index) - .map(ab_glyph::FontArc::from) - } - std::borrow::Cow::Owned(bytes) => { - ab_glyph::FontVec::try_from_vec_and_index(bytes.clone(), data.index) - .map(ab_glyph::FontArc::from) +static mut FREETYPE_LIB: Option = None; + +fn freetype_font_from_font_data(name: &str, data: &FontData) -> freetype::Face { + unsafe { + if FREETYPE_LIB.is_none() { + FREETYPE_LIB = + Some(freetype::Library::init().unwrap_or_else(|err| { + panic!("Failed to initialize freetype library: {:?}", err) + })); } + let lib = FREETYPE_LIB.as_ref().unwrap(); + lib.set_lcd_filter(freetype::LcdFilter::LcdFilterDefault) + .unwrap_or_else(|err| panic!("Failed to set freetype lcdfilter: {}", err)); + lib.new_memory_face(data.font.to_vec(), 0) + .unwrap_or_else(|err| panic!("Error parsing {:?} TTF/OTF font file: {}", name, err)) } - .unwrap_or_else(|err| panic!("Error parsing {:?} TTF/OTF font file: {}", name, err)) } /// Describes the font data and the sizes to use. @@ -710,7 +714,7 @@ impl GalleyCache { struct FontImplCache { atlas: Arc>, pixels_per_point: f32, - ab_glyph_fonts: BTreeMap, + freetype_fonts: BTreeMap, /// Map font pixel sizes and names to the cached [`FontImpl`]. cache: ahash::HashMap<(u32, String), Arc>, @@ -722,28 +726,26 @@ impl FontImplCache { pixels_per_point: f32, font_data: &BTreeMap, ) -> Self { - let ab_glyph_fonts = font_data + let freetype_fonts = font_data .iter() .map(|(name, font_data)| { let tweak = font_data.tweak; - let ab_glyph = ab_glyph_font_from_font_data(name, font_data); - (name.clone(), (tweak, ab_glyph)) + let freetype_font = freetype_font_from_font_data(name, font_data); + (name.clone(), (tweak, freetype_font)) }) .collect(); Self { atlas, pixels_per_point, - ab_glyph_fonts, + freetype_fonts, cache: Default::default(), } } pub fn font_impl(&mut self, scale_in_points: f32, font_name: &str) -> Arc { - use ab_glyph::Font as _; - - let (tweak, ab_glyph_font) = self - .ab_glyph_fonts + let (tweak, freetype_font) = self + .freetype_fonts .get(font_name) .unwrap_or_else(|| panic!("No font data found for {:?}", font_name)) .clone(); @@ -751,13 +753,8 @@ impl FontImplCache { let scale_in_pixels = self.pixels_per_point * scale_in_points; // Scale the font properly (see https://github.com/emilk/egui/issues/2068). - let units_per_em = ab_glyph_font.units_per_em().unwrap_or_else(|| { - panic!( - "The font unit size of {:?} exceeds the expected range (16..=16384)", - font_name - ) - }); - let font_scaling = ab_glyph_font.height_unscaled() / units_per_em; + let units_per_em = freetype_font.em_size(); + let font_scaling = freetype_font.height() as f32 / units_per_em as f32; let scale_in_pixels = scale_in_pixels * font_scaling; // Tweak the scale as the user desired: @@ -779,7 +776,7 @@ impl FontImplCache { self.atlas.clone(), self.pixels_per_point, font_name.to_owned(), - ab_glyph_font, + freetype_font, scale_in_pixels, y_offset_points, )) @@ -787,3 +784,21 @@ impl FontImplCache { .clone() } } + +#[test] +fn test_font_characters() { + let fonts = Fonts::new(1.0, 8 * 1024, FontDefinitions::default()); + let chars: Vec = fonts + .lock() + .fonts + .font(&FontId::new( + 10.0, + FontFamily::Name("emoji-icon-font".into()), + )) + .characters() + .into_iter() + .copied() + .collect(); + + println!("{:#?}", chars); +} diff --git a/crates/epaint/src/texture_atlas.rs b/crates/epaint/src/texture_atlas.rs index d83a0896909..77691f3a415 100644 --- a/crates/epaint/src/texture_atlas.rs +++ b/crates/epaint/src/texture_atlas.rs @@ -1,6 +1,6 @@ use emath::{remap_clamp, Rect}; -use crate::{FontImage, ImageDelta}; +use crate::{Color32, FontImage, ImageDelta}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct Rectu { @@ -89,7 +89,7 @@ impl TextureAtlas { // Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color: let (pos, image) = atlas.allocate((1, 1)); assert_eq!(pos, (0, 0)); - image[pos] = 1.0; + image[pos] = Color32::WHITE; // Allocate a series of anti-aliased discs used to render small filled circles: // TODO(emilk): these circles can be packed A LOT better. @@ -110,9 +110,10 @@ impl TextureAtlas { for dy in -hw..=hw { let distance_to_center = ((dx * dx + dy * dy) as f32).sqrt(); let coverage = - remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 1.0..=0.0); + remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 255.0..=0.0); + let coverage = coverage as u8; image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] = - coverage; + Color32::from_rgba_premultiplied(coverage, coverage, coverage, coverage); } } atlas.discs.push(PrerasterizedDisc { @@ -244,7 +245,9 @@ fn resize_to_min_height(image: &mut FontImage, required_height: usize) -> bool { } if image.width() * image.height() > image.pixels.len() { - image.pixels.resize(image.width() * image.height(), 0.0); + image + .pixels + .resize(image.width() * image.height(), Color32::BLACK); true } else { false From cd55c99920bf80fbd7e9764e6f882f7d48940e1a Mon Sep 17 00:00:00 2001 From: Lu Litao Date: Mon, 28 Nov 2022 20:54:39 +0800 Subject: [PATCH 2/2] fix FT_ULong compilation error --- crates/epaint/src/text/font.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 8904a9c2bac..7af165b71af 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -72,7 +72,7 @@ pub struct FontImpl { struct CharacterIter<'a> { freetype_font: &'a freetype::Face, gindex: u32, - charcode: Option, + charcode: Option, } impl<'a> CharacterIter<'a> { @@ -93,19 +93,28 @@ impl Iterator for CharacterIter<'_> { match self.charcode { Some(charcode) => { self.charcode = Some(unsafe { - freetype::ffi::FT_Get_Next_Char(raw_font, charcode, &mut self.gindex) + freetype::ffi::FT_Get_Next_Char( + raw_font, + charcode as freetype::ffi::FT_ULong, + &mut self.gindex, + ) + .try_into() + .unwrap() }) } None => { - self.charcode = - Some(unsafe { freetype::ffi::FT_Get_First_Char(raw_font, &mut self.gindex) }) + self.charcode = Some( + unsafe { freetype::ffi::FT_Get_First_Char(raw_font, &mut self.gindex) } + .try_into() + .unwrap(), + ) } }; match (self.gindex, self.charcode) { (0, _) => None, (_, None) => None, - (_, Some(charcode)) => char::from_u32(charcode), + (_, Some(charcode)) => Some(charcode), } } }