oxedyne/fe2o3/fe2o3_font/src/face.rs
11.0 KiB, 99 runs
created by r1870400018:35694, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! One typeface: parse, coverage, metrics, shaping and glyph outlines. |
| 2 | //! |
| 3 | //! Where `harfrust` shapes and `skrifa` draws, both turned back into this crate's own types at once. |
| 4 | //! A face is rarely used alone; what a caller draws with is a [`Font`](crate::font::Font), a chain of |
| 5 | //! these. |
| 6 | |
| 7 | use crate::shape::{ |
| 8 | Dir, |
| 9 | Feature, |
| 10 | Glyph, |
| 11 | Run, |
| 12 | }; |
| 13 | |
| 14 | use oxedyne_fe2o3_core::prelude::*; |
| 15 | use oxedyne_fe2o3_graphics::prelude::*; |
| 16 | use oxedyne_fe2o3_graphics::pdf_font::FontProgram; |
| 17 | |
| 18 | use harfrust::{ |
| 19 | Feature as ShapeFeature, |
| 20 | FontRef as ShapeFont, |
| 21 | ShapeOptions, |
| 22 | ShaperData, |
| 23 | UnicodeBuffer, |
| 24 | }; |
| 25 | |
| 26 | use skrifa::{ |
| 27 | instance::{ |
| 28 | LocationRef, |
| 29 | Size, |
| 30 | }, |
| 31 | outline::{ |
| 32 | DrawSettings, |
| 33 | OutlinePen, |
| 34 | }, |
| 35 | attribute::Style, |
| 36 | string::StringId, |
| 37 | FontRef as OutlineFont, |
| 38 | GlyphId, |
| 39 | MetadataProvider, |
| 40 | }; |
| 41 | |
| 42 | use std::collections::HashMap; |
| 43 | use std::collections::HashSet; |
| 44 | use std::sync::{ |
| 45 | Arc, |
| 46 | RwLock, |
| 47 | }; |
| 48 | |
| 49 | /// The part a font plays. A document names a role; the reader's font set decides what it looks like. |
| 50 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
| 51 | pub enum Role { |
| 52 | #[default] |
| 53 | Body, // running text |
| 54 | Bold, // running text, emphasised strongly |
| 55 | Italic, // running text, emphasised |
| 56 | BoldItalic, // running text, emphasised, and strongly |
| 57 | Mono, // preserved source, where the columns must line up |
| 58 | } |
| 59 | |
| 60 | /// The vertical metrics of a font at a size, in pixels. |
| 61 | #[derive(Clone, Copy, Debug, PartialEq)] |
| 62 | pub struct Metrics { |
| 63 | pub ascent: f32, // how far the tallest letters rise above the baseline |
| 64 | pub descent: f32, // how far the deepest fall below it, as a positive number |
| 65 | pub leading: f32, // the gap the designer asks between one line's descent and the next's ascent |
| 66 | } |
| 67 | |
| 68 | impl Metrics { |
| 69 | |
| 70 | /// The distance from one baseline to the next. |
| 71 | pub fn line_height(&self) -> f32 { |
| 72 | self.ascent + self.descent + self.leading |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | /// What a font file says about itself: the family it belongs to and where in that family it sits. This |
| 77 | /// is what a document's `font: "Name"` is matched against, so a face is found by the name its designer |
| 78 | /// gave it rather than by whatever its file happens to be called. |
| 79 | #[derive(Clone, Debug, PartialEq, Eq)] |
| 80 | pub struct FaceInfo { |
| 81 | pub family: String, // the typographic family (name ID 16), else the legacy family (name ID 1) |
| 82 | pub weight: u16, // OS/2 weight class, 100-900; 400 regular, 700 bold |
| 83 | pub italic: bool, // italic or oblique |
| 84 | } |
| 85 | |
| 86 | impl FaceInfo { |
| 87 | |
| 88 | /// Reads a font file's family, weight and slant without building a shaper, so a directory of fonts |
| 89 | /// can be indexed cheaply before any of them is needed. |
| 90 | pub fn read(bytes: &[u8]) -> Outcome<Self> { |
| 91 | let of = match OutlineFont::new(bytes) { |
| 92 | Ok(f) => f, |
| 93 | Err(e) => return Err(err!( |
| 94 | "The {} bytes given are not a font whose names can be read: {:?}.", bytes.len(), e; |
| 95 | Invalid, Input)), |
| 96 | }; |
| 97 | // The typographic family groups every weight and width under one name ("Noto Sans"), where the |
| 98 | // legacy family splits them four to a family ("Noto Sans SemiBold"); prefer it where present. |
| 99 | let family = of.localized_strings(StringId::TYPOGRAPHIC_FAMILY_NAME).english_or_first() |
| 100 | .or_else(|| of.localized_strings(StringId::FAMILY_NAME).english_or_first()) |
| 101 | .map(|s| s.to_string()); |
| 102 | let family = match family { |
| 103 | Some(f) if !f.trim().is_empty() => f.trim().to_string(), |
| 104 | _ => return Err(err!( |
| 105 | "The font of {} bytes names no family in its name table.", bytes.len(); |
| 106 | Invalid, Input, Missing)), |
| 107 | }; |
| 108 | let attrs = of.attributes(); |
| 109 | Ok(Self { |
| 110 | family, |
| 111 | weight: attrs.weight.value().round().clamp(1.0, 1000.0) as u16, |
| 112 | italic: !matches!(attrs.style, Style::Normal), |
| 113 | }) |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | /// One typeface, at any size: a single font file. Its bytes are owned and lent to both third-party |
| 118 | /// parsers when needed; the shaper's tables, the costly part to build, are cached. |
| 119 | pub struct Face { |
| 120 | bytes: Arc<Vec<u8>>, // the font file, shared with its embeddable program |
| 121 | program: Option<Arc<FontProgram>>, // the file as a PDF embeds it; `None` when it cannot be |
| 122 | shaper: ShaperData, // the shaper's cached view, built once |
| 123 | upem: f32, // font units per em, what every measurement in the file is in terms of |
| 124 | covers: HashSet<u32>, // every character the face can draw, read once (asked per character) |
| 125 | // Drawn glyph outlines, memoised by (glyph id, size in its raw bits). A book draws the same few |
| 126 | // hundred glyphs at the same few sizes hundreds of thousands of times; re-reading the font and |
| 127 | // redrawing each outline every time was the whole cost of emit. The outline is a pure function of |
| 128 | // its key, so the cache changes nothing in the bytes drawn -- only how many times they are computed. |
| 129 | outlines: RwLock<HashMap<(u32, u32), Path>>, |
| 130 | } |
| 131 | |
| 132 | impl Face { |
| 133 | |
| 134 | pub fn new(bytes: Vec<u8>) -> Outcome<Self> { |
| 135 | let sf = match ShapeFont::new(&bytes) { |
| 136 | Ok(f) => f, |
| 137 | Err(e) => return Err(err!( |
| 138 | "The {} bytes given are not a font a shaper can read: {:?}.", bytes.len(), e; |
| 139 | Invalid, Input)), |
| 140 | }; |
| 141 | let shaper = ShaperData::new(&sf); |
| 142 | let of = match OutlineFont::new(&bytes) { |
| 143 | Ok(f) => f, |
| 144 | Err(e) => return Err(err!( |
| 145 | "The {} bytes given are not a font an outline reader can read: {:?}.", |
| 146 | bytes.len(), e; |
| 147 | Invalid, Input)), |
| 148 | }; |
| 149 | let upem = of.metrics(Size::unscaled(), LocationRef::default()).units_per_em as f32; |
| 150 | if upem <= 0.0 { |
| 151 | return Err(err!( |
| 152 | "The font declares {} units per em, which cannot be scaled by.", upem; |
| 153 | Invalid, Input)); |
| 154 | } |
| 155 | let covers: HashSet<u32> = of.charmap().mappings().map(|(c, _)| c).collect(); |
| 156 | drop(of); |
| 157 | drop(sf); |
| 158 | let bytes = Arc::new(bytes); |
| 159 | // A file the embedding reader cannot follow is still a face to shape and outline; it is drawn as |
| 160 | // outlines in a PDF rather than embedded, so the failure is not the caller's. |
| 161 | let program = FontProgram::parse(bytes.clone()).ok().flatten().map(Arc::new); |
| 162 | Ok(Self { |
| 163 | bytes, |
| 164 | program, |
| 165 | shaper, |
| 166 | upem, |
| 167 | covers, |
| 168 | outlines: RwLock::new(HashMap::new()), |
| 169 | }) |
| 170 | } |
| 171 | |
| 172 | /// The face's file as a PDF embeds it, or `None` for a face that cannot be embedded -- a variable |
| 173 | /// `CFF2` face, or one whose licence forbids it -- and must be drawn as outlines. |
| 174 | pub fn program(&self) -> Option<&Arc<FontProgram>> { |
| 175 | self.program.as_ref() |
| 176 | } |
| 177 | |
| 178 | /// The family, weight and slant the file declares. |
| 179 | pub fn info(&self) -> Outcome<FaceInfo> { |
| 180 | FaceInfo::read(&self.bytes) |
| 181 | } |
| 182 | |
| 183 | /// Can the face draw this character? |
| 184 | pub fn covers(&self, ch: char) -> bool { |
| 185 | self.covers.contains(&(ch as u32)) |
| 186 | } |
| 187 | |
| 188 | /// The font as the shaper reads it. |
| 189 | fn shape_font(&self) -> Outcome<ShapeFont<'_>> { |
| 190 | match ShapeFont::new(&self.bytes[..]) { |
| 191 | Ok(f) => Ok(f), |
| 192 | Err(e) => Err(err!("The font could not be re-read for shaping: {:?}.", e; Bug)), |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | /// The font as the outline reader reads it. |
| 197 | fn outline_font(&self) -> Outcome<OutlineFont<'_>> { |
| 198 | match OutlineFont::new(&self.bytes[..]) { |
| 199 | Ok(f) => Ok(f), |
| 200 | Err(e) => Err(err!("The font could not be re-read for outlines: {:?}.", e; Bug)), |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | /// The vertical metrics at a size, in pixels. |
| 205 | pub fn metrics(&self, size: f32) -> Outcome<Metrics> { |
| 206 | let of = res!(self.outline_font()); |
| 207 | let m = of.metrics(Size::new(size), LocationRef::default()); |
| 208 | Ok(Metrics { |
| 209 | ascent: m.ascent, |
| 210 | descent: m.descent.abs(), |
| 211 | leading: m.leading.max(0.0), |
| 212 | }) |
| 213 | } |
| 214 | |
| 215 | /// Shapes a string this face can draw the whole of: the glyphs it becomes, and where each sits. |
| 216 | /// `face` is which face in the chain this is, carried on every glyph so painting knows whose |
| 217 | /// outline to ask for; `at` is the string's byte offset in the one it was cut from, added to each |
| 218 | /// cluster so a caret reads offsets into the original text rather than into this fragment. |
| 219 | pub fn shape(&self, text: &str, size: f32, dir: Dir, face: u8, at: usize) -> Outcome<Run> { |
| 220 | self.shape_with(text, size, dir, face, at, &[]) |
| 221 | } |
| 222 | |
| 223 | /// As [`Face::shape`], with OpenType features applied across the whole string. |
| 224 | pub fn shape_with( |
| 225 | &self, |
| 226 | text: &str, |
| 227 | size: f32, |
| 228 | dir: Dir, |
| 229 | face: u8, |
| 230 | at: usize, |
| 231 | features: &[Feature], |
| 232 | ) |
| 233 | -> Outcome<Run> |
| 234 | { |
| 235 | if text.is_empty() { |
| 236 | return Ok(Run { |
| 237 | glyphs: Vec::new(), |
| 238 | advance: 0.0, |
| 239 | size, |
| 240 | }); |
| 241 | } |
| 242 | let sf = res!(self.shape_font()); |
| 243 | let shaper = self.shaper.shaper(&sf).build(); |
| 244 | |
| 245 | let mut buf = UnicodeBuffer::new(); |
| 246 | buf.push_str(text); |
| 247 | buf.set_direction(match dir { |
| 248 | Dir::Ltr => harfrust::Direction::LeftToRight, |
| 249 | Dir::Rtl => harfrust::Direction::RightToLeft, |
| 250 | }); |
| 251 | buf.guess_segment_properties(); |
| 252 | |
| 253 | let feats: Vec<ShapeFeature> = features.iter() |
| 254 | .map(|f| ShapeFeature::new(harfrust::Tag::new(&f.tag), f.value, ..)) |
| 255 | .collect(); |
| 256 | let out = shaper.shape(buf, ShapeOptions::new().features(&feats)); |
| 257 | let infos = out.glyph_infos(); |
| 258 | let posns = out.glyph_positions(); |
| 259 | |
| 260 | // Font units become pixels here, and nowhere else. |
| 261 | let scale = size / self.upem; |
| 262 | let mut glyphs = Vec::with_capacity(infos.len()); |
| 263 | let mut pen = 0.0f32; |
| 264 | for (i, p) in infos.iter().zip(posns.iter()) { |
| 265 | let adv = (p.x_advance as f32) * scale; |
| 266 | glyphs.push(Glyph { |
| 267 | id: i.glyph_id, |
| 268 | face, |
| 269 | x: pen + (p.x_offset as f32) * scale, |
| 270 | y: (p.y_offset as f32) * scale, |
| 271 | adv, |
| 272 | cluster: (i.cluster as usize) + at, |
| 273 | }); |
| 274 | pen += adv; |
| 275 | } |
| 276 | Ok(Run { |
| 277 | glyphs, |
| 278 | advance: pen, |
| 279 | size, |
| 280 | }) |
| 281 | } |
| 282 | |
| 283 | /// The outline of one glyph at a size, in the font's frame: origin the glyph's own, y up. Painting |
| 284 | /// flips it onto the page. |
| 285 | pub fn outline(&self, id: u32, size: f32) -> Outcome<Path> { |
| 286 | // The same glyph at the same size is drawn again and again across a book; memoise it. The key is |
| 287 | // the size's raw bits, so two calls at the identical `f32` share an entry and a re-shaped run at a |
| 288 | // new size (a heading, a footnote) gets its own -- no float is compared for near-equality. |
| 289 | let key = (id, size.to_bits()); |
| 290 | { |
| 291 | let cache = lock_read!(self.outlines); |
| 292 | if let Some(path) = cache.get(&key) { |
| 293 | return Ok(path.clone()); |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | let of = res!(self.outline_font()); |
| 298 | let glyphs = of.outline_glyphs(); |
| 299 | let glyph = match glyphs.get(GlyphId::new(id)) { |
| 300 | Some(g) => g, |
| 301 | None => return Err(err!( |
| 302 | "The font holds no glyph {}, which shaping asked for.", id; Invalid, Input)), |
| 303 | }; |
| 304 | let mut pen = Pen::new(); |
| 305 | let settings = DrawSettings::unhinted(Size::new(size), LocationRef::default()); |
| 306 | if let Err(e) = glyph.draw(settings, &mut pen) { |
| 307 | return Err(err!("The outline of glyph {} could not be drawn: {:?}.", id, e; Invalid)); |
| 308 | } |
| 309 | let path = res!(pen.finish()); |
| 310 | let mut cache = lock_write!(self.outlines); |
| 311 | cache.insert(key, path.clone()); |
| 312 | Ok(path) |
| 313 | } |
| 314 | } |
| 315 | |
| 316 | /// Turns the outline reader's calls into one of our paths. |
| 317 | struct Pen { |
| 318 | pb: PathBuilder, |
| 319 | } |
| 320 | |
| 321 | impl Pen { |
| 322 | |
| 323 | fn new() -> Self { |
| 324 | Self { |
| 325 | pb: PathBuilder::new(), |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | fn finish(self) -> Outcome<Path> { |
| 330 | self.pb.finish() |
| 331 | } |
| 332 | } |
| 333 | |
| 334 | impl OutlinePen for Pen { |
| 335 | |
| 336 | fn move_to(&mut self, x: f32, y: f32) { |
| 337 | self.pb.move_to(Pt::new(x, y)); |
| 338 | } |
| 339 | |
| 340 | fn line_to(&mut self, x: f32, y: f32) { |
| 341 | self.pb.line_to(Pt::new(x, y)); |
| 342 | } |
| 343 | |
| 344 | fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { |
| 345 | self.pb.quad_to(Pt::new(cx, cy), Pt::new(x, y)); |
| 346 | } |
| 347 | |
| 348 | fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { |
| 349 | self.pb.cubic_to(Pt::new(cx0, cy0), Pt::new(cx1, cy1), Pt::new(x, y)); |
| 350 | } |
| 351 | |
| 352 | fn close(&mut self) { |
| 353 | self.pb.close(); |
| 354 | } |
| 355 | } |