oxedyne/fe2o3/fe2o3_austenite/src/font.rs
9.8 KiB, 51 runs
created by r1870400018:35732, 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 | //! Real font metrics and shaped text, over `fe2o3_font`. |
| 2 | //! |
| 3 | //! `fe2o3_font` works in device pixels, which at the SVG boundary are points (the media box is whole |
| 4 | //! points). A text size is therefore a length in points carried as an `f32`, and the shaper's |
| 5 | //! advances and the face's metrics come back in points, converted to [`Sp`] at that boundary. |
| 6 | |
| 7 | use crate::ir::{ |
| 8 | Dims, |
| 9 | Metrics, |
| 10 | Sp, |
| 11 | }; |
| 12 | |
| 13 | use oxedyne_fe2o3_core::prelude::*; |
| 14 | use oxedyne_fe2o3_font::{ |
| 15 | face::Role, |
| 16 | font::Font, |
| 17 | set::FontSet, |
| 18 | shape::{ |
| 19 | Dir, |
| 20 | Feature, |
| 21 | Glyph, |
| 22 | Run, |
| 23 | }, |
| 24 | }; |
| 25 | use oxedyne_fe2o3_graphics::colour::Rgba; |
| 26 | use oxedyne_fe2o3_graphics::path::Path; |
| 27 | use oxedyne_fe2o3_graphics::transform::Transform; |
| 28 | |
| 29 | use std::sync::Arc; |
| 30 | |
| 31 | /// Measures text by shaping it and summing advances; height and depth are the face's own. The |
| 32 | /// Phase 1 replacement for [`StubMetrics`](crate::ir::StubMetrics), which stays for running without |
| 33 | /// a font. |
| 34 | #[derive(Clone)] |
| 35 | pub struct FontMetrics { |
| 36 | fonts: Arc<FontSet>, |
| 37 | role: Role, |
| 38 | dir: Dir, |
| 39 | size: f32, // device points, the shaper's pixel |
| 40 | } |
| 41 | |
| 42 | impl FontMetrics { |
| 43 | pub fn new(fonts: Arc<FontSet>, role: Role, dir: Dir, size: Sp) -> Self { |
| 44 | Self { fonts, role, dir, size: size.to_pt() as f32 } |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | impl Metrics for FontMetrics { |
| 49 | fn measure(&self, text: &str) -> Outcome<Dims> { |
| 50 | let shaped = res!(ShapedText::shape(self.fonts.clone(), self.role, self.dir, self.size, text)); |
| 51 | Ok(shaped.dims()) |
| 52 | } |
| 53 | |
| 54 | fn shape(&self, text: &str) -> Outcome<Option<ShapedText>> { |
| 55 | Ok(Some(res!(ShapedText::shape(self.fonts.clone(), self.role, self.dir, self.size, text)))) |
| 56 | } |
| 57 | |
| 58 | fn shape_bold(&self, text: &str) -> Outcome<Option<ShapedText>> { |
| 59 | Ok(Some(res!(ShapedText::shape(self.fonts.clone(), Role::Bold, self.dir, self.size, text)))) |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | /// Where a shaped run draws its glyphs from: a role in the reader's set, or a standalone font handed |
| 64 | /// in outside the set -- the maths font, which is not one of the reading roles. Both are shared (`Arc`) |
| 65 | /// because a run is cloned into the page frame and outlives the composition that placed it. |
| 66 | #[derive(Clone)] |
| 67 | enum Source { |
| 68 | Set { fonts: Arc<FontSet>, role: Role }, |
| 69 | Solo { font: Arc<Font> }, |
| 70 | } |
| 71 | |
| 72 | impl Source { |
| 73 | fn font(&self) -> &Font { |
| 74 | match self { |
| 75 | Source::Set { fonts, role } => fonts.get(*role), |
| 76 | Source::Solo { font } => font, |
| 77 | } |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | /// A shaped run and the font handle to draw it, carried by a [`LeafKind::Text`](crate::ir::LeafKind) |
| 82 | /// so the emitter can outline each glyph. |
| 83 | #[derive(Clone)] |
| 84 | pub struct ShapedText { |
| 85 | src: Source, |
| 86 | size: f32, // device points, the shaper's pixel and the outline's size |
| 87 | run: Run, |
| 88 | dims: Dims, |
| 89 | text: String, // the shaped source string, so a glyph's cluster recovers its source scalar(s) |
| 90 | colour: Rgba, // the fill the glyphs draw in; black unless a `#set text(fill:)` sets it |
| 91 | } |
| 92 | |
| 93 | impl ShapedText { |
| 94 | /// Shapes `text` in `role` at `size` points, keeping the run and the handle to draw it. |
| 95 | pub fn new( |
| 96 | fonts: Arc<FontSet>, |
| 97 | role: Role, |
| 98 | dir: Dir, |
| 99 | size: Sp, |
| 100 | text: &str, |
| 101 | ) |
| 102 | -> Outcome<Self> |
| 103 | { |
| 104 | Self::shape(fonts, role, dir, size.to_pt() as f32, text) |
| 105 | } |
| 106 | |
| 107 | /// As [`ShapedText::new`], with OpenType features applied across the run -- `smcp` for a |
| 108 | /// `#smallcaps[...]` run. |
| 109 | pub fn new_with_features( |
| 110 | fonts: Arc<FontSet>, |
| 111 | role: Role, |
| 112 | dir: Dir, |
| 113 | size: Sp, |
| 114 | text: &str, |
| 115 | features: &[Feature], |
| 116 | ) |
| 117 | -> Outcome<Self> |
| 118 | { |
| 119 | Self::from_source_with(Source::Set { fonts, role }, dir, size.to_pt() as f32, text, features) |
| 120 | } |
| 121 | |
| 122 | /// Shapes in the shaper's own unit, shared by measurement and placement. |
| 123 | fn shape( |
| 124 | fonts: Arc<FontSet>, |
| 125 | role: Role, |
| 126 | dir: Dir, |
| 127 | size: f32, |
| 128 | text: &str, |
| 129 | ) |
| 130 | -> Outcome<Self> |
| 131 | { |
| 132 | Self::from_source(Source::Set { fonts, role }, dir, size, text) |
| 133 | } |
| 134 | |
| 135 | /// Shapes in a standalone font outside the reading set -- the maths font, whose glyphs a role's |
| 136 | /// chain does not carry. The run seats and outlines against that same font. |
| 137 | pub fn new_with_font( |
| 138 | font: Arc<Font>, |
| 139 | dir: Dir, |
| 140 | size: Sp, |
| 141 | text: &str, |
| 142 | ) |
| 143 | -> Outcome<Self> |
| 144 | { |
| 145 | Self::from_source(Source::Solo { font }, dir, size.to_pt() as f32, text) |
| 146 | } |
| 147 | |
| 148 | fn from_source(src: Source, dir: Dir, size: f32, text: &str) -> Outcome<Self> { |
| 149 | Self::from_source_with(src, dir, size, text, &[]) |
| 150 | } |
| 151 | |
| 152 | fn from_source_with(src: Source, dir: Dir, size: f32, text: &str, features: &[Feature]) -> Outcome<Self> { |
| 153 | let font = src.font(); |
| 154 | let run = res!(font.shape_with(text, size, dir, features)); |
| 155 | let vm = res!(font.metrics(size)); |
| 156 | let dims = Dims::new( |
| 157 | Sp::from_pt(run.advance as f64), // the width the shaper's advances sum to |
| 158 | Sp::from_pt(vm.ascent as f64), // height above the baseline |
| 159 | Sp::from_pt(vm.descent as f64), // depth below it |
| 160 | ); |
| 161 | Ok(Self { src, size, run, dims, text: text.to_string(), colour: Rgba::BLACK }) |
| 162 | } |
| 163 | |
| 164 | /// The same run set to draw in `colour`. Consumes and returns `self` so a caller can colour a shaped |
| 165 | /// box inline without a second binding -- the line breaker paints every prose leaf this way. |
| 166 | pub fn with_colour(mut self, colour: Rgba) -> Self { |
| 167 | self.colour = colour; |
| 168 | self |
| 169 | } |
| 170 | |
| 171 | /// The fill the glyphs draw in, black unless a `#set text(fill:)` set it. |
| 172 | pub fn colour(&self) -> Rgba { self.colour } |
| 173 | |
| 174 | pub fn dims(&self) -> Dims { self.dims } |
| 175 | pub fn run(&self) -> &Run { &self.run } |
| 176 | |
| 177 | /// The shaped source string, whose byte offsets a glyph's [`cluster`](Glyph::cluster) indexes. |
| 178 | pub fn source(&self) -> &str { &self.text } |
| 179 | |
| 180 | /// The family, weight and slant of the face heading the run's font chain: the face the run was set in |
| 181 | /// wherever that face covered its characters. |
| 182 | pub fn face_info(&self) -> Outcome<oxedyne_fe2o3_font::face::FaceInfo> { |
| 183 | self.src.font().info() |
| 184 | } |
| 185 | |
| 186 | /// The size the run was shaped at, in device points. |
| 187 | pub fn size(&self) -> f32 { self.size } |
| 188 | |
| 189 | /// Folds this run's content into a page-emit key: its size, its fill, its source string and every |
| 190 | /// glyph's identity and placed position. This is what the page memo hashes to decide a body frame is |
| 191 | /// unchanged. The theme is not folded in as such -- it never needs to be, because it has already |
| 192 | /// decided these very glyph ids, their positions and the fill, so two runs that hash alike here draw |
| 193 | /// identically whatever theme produced them. |
| 194 | pub fn hash_into(&self, h: &mut crate::memo::Fnv) { |
| 195 | h.write_f32(self.size); |
| 196 | h.write(&[self.colour.r, self.colour.g, self.colour.b, self.colour.a]); |
| 197 | h.write_str(&self.text); |
| 198 | h.write_u64(self.run.glyphs.len() as u64); |
| 199 | for g in &self.run.glyphs { |
| 200 | h.write_u32(g.id); |
| 201 | h.write_u8(g.face); |
| 202 | h.write_f32(g.x); |
| 203 | h.write_f32(g.y); |
| 204 | h.write_f32(g.adv); |
| 205 | h.write_usize(g.cluster); |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | /// One glyph's outline, in the font frame (origin at the glyph, y up); empty for a space. |
| 210 | pub fn outline(&self, glyph: &Glyph) -> Outcome<Path> { |
| 211 | self.src.font().outline(glyph.face, glyph.id, self.size) |
| 212 | } |
| 213 | |
| 214 | /// The embeddable program of the face that shaped `glyph`, or `None` when that face must be drawn as |
| 215 | /// outlines instead. |
| 216 | pub fn program(&self, glyph: &Glyph) -> Outcome<Option<Arc<oxedyne_fe2o3_graphics::pdf_font::FontProgram>>> { |
| 217 | Ok(res!(self.src.font().face(glyph.face)).program().cloned()) |
| 218 | } |
| 219 | |
| 220 | /// One glyph's outline at a canonical thousand units per em, in the font frame (origin at the glyph, y |
| 221 | /// up); empty for a space. The PDF writer stores this once and shows it at any point size, so the same |
| 222 | /// glyph in body and in a heading shares a single stored outline. |
| 223 | pub fn outline_canonical(&self, glyph: &Glyph) -> Outcome<Path> { |
| 224 | self.src.font().outline(glyph.face, glyph.id, 1000.0) |
| 225 | } |
| 226 | |
| 227 | /// The source text each glyph stands for, one entry per glyph in [`run`](Self::run)'s order: from |
| 228 | /// its cluster to the next cluster boundary, given only to the first inked glyph at that cluster so |
| 229 | /// a decomposed mark does not repeat the character its base already carries (a later glyph at the |
| 230 | /// same cluster gets the empty string). Both the PDF writer's `/ToUnicode` CMap and the SVG writer's |
| 231 | /// selectable text layer need exactly this word-to-glyph correspondence, so it is derived once here |
| 232 | /// rather than twice: the two writers must never disagree about what a glyph stands for. |
| 233 | pub fn glyph_text(&self) -> Vec<String> { |
| 234 | let src = &self.text; |
| 235 | let mut bounds: Vec<usize> = self.run.glyphs.iter().map(|g| g.cluster).collect(); |
| 236 | bounds.sort_unstable(); |
| 237 | bounds.dedup(); |
| 238 | let mut claimed: std::collections::HashSet<usize> = std::collections::HashSet::new(); |
| 239 | self.run.glyphs.iter().map(|glyph| { |
| 240 | if claimed.insert(glyph.cluster) { |
| 241 | let start = glyph.cluster; |
| 242 | let end = bounds.iter().copied().find(|&b| b > start).unwrap_or(src.len()); |
| 243 | src.get(start..end).unwrap_or("").to_string() |
| 244 | } else { |
| 245 | String::new() |
| 246 | } |
| 247 | }).collect() |
| 248 | } |
| 249 | |
| 250 | /// The run's real ink extent above and below the baseline, taken from the glyph outlines rather |
| 251 | /// than the font's global ascent and descent. Maths needs this: a maths font's global ascent spans |
| 252 | /// its tallest construction -- a big integral, a three-storey brace -- not the single symbol in |
| 253 | /// hand, so seating a fraction or a script by the font metric puts it wildly wrong. The outline is |
| 254 | /// y up with the baseline at zero, so the top of the ink is the height and the bottom, when it dips |
| 255 | /// below the baseline, is the depth. |
| 256 | pub fn ink_extent(&self) -> Outcome<(Sp, Sp)> { |
| 257 | let mut top = 0.0f32; // greatest height above the baseline |
| 258 | let mut bot = 0.0f32; // greatest depth below it, as a positive number |
| 259 | for glyph in &self.run.glyphs { |
| 260 | let path = res!(self.outline(glyph)); |
| 261 | if let Some(b) = path.bounds(&Transform::IDENTITY) { |
| 262 | if b.y1 > top { top = b.y1; } |
| 263 | if -b.y0 > bot { bot = -b.y0; } |
| 264 | } |
| 265 | } |
| 266 | Ok((Sp::from_pt(top as f64), Sp::from_pt(bot as f64))) |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | impl std::fmt::Debug for ShapedText { |
| 271 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 272 | // The font set holds parsed faces that do not print; summarise the run instead. |
| 273 | f.debug_struct("ShapedText") |
| 274 | .field("size", &self.size) |
| 275 | .field("glyphs", &self.run.glyphs.len()) |
| 276 | .field("dims", &self.dims) |
| 277 | .finish() |
| 278 | } |
| 279 | } |