Oregami
Repositories/oxedyne/fe2o3

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
7use crate::ir::{
8 Dims,
9 Metrics,
10 Sp,
11};
12
13use oxedyne_fe2o3_core::prelude::*;
14use oxedyne_fe2o3_font::{
15 face::Role,
16 font::Font,
17 set::FontSet,
18 shape::{
19 Dir,
20 Feature,
21 Glyph,
22 Run,
23 },
24};
25use oxedyne_fe2o3_graphics::colour::Rgba;
26use oxedyne_fe2o3_graphics::path::Path;
27use oxedyne_fe2o3_graphics::transform::Transform;
28
29use 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)]
35pub struct FontMetrics {
36 fonts: Arc<FontSet>,
37 role: Role,
38 dir: Dir,
39 size: f32, // device points, the shaper's pixel
40}
41
42impl 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
48impl 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)]
67enum Source {
68 Set { fonts: Arc<FontSet>, role: Role },
69 Solo { font: Arc<Font> },
70}
71
72impl 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)]
84pub 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
93impl 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
270impl 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}