1use std::{
2 borrow::Cow,
3 cmp::max,
4 fmt,
5 hash::{Hash, Hasher},
6 rc::Rc,
7};
8
9use once_cell::sync::Lazy;
10use regex::Regex;
17use rustc_hash::FxHashMap as HashMap;
18
19use crate::{
20 BoxOps, Digested, DigestedData, Result,
21 binding::content::{fontmap_family_key_sym, fontmap_key_syms, load_font_map, preload_font_map},
22 common::{
23 arena::{self, SymHashMap, SymStr},
24 color::{self, Color},
25 dimension::Dimension,
26 numeric_ops::{NumericOps, UNITY, UNITY_F64, kround},
27 store::Stored,
28 },
29 state::*,
30};
31
32pub mod standard_metrics;
33use standard_metrics::{MetricData, STDMETRICS};
34
35use crate::pin;
36
37pub type Fontmap = Rc<[Option<char>]>;
38
39static DEFFAMILY: &str = "serif";
40static DEFSERIES: &str = "medium";
41static DEFSHAPE: &str = "upright";
42static DEFCOLOR: Color = color::BLACK;
44static DEFOPACITY: &str = "1";
47static DEFENCODING: &str = "OT1";
48fn defsize() -> f64 {
53 let v = lookup_float("NOMINAL_FONT_SIZE").map_or(0.0, |f| f.0);
54 if v > 0.0 { v } else { 10.0 }
55}
56
57pub const TEXT_FONTS: [&str; 6] = ["cmr", "cmm", "cmsy", "cmex", "amsa", "amsb"];
58pub const MATH_FONTS: [&str; 6] = ["cmm", "cmsy", "cmex", "amsa", "amsb", "cmr"];
59
60pub const FLAG_FORCE_FAMILY: u8 = 0x1;
61pub const FLAG_FORCE_SERIES: u8 = 0x2;
62pub const FLAG_FORCE_SHAPE: u8 = 0x4;
63pub const FLAG_EMPH: u8 = 0x10;
64
65pub static FONT_TEXT_DEFAULT: Lazy<Font> = Lazy::new(Font::text_default);
66static LATIN_LETTER_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[\p{Latin}&&\pL]$").unwrap());
67static GREEK_LETTER_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[\p{Greek}&&\pL]$").unwrap());
68static UPPER_LETTER_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[\p{Lu}]$").unwrap());
69static DIGIT_LETTER_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[\p{N}]$").unwrap());
70#[rustfmt::skip]
71static FONT_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(xylubt|xyluat|xydash|xycmbt|xycmat|xycirc|xybtip|xybsql|xyatip|ul9|ugq|uaq|txtt|txsyb|txsya|txss|txr|txmi|pzd|pzc|pxsyb|pxsya|pxr|pxmi|put|ptm|psy|ppl|pnc|phv|pcr|pbk|pag|msy|msx|msb|msa|manfnt|linew|line|lcirclew|lcircle|futs|futmi|futm|eus|eur|euf|euex|cmvtt|cmtt|cmtl|cmt|cmsy|cmssqi|cmssq|cmss|cmsltt|cmr|cmmib|cmm|cmfr|cmfib|cmex|cmdunh|cmdh|cmu|cmbsy|cmbrs|cmbrm|cmbr|cm|ccy|ccr|ccm|ccitt|bch|bbold|bbmss|bbm)(sbc|sb|mc|m|bx|bm|bc|b|)(sl|sc|n|it|i|csc|)(\d*)$").unwrap());
72static FONT_FAMILY: Lazy<HashMap<&'static str, Font>> = Lazy::new(|| {
84 raw_map!(
85 "cmr" => fontmap!(family => "serif"), "cmss" => fontmap!(family => "sansserif"),
86 "cmtt" => fontmap!(family => "typewriter"), "cmvtt" => fontmap!(family => "typewriter"),
87 "cmt" => fontmap!(family => "serif"),
88 "cmsltt" => fontmap!(family => "typewriter", shape => "slanted"),
89 "cmssq" => fontmap!(family => "sansserif"),
90 "cmssqi" => fontmap!(family => "sansserif", shape => "italic"),
91 "cmdunh" => fontmap!(family => "serif"),
92 "cmu" => fontmap!(family => "serif"),
93 "cmfib" => fontmap!(family => "serif"), "cmfr" => fontmap!(family => "serif"),
94 "cmdh" => fontmap!(family => "serif"), "cm" => fontmap!(family => "serif"),
95 "ptm" => fontmap!(family => "serif"), "ppl" => fontmap!(family => "serif"),
96 "pnc" => fontmap!(family => "serif"), "pbk" => fontmap!(family => "serif"),
97 "phv" => fontmap!(family => "sansserif"), "pag" => fontmap!(family => "serif"),
98 "pcr" => fontmap!(family => "typewriter"), "pzc" => fontmap!(family => "script"),
99 "put" => fontmap!(family => "serif"), "bch" => fontmap!(family => "serif"),
100 "psy" => fontmap!(family => "symbol"), "pzd" => fontmap!(family => "dingbats"),
101 "ccr" => fontmap!(family => "serif"), "ccy" => fontmap!(family => "symbol"),
102 "cct" => fontmap!(family => "serif"),
106 "cmbr" => fontmap!(family => "sansserif"), "cmtl" => fontmap!(family => "typewriter"),
107 "cmbrs" => fontmap!(family => "symbol"), "ul9" => fontmap!(family => "typewriter"),
108 "txr" => fontmap!(family => "serif"), "txss" => fontmap!(family => "sansserif"),
109 "txtt" => fontmap!(family => "typewriter"),
110 "lmr" => fontmap!(family => "serif"), "lmss" => fontmap!(family => "sansserif"),
120 "lmtt" => fontmap!(family => "typewriter"), "lmvtt" => fontmap!(family => "typewriter"),
121 "qpl" => fontmap!(family => "serif"), "qtm" => fontmap!(family => "serif"),
123 "qbk" => fontmap!(family => "serif"), "qcs" => fontmap!(family => "serif"),
124 "qhv" => fontmap!(family => "sansserif"), "qag" => fontmap!(family => "sansserif"),
125 "qcr" => fontmap!(family => "typewriter"), "qzc" => fontmap!(family => "script"),
126 "zi4" => fontmap!(family => "typewriter"), "fi4" => fontmap!(family => "typewriter"),
129 "fvm" => fontmap!(family => "typewriter"), "fve" => fontmap!(family => "serif"),
130 "fvs" => fontmap!(family => "sansserif"),
131 "zsourcecodepro" => fontmap!(family => "typewriter"),
133 "SourceCodePro-TLF" => fontmap!(family => "typewriter"),
134 "FiraMono-TLF" => fontmap!(family => "typewriter"),
135 "FiraSans-TLF" => fontmap!(family => "sansserif"),
136 "txsya" => fontmap!(encoding => "AMSa"), "txsyb" => fontmap!(encoding => "AMSb"),
137 "pxr" => fontmap!(family => "serif"),
138 "pxsya" => fontmap!(encoding => "AMSa"), "pxsyb" => fontmap!(encoding => "AMSb"),
139 "futs" => fontmap!(family => "serif"),
140 "uaq" => fontmap!(family => "serif"), "ugq" => fontmap!(family => "sansserif"),
141 "manfnt" => fontmap!(family => "graphic", encoding => "manfnt"),
143 "line" => fontmap!(family => "graphic", encoding => "line"),
144 "linew" => fontmap!(family => "graphic", encoding => "line", series => "bold"),
145 "lcircle" => fontmap!(family => "graphic", encoding => "lcircle"),
146 "lcirclew" => fontmap!(family => "graphic", encoding => "lcircle", series => "bold"),
147 "xydash" => fontmap!(family => "graphic"), "xyatip" => fontmap!(family => "graphic"),
149 "xybtip" => fontmap!(family => "graphic"), "xybsql" => fontmap!(family => "graphic"),
150 "xycirc" => fontmap!(family => "graphic"), "xycmat" => fontmap!(family => "graphic"),
151 "xycmbt" => fontmap!(family => "graphic"), "xyluat" => fontmap!(family => "graphic"),
152 "xylubt" => fontmap!(family => "graphic"),
153 "eur" => fontmap!(family => "serif"), "eus" => fontmap!(family => "script"),
154 "euf" => fontmap!(family => "fraktur"), "euex" => fontmap!(encoding => "OMX"),
155 "ccm" => fontmap!(family => "serif", shape => "italic"),
157 "cmm" => fontmap!(family => "math", shape => "italic", encoding => "OML"),
158 "cmex" => fontmap!(encoding => "OMX"),
159 "cmsy" => fontmap!(encoding => "OMS"),
160 "ccitt" => fontmap!(family => "typewriter", shape => "italic"),
161 "cmbrm" => fontmap!(family => "sansserif", shape => "italic"),
162 "futm" => fontmap!(family => "serif", shape => "italic"),
163 "futmi" => fontmap!(family => "serif", shape => "italic"),
164 "txmi" => fontmap!(family => "serif", shape => "italic"),
165 "pxmi" => fontmap!(family => "serif", shape => "italic"),
166 "bbm" => fontmap!(family => "blackboard"),
168 "bbold" => fontmap!(family => "blackboard"),
169 "bbmss" => fontmap!(family => "blackboard"),
170 "cmmib" => fontmap!(family => "italic", series => "bold"),
172 "cmbsy" => fontmap!(series => "bold", encoding => "OMS"),
173 "msa" => fontmap!(encoding => "AMSa"),
174 "msb" => fontmap!(encoding => "AMSb"),
175 "msx" => fontmap!(encoding => "AMSa"),
177 "msy" => fontmap!(encoding => "AMSb")
178 )
179});
180static FONT_SERIES: Lazy<HashMap<&'static str, Font>> = Lazy::new(|| {
182 raw_map!(
183 "" => Font::default(), "m" => fontmap!(series => "medium"),
184 "mc" => fontmap!(series => "medium"),
185 "b" => fontmap!(series => "bold"), "bc" => fontmap!(series => "bold"),
186 "bx" => fontmap!(series => "bold"),
187 "sb" => fontmap!(series => "bold"), "sbc" => fontmap!(series => "bold"),
188 "bm" => fontmap!(series => "bold")
189 )
190});
191
192static FONT_SHAPE: Lazy<HashMap<&'static str, Font>> = Lazy::new(|| {
194 raw_map!(
195 "" => Font::default(), "n" => fontmap!(shape => "upright"),
196 "i" => fontmap!(shape => "italic"), "it" => fontmap!(shape => "italic"),
197 "sl" => fontmap!(shape => "slanted"),
198 "sc" => fontmap!(shape => "smallcaps"), "csc" => fontmap!(shape => "smallcaps")
199 )
200});
201
202static FONT_SIZE: Lazy<HashMap<&'static str, f64>> = Lazy::new(|| {
206 raw_map!(
207"tiny" => 0.5, "SMALL" => 0.7, "Small" => 0.8, "small" => 0.9,
208"normal" => 1.0, "large" => 1.2, "Large" => 1.44, "LARGE" => 1.728,
209"huge" => 2.074, "Huge" => 2.488,
210"big" => 1.2, "Big" => 1.6, "bigg" => 2.1, "Bigg" => 2.6)
211});
212
213static SCRIPT_STYLE_MAP: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
214 raw_map!(
215 "display" => "script", "text" => "script",
216 "script" => "scriptscript", "scriptscript" => "scriptscript")
217});
218
219static FRAC_STYLE_MAP: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
220 raw_map!(
221 "display" => "text", "text" => "script",
222 "script" => "scriptscript", "scriptscript" => "scriptscript")
223});
224
225static STYLE_SIZE: Lazy<HashMap<&'static str, usize>> = Lazy::new(|| {
226 raw_map!(
227 "display" => 10, "text" => 10, "script" => 7, "scriptscript" => 5)
228});
229
230static MATH_STYLE_STEP: Lazy<HashMap<&'static str, HashMap<&'static str, i32>>> = Lazy::new(|| {
238 raw_map!(
239 "display" => raw_map!(
240 "display" => 0, "text" => 1, "script" => 2, "scriptscript" => 3),
241 "text"=> raw_map!("display" => -1, "text" => 0, "script" => 1, "scriptscript" => 2),
242 "script"=> raw_map!("display" => -2, "text" => -1, "script" => 0, "scriptscript" => 1),
243 "scriptscript" => raw_map!("display" => -3, "text" => -2, "script" => -1, "scriptscript" => 0))
244});
245static STEP_MATH_STYLE: Lazy<HashMap<&'static str, HashMap<i32, &'static str>>> = Lazy::new(|| {
246 raw_map!(
247"display" => raw_map!(-3 => "display", -2 => "display", -1 => "display",
248 0 => "display", 1 => "text", 2 => "script", 3 => "scriptscript"),
249"text" => raw_map!(-3 => "display", -2 => "display", -1 => "display",
250 0 => "text", 1 => "script", 2 => "scriptscript", 3 => "scriptscript"),
251"script" => raw_map!(-3 => "display", -2 => "display", -1 => "text",
252 0 => "script", 1 => "scriptscript", 2 => "scriptscript", 3 => "scriptscript"),
253"scriptscript" => raw_map!(-3 => "display", -2 => "text", -1 => "script",
254 0 => "scriptscript", 1 => "scriptscript", 2 => "scriptscript", 3 => "scriptscript"))
255});
256
257fn lookup_metric_name(family: &str, series: &str, shape: &str) -> Option<&'static str> {
264 match (family, series, shape) {
265 ("serif", "medium", "upright") => Some("cmr"),
266 ("serif", "medium", "slanted") => Some("cmsl"),
267 ("serif", "medium", "italic") => Some("cmti"),
268 ("serif", "medium", "uprightitalic") => Some("cmu"),
269 ("serif", "bold", "upright") => Some("cmbx"),
270 ("serif", "medum", "smallcaps") => Some("cmcsc"), ("sansserif", "medium", "upright") => Some("cmss"),
272 ("sansserif", "medium", "italic") => Some("cmssi"),
273 ("sansserif", "bold", "upright") => Some("cmssbx"),
274 ("typewriter", "medium", "upright") => Some("cmtt"),
275 ("typewriter", "medium", "slanted") => Some("cmsltt"),
276 ("math", "medium", "italic") => Some("cmmi"),
277 ("math", "medium", "upright") => Some("cmr"),
278 ("math", "bold", "italic") => Some("cmmib"),
279 _ => None,
280 }
281}
282
283static METRIC_FALLBACKS: [&str; 7] = ["cmr", "cmmi", "cmsy", "cmex", "msam", "msbm", "ifgeo"];
290
291#[rustfmt::skip]
294static MATH_ATOM_TYPE: Lazy<HashMap<&'static str, usize>> = Lazy::new(|| {
295 raw_map!(
296 "ID" => 0,
297 "BIGOP" => 1, "SUMOP" => 1, "INTOP" => 1, "OPERATOR" => 1, "LIMITOP" => 1, "DIFFOP" => 1,
298 "ADDOP" => 2, "MULOP" => 2, "BINOP" => 2, "COMPOSEOP" => 2, "MIDDLE" => 2, "VERTBAR" => 2,
299 "RELOP" => 3, "METARELOP" => 3, "ARROW" => 3,
300 "OPEN" => 4, "CLOSE" => 5,
301 "PUNCT" => 6, "PERIOD" => 6,
302 "ARRAY" => 7, "MODIFIER" => 7
303 )
304});
305
306#[rustfmt::skip]
310static MATH_BEARINGS: [[i8; 8]; 8] = [
311 [ 0, 1, -2, -3, 0, 0, 0, -1],
312 [ 1, 1, 0, -3, 0, 0, 0, -1],
313 [-2, -2, 0, 0, -2, 0, 0, -2],
314 [-3, -3, 0, 0, -3, 0, 0, -3],
315 [ 0, 0, 0, 0, 0, 0, 0, 0],
316 [ 0, 1, -2, -3, 0, 0, 0, -1],
317 [-1, -1, 0, -1, -1, -1, -1, -1],
318 [-1, 1, -2, -3, -1, 0, -1, -1],
319];
320
321pub fn lookup_font_family(code: &str) -> Option<&Font> { FONT_FAMILY.get(code) }
328
329pub fn lookup_font_series(code: &str) -> Option<&Font> { FONT_SERIES.get(code) }
331
332pub fn lookup_font_shape(code: &str) -> Option<&Font> { FONT_SHAPE.get(code) }
334
335pub fn lookup_tex_font(fontname: &str, seriescode: &str, shapecode: &str) -> Font {
337 let mut props = Font::default();
338 if let Some(ffam) = lookup_font_family(fontname) {
339 props = props.merge_ref(ffam);
340 }
341 if let Some(fser) = lookup_font_series(seriescode) {
342 props = props.merge_ref(fser);
343 }
344 if let Some(fsh) = lookup_font_shape(shapecode) {
345 props = props.merge_ref(fsh);
346 }
347 props
348}
349
350pub fn get_metric_for_name(name: &str) -> &'static MetricData {
353 let base = if let Some(idx) = name.find(|c: char| c.is_ascii_digit()) {
354 &name[..idx]
355 } else {
356 name
357 };
358 if let Some(m) = STDMETRICS.get(name) {
360 return m;
361 }
362 if let Some(m) = STDMETRICS.get(base) {
364 return m;
365 }
366 let base_bytes = base.as_bytes();
372 if base_bytes.len() + 2 <= 32 {
373 let mut buf = [0u8; 32];
374 buf[..base_bytes.len()].copy_from_slice(base_bytes);
375 buf[base_bytes.len()] = b'1';
376 buf[base_bytes.len() + 1] = b'0';
377 if let Ok(s) = std::str::from_utf8(&buf[..base_bytes.len() + 2])
378 && let Some(m) = STDMETRICS.get(s)
379 {
380 return m;
381 }
382 }
383 STDMETRICS
385 .get("cmr")
386 .expect("STDMETRICS must contain 'cmr'")
387}
388
389pub fn decode_fontname(name: &str, at_opt: Option<f64>, scaled_opt: Option<f64>) -> Option<Font> {
390 if let Some(cap) = FONT_RE.captures(name) {
391 let mut props = Font {
393 series: Some(Cow::Borrowed(DEFSERIES)),
394 shape: Some(Cow::Borrowed(DEFSHAPE)),
395 encoding: Some(Cow::Borrowed("OT1")),
396 ..Font::default()
397 };
398 let fam = cap.get(1).map_or("", |m| m.as_str());
399 let ser = cap.get(2).map_or("", |m| m.as_str());
400 let shp = cap.get(3).map_or("", |m| m.as_str());
401 let size_str = cap.get(4).map_or("", |m| m.as_str());
402 if let Some(ffam) = lookup_font_family(fam) {
403 props = props.merge_ref(ffam);
404 }
405 if let Some(fser) = lookup_font_series(ser) {
406 props = props.merge_ref(fser);
407 }
408 if let Some(fsh) = lookup_font_shape(shp) {
409 props = props.merge_ref(fsh);
410 }
411 let mut size = if let Some(at) = at_opt {
412 at
413 } else {
414 let size_f64 = size_str.parse::<f64>().unwrap_or(1.0);
415 if size_f64 == 0.0 { 1.0 } else { size_f64 } };
417 if let Some(scaled) = scaled_opt {
418 size *= scaled;
419 }
420 props.size = Some(size);
421 if props.encoding.is_none() {
423 props.encoding = Some(Cow::Borrowed("OT1"));
424 }
425 Some(props)
430 } else {
431 None
432 }
433}
434
435#[derive(Clone, Default)]
443pub struct Font {
444 pub family: Option<Cow<'static, str>>,
445 pub series: Option<Cow<'static, str>>,
446 pub shape: Option<Cow<'static, str>>,
447 pub size: Option<f64>,
448 pub color: Option<Color>,
449 pub bg: Option<Color>,
450 pub opacity: Option<Cow<'static, str>>,
451 pub encoding: Option<Cow<'static, str>>,
452 pub language: Option<Cow<'static, str>>,
453 pub mathstyle: Option<Cow<'static, str>>,
454 pub mathstylestep: Option<i32>,
455 pub name: Option<Cow<'static, str>>,
456 pub emph: Option<bool>,
457 pub scripted: Option<bool>,
458 pub fraction: Option<bool>,
459 pub forceseries: Option<bool>,
462 pub forcefamily: Option<bool>,
463 pub forceshape: Option<bool>,
464 pub forcebold: Option<bool>,
465 pub scale: Option<f64>,
466 pub flags: Option<u8>,
467}
468
469impl Hash for Font {
470 fn hash<H: Hasher>(&self, hasher: &mut H) {
474 self.family.hash(hasher);
475 self.series.hash(hasher);
476 self.shape.hash(hasher);
477 self.size.map(|size| (size * 1000.0) as i64).hash(hasher);
478 Some(self.color.unwrap_or(DEFCOLOR)).hash(hasher);
480 self.bg.hash(hasher);
481 self.opacity.hash(hasher);
482 self.encoding.hash(hasher);
483 self.language.hash(hasher);
484 self.mathstyle.hash(hasher);
485 self.mathstylestep.hash(hasher);
486 self.name.hash(hasher);
487 self.emph.hash(hasher);
488 self.scripted.hash(hasher);
489 self.forceseries.hash(hasher);
490 self.forcefamily.hash(hasher);
491 self.forceshape.hash(hasher);
492 self.scale.map(|scale| (scale * 1000.0) as i64).hash(hasher);
493 self.flags.hash(hasher);
494 }
495}
496impl PartialEq for Font {
497 fn eq(&self, other: &Self) -> bool {
498 self.family == other.family
499 && self.series == other.series
500 && self.shape == other.shape
501 && self.size == other.size
502 && !is_diff_font_color(self.color.as_ref(), other.color.as_ref())
503 && self.bg == other.bg
504 && self.opacity == other.opacity
505 && self.encoding == other.encoding
506 && self.language == other.language
507 && self.mathstyle == other.mathstyle
508 && self.mathstylestep == other.mathstylestep
509 && self.name == other.name
510 && self.emph == other.emph
511 && self.scripted == other.scripted
512 && self.fraction == other.fraction
513 && self.forceseries == other.forceseries
514 && self.forcefamily == other.forcefamily
515 && self.forceshape == other.forceshape
516 && self.forcebold == other.forcebold
517 && self.scale == other.scale
518 && self.flags == other.flags
519 }
520}
521impl Eq for Font {}
522impl fmt::Display for Font {
525 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
526 write!(f, "{}", self.family.as_ref().unwrap_or(&Cow::Borrowed("")))
527 }
528}
529
530impl fmt::Debug for Font {
531 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
532 let star = Cow::Borrowed("*");
533 write!(f, "Font[")?;
534 write!(f, "{}", self.family.as_ref().unwrap_or(&star))?;
535 write!(f, ",")?;
536 write!(f, "{}", self.series.as_ref().unwrap_or(&star))?;
537 write!(f, ",")?;
538 write!(f, "{}", self.shape.as_ref().unwrap_or(&star))?;
539 write!(f, ",")?;
540 let size_str = self
541 .size
542 .as_ref()
543 .map(|x| x.to_string())
544 .unwrap_or_else(|| String::from('*'));
545 write!(f, "{}", size_str)?;
546 write!(f, ",")?;
547 if let Some(ref c) = self.color {
548 write!(f, "{c}")?;
549 } else {
550 write!(f, "*")?;
551 }
552 write!(f, ",")?;
553 if let Some(ref b) = self.bg {
554 write!(f, "{b}")?;
555 } else {
556 write!(f, "*")?;
557 }
558 write!(f, ",")?;
559 write!(f, "{}", self.opacity.as_ref().unwrap_or(&star))?;
560 write!(f, ",")?;
561 let scale_str = self
562 .scale
563 .as_ref()
564 .map(|x| x.to_string())
565 .unwrap_or_else(|| String::from('*'));
566 write!(f, "{}", scale_str)?;
567 write!(f, ",")?;
568 write!(f, "{}", self.mathstyle.as_ref().unwrap_or(&star))?;
569 write!(f, "]")
592 }
593}
594
595impl Font {
596 pub fn text_default() -> Self {
597 Font {
598 family: Some(Cow::Borrowed(DEFFAMILY)),
599 series: Some(Cow::Borrowed(DEFSERIES)),
600 shape: Some(Cow::Borrowed(DEFSHAPE)),
601 size: Some(defsize()),
602 color: None, bg: None, opacity: Some(Cow::Borrowed(DEFOPACITY)),
605 encoding: Some(Cow::Borrowed(DEFENCODING)),
606 language: None, mathstyle: None,
608 mathstylestep: None,
609 emph: None,
610 name: None,
611 scripted: None,
612 fraction: None,
613 forceseries: None,
614 forcefamily: None,
615 forceshape: None,
616 forcebold: None,
617 scale: None,
618 flags: None,
619 }
620 }
621 pub fn math_default() -> Self {
622 Font {
623 family: Some(Cow::Borrowed("math")),
624 series: Some(Cow::Borrowed(DEFSERIES)),
625 shape: Some(Cow::Borrowed("italic")),
626 size: Some(defsize()),
627 color: None, bg: None, opacity: Some(Cow::Borrowed(DEFOPACITY)),
630 encoding: None, language: None, mathstyle: Some(Cow::Borrowed("text")),
633 mathstylestep: None,
634 emph: None,
635 name: None,
636 scripted: None,
637 fraction: None,
638 forceseries: None,
639 forcefamily: None,
640 forceshape: None,
641 forcebold: None,
642 scale: None,
643 flags: None,
644 }
645 }
646
647 pub fn to_hashable(&self) -> u64 {
648 let mut hasher = rustc_hash::FxHasher::default();
656 Hash::hash(self, &mut hasher);
657 hasher.finish()
658 }
659
660 pub fn stringify(&self) -> String {
663 let fam = self
664 .family
665 .as_deref()
666 .map(|f| if f == "math" { "serif" } else { f });
667 let mut parts: Vec<&str> = Vec::new();
668 if let Some(f) = fam
669 && f != DEFFAMILY
670 {
671 parts.push(f);
672 }
673 if let Some(ref ser) = self.series
674 && ser.as_ref() != DEFSERIES
675 {
676 parts.push(ser);
677 }
678 if let Some(ref shp) = self.shape
679 && shp.as_ref() != DEFSHAPE
680 {
681 parts.push(shp);
682 }
683 let size_str;
685 if let Some(siz) = self.size
686 && (siz - defsize()).abs() > 0.001
687 {
688 size_str = siz.to_string();
689 parts.push(&size_str);
690 }
691 let color_str;
692 if let Some(ref col) = self.color
693 && *col != DEFCOLOR
694 {
695 color_str = col.to_attribute();
696 parts.push(&color_str);
697 }
698 let bg_str;
699 if let Some(ref bkg) = self.bg {
700 bg_str = bkg.to_attribute();
702 parts.push(&bg_str);
703 }
704 if let Some(ref opa) = self.opacity
705 && opa.as_ref() != DEFOPACITY
706 {
707 parts.push(opa);
708 }
709 if let Some(ref ms) = self.mathstyle {
710 parts.push(ms);
711 }
712 let flags_str;
713 if let Some(flags) = self.flags
714 && flags != 0
715 {
716 flags_str = flags.to_string();
717 parts.push(&flags_str);
718 }
719 format!("Font[{}]", parts.join(","))
720 }
721
722 pub fn font_match(&self, other: &Font) -> bool {
725 fn check<T: PartialEq>(a: &Option<T>, b: &Option<T>) -> bool {
726 !(a.is_some() && b.is_some() && a != b)
727 }
728 check(&self.family, &other.family)
729 && check(&self.series, &other.series)
730 && check(&self.shape, &other.shape)
731 && check(&self.size, &other.size)
732 && !is_diff_font_color(self.color.as_ref(), other.color.as_ref())
734 && check(&self.bg, &other.bg)
735 && check(&self.opacity, &other.opacity)
736 && check(&self.encoding, &other.encoding)
737 && check(&self.language, &other.language)
738 && check(&self.mathstyle, &other.mathstyle)
739 }
740
741 pub fn make_concrete(&self, concrete: &Font) -> Self {
744 Font {
745 family: self.family.clone().or_else(|| concrete.family.clone()),
746 series: self.series.clone().or_else(|| concrete.series.clone()),
747 shape: self.shape.clone().or_else(|| concrete.shape.clone()),
748 size: self.size.or(concrete.size),
749 color: self.color.or(concrete.color),
750 bg: self.bg.or(concrete.bg),
751 opacity: self.opacity.clone().or_else(|| concrete.opacity.clone()),
752 encoding: self.encoding.clone().or_else(|| concrete.encoding.clone()),
753 language: self.language.clone().or_else(|| concrete.language.clone()),
754 mathstyle: self
755 .mathstyle
756 .clone()
757 .or_else(|| concrete.mathstyle.clone()),
758 flags: Some(self.flags.unwrap_or(0) | concrete.flags.unwrap_or(0)),
759 mathstylestep: self.mathstylestep.or(concrete.mathstylestep),
760 name: self.name.clone().or_else(|| concrete.name.clone()),
761 emph: self.emph.or(concrete.emph),
762 scripted: self.scripted.or(concrete.scripted),
763 fraction: self.fraction.or(concrete.fraction),
764 forceseries: self.forceseries.or(concrete.forceseries),
765 forcefamily: self.forcefamily.or(concrete.forcefamily),
766 forceshape: self.forceshape.or(concrete.forceshape),
767 forcebold: self.forcebold.or(concrete.forcebold),
768 scale: self.scale.or(concrete.scale),
769 }
770 }
771
772 pub fn merge_purestyle(&self, changes: &Font) -> Self {
775 let mut new = self.clone();
776 if let Some(scale) = changes.scale
777 && let Some(ref mut sz) = new.size
778 {
779 *sz *= scale;
780 }
781 if changes.color.is_some() {
782 new.color.clone_from(&changes.color);
783 }
784 if changes.bg.is_some() {
785 new.bg.clone_from(&changes.bg);
786 }
787 if changes.opacity.is_some() {
788 new.opacity.clone_from(&changes.opacity);
789 }
790 if let Some(step) = changes.mathstylestep {
791 let cur_style: &str = new.mathstyle.as_deref().unwrap_or("display");
792 if let Some(step_map) = STEP_MATH_STYLE.get(cur_style)
793 && let Some(new_style) = step_map.get(&step)
794 {
795 new.mathstyle = Some(Cow::Borrowed(new_style));
796 }
797 }
798 new
799 }
800
801 pub fn math_bearing(&self, thisbox: &Digested, prevbox: &Digested) -> f64 {
804 let r0 = prevbox
805 .get_property("role")
806 .and_then(|s| match s.into_owned() {
807 Stored::String(sym) => Some(arena::with(sym, |s| s.to_string())),
808 _ => None,
809 })
810 .unwrap_or_else(|| "ID".to_string());
811 let r1 = thisbox
812 .get_property("role")
813 .and_then(|s| match s.into_owned() {
814 Stored::String(sym) => Some(arena::with(sym, |s| s.to_string())),
815 _ => None,
816 })
817 .unwrap_or_else(|| "ID".to_string());
818 let t0 = *MATH_ATOM_TYPE.get(r0.as_str()).unwrap_or(&0);
819 let t1 = *MATH_ATOM_TYPE.get(r1.as_str()).unwrap_or(&0);
820 let bearing = MATH_BEARINGS[t0][t1];
821 let style = self
822 .get_mathstyle()
823 .map(|s| s.to_string())
824 .unwrap_or_else(|| "text".to_string());
825 if bearing == 0 || (bearing < 0 && style != "display" && style != "text") {
826 return 0.0;
827 }
828 let reg_cs = match bearing.unsigned_abs() {
830 1 => T_CS!("\\thinmuskip"),
831 2 => T_CS!("\\medmuskip"),
832 3 => T_CS!("\\thickmuskip"),
833 _ => return 0.0,
834 };
835 if let Ok(Some(def)) = lookup_definition(®_cs)
836 && let Some(val) = def.value_of(Vec::new())
837 {
838 let skip = val.value_of();
843 let mu_width = self.get_mu_width() as f64;
844 return (skip as f64 / UNITY_F64 * mu_width).trunc();
845 }
846 0.0
847 }
848
849 pub fn is_sticky(&self) -> bool {
850 if let Some(ref family) = self.family {
851 family == "serif" || family == "sansserif" || family == "typewriter"
852 } else {
853 false
854 }
855 }
856
857 pub fn get_family(&self) -> Option<&Cow<'_, str>> { self.family.as_ref() }
859 pub fn get_series(&self) -> Option<&Cow<'_, str>> { self.series.as_ref() }
860 pub fn get_shape(&self) -> Option<&Cow<'_, str>> { self.shape.as_ref() }
861 pub fn get_size(&self) -> Option<f64> { self.size }
862 pub fn get_color(&self) -> Option<&Color> { self.color.as_ref() }
863 pub fn get_background(&self) -> Option<&Color> { self.bg.as_ref() }
864 pub fn get_opacity(&self) -> Option<&Cow<'_, str>> { self.opacity.as_ref() }
865 pub fn get_encoding(&self) -> Option<&Cow<'_, str>> { self.encoding.as_ref() }
866 pub fn get_language(&self) -> Option<&Cow<'_, str>> { self.language.as_ref() }
867 pub fn get_mathstyle(&self) -> Option<&Cow<'_, str>> { self.mathstyle.as_ref() }
868 pub fn get_flags(&self) -> Option<u8> { self.flags }
869
870 pub fn merge(&self, other: Font) -> Self { self.merge_ref(&other) }
875
876 pub fn merge_ref(&self, other: &Font) -> Self {
881 let mut series = other.series.clone();
883 let mut force_series = other.forceseries;
884 if other.forcebold == Some(true) {
885 series = Some(Cow::Borrowed("bold"));
886 force_series = Some(true);
887 }
888
889 let mut flags: u8 = 0;
891 if other.forcefamily == Some(true) {
892 flags |= FLAG_FORCE_FAMILY;
893 }
894 if force_series == Some(true) {
895 flags |= FLAG_FORCE_SERIES;
896 }
897 if other.forceshape == Some(true) {
898 flags |= FLAG_FORCE_SHAPE;
899 }
900
901 let oflags = self.flags.unwrap_or(0);
902 let family = if other.family.is_none() || (oflags & FLAG_FORCE_FAMILY != 0) {
904 self.family.clone()
905 } else {
906 other.family.clone()
907 };
908 let series = if series.is_none() || (oflags & FLAG_FORCE_SERIES != 0) {
909 self.series.clone()
910 } else {
911 series
912 };
913 let mut shape = if other.shape.is_none() || (oflags & FLAG_FORCE_SHAPE != 0) {
914 self.shape.clone()
915 } else {
916 other.shape.clone()
917 };
918 let mut size = other.size.or(self.size);
919 let color = other.color.or(self.color);
920 let bg = if other.bg.is_some() {
923 other.bg
924 } else {
925 self.bg
926 };
927 let opacity = other.opacity.clone().or_else(|| self.opacity.clone());
928 let encoding = other.encoding.clone().or_else(|| self.encoding.clone());
929 let language = other.language.clone().or_else(|| self.language.clone());
930 let mut mathstyle = other.mathstyle.clone().or_else(|| self.mathstyle.clone());
931 flags |= self.flags.unwrap_or(0);
932
933 if let Some(scale) = other.scale
935 && let Some(ref mut sz) = size
936 {
937 *sz *= scale;
938 }
939
940 let style_scale = if let Some(sz) = self.size {
942 let key: &str = self.mathstyle.as_deref().unwrap_or("display");
943 sz / *STYLE_SIZE.get(key).unwrap_or(&10) as f64
944 } else {
945 1.0
946 };
947
948 if other.size.is_some() {
949 } else if other.mathstyle.is_some() {
951 let ms: &str = mathstyle.as_deref().unwrap_or("display");
953 size = Some(style_scale * *STYLE_SIZE.get(ms).unwrap_or(&10) as f64);
954 } else if other.scripted == Some(true) {
955 let ms: &str = mathstyle.as_deref().unwrap_or("display");
957 mathstyle = SCRIPT_STYLE_MAP.get(ms).map(|c| Cow::Borrowed(*c));
958 let new_ms: &str = mathstyle.as_deref().unwrap_or("display");
959 size = Some(style_scale * *STYLE_SIZE.get(new_ms).unwrap_or(&10) as f64);
960 } else if other.fraction == Some(true) {
961 let ms: &str = mathstyle.as_deref().unwrap_or("display");
963 mathstyle = FRAC_STYLE_MAP.get(ms).map(|c| Cow::Borrowed(*c));
964 let new_ms: &str = mathstyle.as_deref().unwrap_or("display");
965 size = Some(style_scale * *STYLE_SIZE.get(new_ms).unwrap_or(&10) as f64);
966 }
967
968 if other.emph == Some(true) {
970 shape = if shape.as_deref() == Some("italic") {
971 Some(Cow::Borrowed("upright"))
972 } else {
973 Some(Cow::Borrowed("italic"))
974 };
975 flags |= FLAG_EMPH;
976 }
977 if mathstyle.is_some() {
979 flags &= !FLAG_EMPH;
980 }
981
982 let newfont = Font {
983 family,
984 series,
985 shape,
986 size,
987 color,
988 bg,
989 opacity,
990 encoding,
991 language,
992 mathstyle,
993 flags: Some(flags),
994 mathstylestep: other.mathstylestep.or(self.mathstylestep),
996 name: other.name.clone().or_else(|| self.name.clone()),
997 emph: None,
998 scripted: None,
999 fraction: None,
1000 forceseries: if flags & FLAG_FORCE_SERIES != 0 {
1001 Some(true)
1002 } else {
1003 None
1004 },
1005 forcefamily: if flags & FLAG_FORCE_FAMILY != 0 {
1006 Some(true)
1007 } else {
1008 None
1009 },
1010 forceshape: if flags & FLAG_FORCE_SHAPE != 0 {
1011 Some(true)
1012 } else {
1013 None
1014 },
1015 forcebold: None,
1016 scale: None,
1017 };
1018 newfont
1024 }
1025
1026 pub fn specialize(&self, text: &str) -> Self {
1035 let mut new = self.clone();
1036 if text.is_empty() {
1037 return new; }
1039 let deffamily = if self.forcefamily.unwrap_or(false) {
1040 self.family.clone().unwrap_or_else(|| DEFFAMILY.into())
1041 } else {
1042 DEFFAMILY.into()
1043 };
1044 let defseries = if self.forceseries.unwrap_or(false) {
1045 self.series.clone().unwrap_or_else(|| DEFSERIES.into())
1046 } else {
1047 DEFSERIES.into()
1048 };
1049 let defshape = if self.forceshape.unwrap_or(false) {
1050 self.shape.clone().unwrap_or_else(|| DEFSHAPE.into())
1051 } else {
1052 DEFSHAPE.into()
1053 };
1054 if LATIN_LETTER_RE.is_match(text) {
1055 if new.shape.is_none() && new.family.is_none() {
1057 new.shape = Some("italic".into());
1058 }
1059 } else if GREEK_LETTER_RE.is_match(text) {
1060 if UPPER_LETTER_RE.is_match(text) {
1062 if new.family.is_none() || (new.family.as_ref().unwrap() == "math") {
1064 new.family = Some(deffamily);
1065 if new.shape.is_some() && (new.shape != Some(DEFSHAPE.into())) {
1066 new.shape = Some(defshape); }
1068 }
1069 } else {
1070 if new.family.is_none() || (new.family.as_deref() != Some(DEFFAMILY)) {
1072 new.family = Some(deffamily);
1073 }
1074 if new.shape.is_none() || !self.forceshape.unwrap_or(false) {
1076 new.shape = Some("italic".into());
1077 }
1078 if new.series.is_some() && (new.series != Some(DEFSERIES.into())) {
1079 new.series = Some(defseries);
1080 }
1081 }
1082 } else if DIGIT_LETTER_RE.is_match(text) {
1083 if new.family.is_none() || (new.family.as_ref().unwrap() == "math") {
1085 new.family = Some(deffamily);
1086 new.shape = Some(defshape); }
1088 } else {
1089 new.family = Some(deffamily);
1091 new.shape = Some(defshape); if new.series.is_some() && (new.series.as_ref().unwrap() != DEFSERIES) {
1093 new.series = Some(defseries);
1094 } }
1096 new
1097 }
1098
1099 pub fn distance(&self, other: &Font) -> i8 {
1100 let mut distance: i8 = 0;
1101 let fam = self
1103 .family
1104 .as_deref()
1105 .map(|f| if f == "math" { "serif" } else { f });
1106 let ofam = other
1107 .family
1108 .as_deref()
1109 .map(|f| if f == "math" { "serif" } else { f });
1110 if is_diff_opt_str(fam, ofam) {
1111 distance += 1;
1112 }
1113 if is_diff_opt_str(self.series.as_deref(), other.series.as_deref()) {
1114 distance += 1;
1115 }
1116 if is_diff_opt_str(self.shape.as_deref(), other.shape.as_deref()) {
1117 distance += 1;
1118 }
1119 if is_diff_f64(self.size, other.size) {
1120 distance += 1;
1121 }
1122 if is_diff_font_color_ref(self.color.as_ref(), other.color.as_ref()) {
1126 distance += 1;
1127 }
1128 if is_diff_color(self.bg.as_ref(), other.bg.as_ref()) {
1129 distance += 1;
1130 }
1131 if is_diff_opt_str(self.opacity.as_deref(), other.opacity.as_deref()) {
1132 distance += 1;
1133 }
1134 if is_diff_opt_str(self.language.as_deref(), other.language.as_deref()) {
1137 distance += 1;
1138 }
1139 let flags = self.flags.unwrap_or(0);
1141 let oflags = other.flags.unwrap_or(0);
1142 if (flags & FLAG_EMPH) ^ (oflags & FLAG_EMPH) != 0 {
1143 distance += 1;
1144 }
1145 distance
1146 }
1147
1148 pub fn font_attribute_string(&self) -> String {
1153 let mut parts = Vec::new();
1154 if let Some(ref fam) = self.family {
1155 let f = if fam == "math" { "serif" } else { fam.as_ref() };
1156 if f != DEFFAMILY {
1157 parts.push(f.to_string());
1158 }
1159 }
1160 if let Some(ref ser) = self.series
1161 && ser.as_ref() != DEFSERIES
1162 {
1163 parts.push(ser.to_string());
1164 }
1165 if let Some(ref shp) = self.shape
1166 && shp.as_ref() != DEFSHAPE
1167 {
1168 parts.push(shp.to_string());
1169 }
1170 parts.join(" ")
1171 }
1172
1173 pub fn relative_to(&self, other: &Font) -> HashMap<String, (String, Font)> {
1182 let family = match self.family {
1183 Some(ref fam) => {
1184 if fam == "math" {
1185 Some(Cow::Borrowed("serif"))
1186 } else {
1187 Some(fam.clone())
1188 }
1189 },
1190 None => None,
1191 };
1192 let other_family = match other.family {
1193 Some(ref fam) => {
1194 if fam == "math" {
1195 Some(Cow::Borrowed("serif"))
1196 } else {
1197 Some(fam.clone())
1198 }
1199 },
1200 None => None,
1201 };
1202 let mut diffs = vec![];
1203 let mut font_properties = Font::default();
1204 if is_diff(family.as_ref(), other_family.as_ref()) {
1205 diffs.push(family.clone().unwrap());
1206 font_properties.family = family;
1207 }
1208 if is_diff(self.series.as_ref(), other.series.as_ref()) {
1209 let series = self.series.clone().unwrap();
1210 diffs.push(series);
1211 font_properties.series.clone_from(&self.series);
1212 }
1213 if is_diff(self.shape.as_ref(), other.shape.as_ref()) {
1214 let shape = self.shape.clone().unwrap();
1215 diffs.push(shape);
1216 font_properties.shape.clone_from(&self.shape);
1217 }
1218 let mut result = HashMap::default();
1219
1220 if !diffs.is_empty() {
1221 let font_value = diffs.join(" ");
1222 result.insert(s!("font"), (font_value, font_properties));
1223 }
1224
1225 if is_diff_f64(self.size.as_ref().copied(), other.size.as_ref().copied()) {
1226 result.insert(
1227 "fontsize".to_string(),
1228 (
1229 relative_font_size(self.size.unwrap(), other.size.unwrap()),
1230 Font {
1231 size: self.size,
1232 ..Font::default()
1233 },
1234 ),
1235 );
1236 }
1237 if is_diff_font_color_ref(self.color.as_ref(), other.color.as_ref()) {
1240 let effective_color = self.color.unwrap_or(DEFCOLOR);
1241 result.insert(
1242 "color".to_string(),
1243 (effective_color.to_attribute(), Font {
1244 color: Some(effective_color),
1245 ..Font::default()
1246 }),
1247 );
1248 }
1249 if is_diff_color(self.bg.as_ref(), other.bg.as_ref()) {
1250 result.insert(
1251 "backgroundcolor".to_string(),
1252 (self.bg.as_ref().unwrap().to_attribute(), Font {
1253 bg: self.bg,
1254 ..Font::default()
1255 }),
1256 );
1257 }
1258 if is_diff(self.opacity.as_ref(), other.opacity.as_ref()) {
1259 result.insert(
1260 "opacity".to_string(),
1261 (self.opacity.as_ref().unwrap().to_string(), Font {
1262 opacity: self.opacity.clone(),
1263 ..Font::default()
1264 }),
1265 );
1266 }
1267 if is_diff(self.language.as_ref(), other.language.as_ref()) {
1275 result.insert(
1276 "xml:lang".to_string(),
1277 (self.language.as_ref().unwrap().to_string(), Font {
1278 language: self.language.clone(),
1279 ..Font::default()
1280 }),
1281 );
1282 }
1283 let flags = self.flags.unwrap_or(0);
1285 let oflags = other.flags.unwrap_or(0);
1286 if self.mathstyle.is_none()
1287 && flags != 0
1288 && (flags & FLAG_EMPH) != 0
1289 && (oflags == 0 || (oflags & FLAG_EMPH) == 0)
1290 {
1291 result.insert(
1292 "element".to_string(),
1293 ("ltx:emph".to_string(), Font::default()),
1294 );
1295 }
1296 result
1298 }
1299
1300 pub fn purestyle_changes(&self, other: &Font) -> Font {
1301 let mathstyle = self.get_mathstyle();
1302 let othermathstyle = other.get_mathstyle();
1303 let othercolor = other.get_color();
1304 let mut changes = Font {
1305 scale: Some(other.get_size().unwrap() / self.get_size().unwrap()),
1306 bg: other.bg,
1307 opacity: other.opacity.clone(), ..Font::default()
1309 };
1310 if is_diff_font_color(othercolor, Some(&DEFCOLOR)) {
1311 changes.color = Some(othercolor.copied().unwrap_or(DEFCOLOR));
1312 }
1313
1314 if let Some(ms) = mathstyle
1315 && let Some(os) = othermathstyle
1316 {
1317 let ms_str: &str = ms;
1318 let os_str: &str = os;
1319 changes.mathstylestep = Some(*MATH_STYLE_STEP.get(ms_str).unwrap().get(os_str).unwrap());
1320 }
1321 changes
1322 }
1323
1324 pub fn get_metric(&self, c_opt: Option<char>) -> &MetricData {
1329 let family = self.family.as_deref().unwrap_or("serif");
1330 let series = self.series.as_deref().unwrap_or("medium");
1331 let shape = self.shape.as_deref().unwrap_or("upright");
1332 let size = self.size.unwrap_or_else(defsize) as i64;
1333 let mut ch_buf = [0u8; 4];
1337 let ch_key = c_opt.map(|c| c.encode_utf8(&mut ch_buf) as &str);
1338 if let Some(name) = lookup_metric_name(family, series, shape) {
1339 let fullname = format!("{name}{size}");
1340 if let Some(metric) = STDMETRICS.get(fullname.as_str())
1341 && ch_key.is_none_or(|k| metric.sizes.contains_key(k))
1342 {
1343 return metric;
1344 }
1345 let metric = get_metric_for_name(name);
1347 if ch_key.is_none_or(|k| metric.sizes.contains_key(k)) {
1348 return metric;
1349 }
1350 }
1351 if let Some(k) = ch_key {
1353 for name in METRIC_FALLBACKS {
1354 let fullname = format!("{name}{size}");
1355 let metric = STDMETRICS
1356 .get(fullname.as_str())
1357 .unwrap_or_else(|| get_metric_for_name(name));
1358 if metric.sizes.contains_key(k) {
1359 return metric;
1360 }
1361 }
1362 }
1363 get_metric_for_name("cmr")
1364 }
1365
1366 pub fn get_em_width(&self) -> i64 {
1367 let size = self.get_size().unwrap_or_else(defsize);
1368 let m = self.get_metric(None);
1369 (size * m.emwidth).trunc() as i64
1370 }
1371 pub fn get_ex_height(&self) -> i64 {
1372 let size = self.get_size().unwrap_or_else(defsize);
1373 let m = self.get_metric(None);
1374 (size * m.exheight).trunc() as i64
1375 }
1376 pub fn get_mu_width(&self) -> i64 {
1377 let size = self.get_size().unwrap_or_else(defsize);
1378 let m = self.get_metric(None);
1379 (size * m.emwidth / 18.0).trunc() as i64
1380 }
1381
1382 pub fn compute_string_size(
1383 &self,
1384 text: &str,
1385 _options: SymHashMap<Stored>,
1386 ) -> (Dimension, Dimension, Dimension) {
1387 if text.is_empty()
1388 || self
1389 .get_family()
1390 .map(|fam| fam == "nullfont")
1391 .unwrap_or(false)
1392 {
1393 return (
1394 Dimension::default(),
1395 Dimension::default(),
1396 Dimension::default(),
1397 );
1398 }
1399 let size = self.get_size().unwrap_or_else(defsize);
1400 let ismath = self.get_family().map(|fam| fam == "math").unwrap_or(false);
1401 let (mut w, mut h, mut d) = (0, 0, 0);
1402 let mut chars_iter = text.chars().peekable();
1405 let mut ch_buf = [0u8; 4];
1410 let mut kern_buf = [0u8; 8];
1411 while let Some(ch) = chars_iter.next() {
1412 let metric = self.get_metric(Some(ch));
1413 let ch_key = ch.encode_utf8(&mut ch_buf);
1414 let entry_opt = metric.sizes.get(ch_key);
1415 let (cw, ch_sz, cd, ci) = if let Some(entry) = entry_opt {
1416 *entry
1417 } else {
1418 (0.75 * UNITY_F64, 0.7 * UNITY_F64, 0.2 * UNITY_F64, 0.0)
1419 };
1420 w += (cw * size).trunc() as i64;
1421 if let Some(&next_ch) = chars_iter.peek() {
1423 let first_len = ch.encode_utf8(&mut kern_buf).len();
1424 let second_len = next_ch.encode_utf8(&mut kern_buf[first_len..]).len();
1425 let kern_key = std::str::from_utf8(&kern_buf[..first_len + second_len]).unwrap();
1426 if let Some(kern) = metric.kerns.get(kern_key) {
1427 w += (size * kern).trunc() as i64;
1428 }
1429 }
1430 if ismath && ci != 0.0 {
1432 w += (size * ci).trunc() as i64;
1433 }
1434 h = max(h, (ch_sz * size).trunc() as i64);
1435 d = max(d, (cd * size).trunc() as i64);
1436 }
1437 if w == 0 {
1440 w = 1;
1441 }
1442 (Dimension::new(w), Dimension::new(h), Dimension::new(d))
1443 }
1444
1445 pub fn get_nominal_size(&self) -> (Dimension, Dimension, Dimension) {
1448 let size = self.get_size().unwrap_or_else(defsize);
1449 let u = size * UNITY_F64;
1450 (
1451 Dimension::new_f64(0.75 * u),
1452 Dimension::new_f64(0.7 * u),
1453 Dimension::new_f64(0.2 * u),
1454 )
1455 }
1456
1457 pub fn compute_boxes_size(
1477 &self,
1478 boxes: &[Digested],
1479 options: SymHashMap<Stored>,
1480 ) -> Result<(Dimension, Dimension, Dimension)> {
1481 if let [single] = boxes
1491 && !matches!(single.data(), DigestedData::List(_))
1492 {
1493 let mut bx_clone = single.clone();
1494 let (w, h, d, ..) = bx_clone.get_size(None)?;
1495 return Ok((w, h, d));
1496 }
1497 let mode_str = match options.get("mode") {
1499 Some(Stored::String(s)) => arena::with(*s, |s| s.to_string()),
1500 _ => "restricted_horizontal".to_string(),
1501 };
1502 let vattach = match options.get("vattach") {
1504 Some(Stored::String(s)) => arena::with(*s, |s| s.to_string()),
1505 _ => "baseline".to_string(),
1506 };
1507 let para_width: Option<i64> = if mode_str.ends_with("horizontal") {
1513 match options.get("width") {
1514 Some(Stored::Dimension(d)) => Some(d.value_of()),
1515 Some(Stored::Int(i)) => Some(*i),
1516 _ => None,
1517 }
1518 } else {
1519 None
1520 };
1521 let baseline: i64 = match options.get("baseline") {
1523 Some(Stored::Dimension(d)) => d.value_of(),
1524 Some(Stored::Int(i)) => *i,
1525 _ => 12 * UNITY,
1526 };
1527 let mut maxwidth: i64 = 0;
1528 let mut lines: Vec<[i64; 4]> = Vec::new();
1531 if mode_str.ends_with("vertical") {
1532 for bx in boxes {
1538 if bx.has_property("isEmpty") {
1539 continue;
1540 }
1541 if matches!(bx.data(), DigestedData::List(_))
1543 && bx
1544 .get_property("mode")
1545 .map(|v| v.to_string())
1546 .unwrap_or_default()
1547 == "horizontal"
1548 && let Some(w) = bx.get_property("width").and_then(|v| match &*v {
1549 Stored::Dimension(d) => Some(d.value_of()),
1550 Stored::Int(i) => Some(*i),
1551 _ => None,
1552 })
1553 {
1554 if w > maxwidth {
1555 maxwidth = w;
1556 }
1557 let sub_baseline = bx
1558 .get_property("baseline")
1559 .and_then(|v| match &*v {
1560 Stored::Dimension(d) => Some(d.value_of()),
1561 Stored::Int(i) => Some(*i),
1562 _ => None,
1563 })
1564 .unwrap_or(baseline);
1565 lines.extend(self.linebreak_paragraph(&bx.unlist(), w, sub_baseline)?);
1566 continue;
1567 }
1568 let (w, h, d) = self.compute_boxes_size_box(bx)?;
1581 let bs = if bx.get_property_bool("isHorizontalRule") {
1582 -2
1583 } else if bx.get_property_bool("isVerticalSpace") {
1584 -1
1585 } else {
1586 baseline
1587 };
1588 if w != 0 || h != 0 || d != 0 {
1589 lines.push([bs, w, h, d]);
1590 }
1591 }
1592 } else if let Some(w) = para_width {
1593 if w > maxwidth {
1595 maxwidth = w;
1596 }
1597 let flat: Vec<&Digested> = boxes
1598 .iter()
1599 .filter(|b| !b.has_property("isEmpty"))
1600 .collect();
1601 let flat_owned: Vec<Digested> = flat.into_iter().cloned().collect();
1602 lines = self.linebreak_paragraph(&flat_owned, w, baseline)?;
1603 } else {
1604 let filtered: Vec<&Digested> = boxes
1606 .iter()
1607 .filter(|b| !b.has_property("isEmpty"))
1608 .collect();
1609 let words = self.compute_boxes_size_words(&filtered)?;
1610 lines = Self::compute_boxes_size_lines(None, baseline, &words);
1611 }
1612 let size = self.get_size().unwrap_or_else(defsize) as i64;
1614 let mathaxis = size * UNITY / 4;
1615 static SIZE_TRACE: std::sync::LazyLock<bool> =
1616 std::sync::LazyLock::new(|| std::env::var("LXML_SIZE_TRACE").is_ok());
1617 if *SIZE_TRACE {
1618 eprintln!(
1619 "SIZE mode={mode_str} vattach={vattach} baseline={} nboxes={} lines={:?}",
1620 baseline as f64 / 65536.0,
1621 boxes.len(),
1622 lines
1623 .iter()
1624 .map(|l| [
1625 l[0] as f64 / 65536.0,
1626 l[1] as f64 / 65536.0,
1627 l[2] as f64 / 65536.0,
1628 l[3] as f64 / 65536.0
1629 ])
1630 .collect::<Vec<_>>()
1631 );
1632 }
1633 let (mut wd, mut ht, mut dp) = Self::compute_boxes_size_stack(&vattach, mathaxis, &lines);
1634 if wd != 0 && maxwidth != 0 {
1636 wd = maxwidth;
1637 }
1638 if let Some(th) = options.get("totalheight").and_then(|v| match v {
1640 Stored::Dimension(d) => Some(d.value_of()),
1641 Stored::Int(i) => Some(*i),
1642 _ => None,
1643 }) {
1644 let diff = th - ht - dp;
1645 if diff > 0 {
1646 match vattach.as_str() {
1647 "bottom" => ht += diff,
1648 "middle" => {
1649 ht += diff / 2;
1650 dp += diff / 2;
1651 },
1652 _ => dp += diff,
1653 }
1654 }
1655 }
1656 Ok((Dimension::new(wd), Dimension::new(ht), Dimension::new(dp)))
1657 }
1658
1659 fn linebreak_paragraph(
1663 &self,
1664 boxes: &[Digested],
1665 width: i64,
1666 baseline: i64,
1667 ) -> Result<Vec<[i64; 4]>> {
1668 let flat = Self::flatten_paragraph(boxes);
1669 let flat_refs: Vec<&Digested> = flat.iter().collect();
1670 let words = self.compute_boxes_size_words(&flat_refs)?;
1671 Ok(Self::compute_boxes_size_lines(
1672 Some(width),
1673 baseline,
1674 &words,
1675 ))
1676 }
1677
1678 fn flatten_paragraph(boxes: &[Digested]) -> Vec<Digested> {
1682 let mut queue: std::collections::VecDeque<Digested> = boxes.iter().cloned().collect();
1683 let mut out: Vec<Digested> = Vec::new();
1684 while let Some(bx) = queue.pop_front() {
1685 if matches!(bx.data(), DigestedData::List(_))
1686 && bx
1687 .get_property("mode")
1688 .map(|v| v.to_string())
1689 .unwrap_or_default()
1690 == "horizontal"
1691 {
1692 for ib in bx.unlist().into_iter().rev() {
1693 queue.push_front(ib);
1694 }
1695 } else if matches!(bx.data(), DigestedData::Whatsit(_))
1696 && let Some(repl) = flatten_for_sizing(&bx)
1697 {
1698 for ib in repl.into_iter().rev() {
1699 queue.push_front(ib);
1700 }
1701 } else {
1702 out.push(bx);
1703 }
1704 }
1705 out
1706 }
1707
1708 fn compute_boxes_size_box(&self, bx: &Digested) -> Result<(i64, i64, i64)> {
1711 let mut bx_clone = bx.clone();
1713 let (w, h, d, ..) = bx_clone.get_size(None)?;
1714 Ok((w.value_of(), h.value_of(), d.value_of()))
1715 }
1716
1717 fn compute_boxes_size_words(&self, boxes: &[&Digested]) -> Result<Vec<[f64; 4]>> {
1721 let mut words: Vec<[f64; 4]> = Vec::new();
1722 let mut prevbox: Option<&Digested> = None;
1723 let mut prevspace: f64 = 0.0;
1724 let size = self.get_size().unwrap_or_else(defsize) as i64;
1726 let (mut wd, mut ht, mut dp): (f64, i64, i64) = (0.0, 0, 0);
1727 for bx in boxes {
1728 let (w, h, d) = self.compute_boxes_size_box(bx)?;
1729 if bx.get_property_bool("isBreak") {
1731 if bx.get_property_bool("isVerticalSpace") {
1735 ht = max(ht, h);
1736 dp = max(dp, d);
1737 }
1738 if wd != 0.0 || ht != 0 || dp != 0 || prevspace > 0.0 {
1739 words.push([prevspace, wd, ht as f64, dp as f64]);
1740 wd = 0.0;
1741 ht = 0;
1742 dp = 0;
1743 prevspace = -1.0;
1744 } else {
1745 prevspace = -1.0;
1746 }
1747 }
1748 else if bx.get_property_bool("isSpace") && !bx.get_property_bool("isVerticalSpace") {
1750 if wd != 0.0 || ht != 0 || dp != 0 || prevspace < 0.0 {
1751 words.push([prevspace, wd, ht as f64, dp as f64]);
1752 wd = 0.0;
1753 ht = 0;
1754 dp = 0;
1755 prevspace = w as f64;
1756 } else {
1757 prevspace += w as f64;
1758 }
1759 }
1760 else if bx.get_property_bool("isIdeographic") {
1762 if wd != 0.0 {
1763 words.push([prevspace, wd, ht as f64, dp as f64]);
1764 }
1765 words.push([0.0, w as f64, h as f64, d as f64]);
1766 wd = 0.0;
1767 ht = 0;
1768 dp = 0;
1769 prevspace = 0.0;
1770 }
1771 else {
1773 wd += w as f64;
1774 ht = max(ht, h);
1775 dp = max(dp, d);
1776 if let Some(pb) = prevbox
1778 && matches!(pb.data(), DigestedData::TBox(_))
1779 && matches!(bx.data(), DigestedData::TBox(_))
1780 {
1781 let prevchar = pb.get_string()?.chars().last();
1782 let curchar = bx.get_string()?.chars().next();
1783 let metric = self.get_metric(curchar);
1784 if let Some(family) = self.get_family()
1786 && family == "math"
1787 {
1788 wd += self.math_bearing(bx, pb);
1789 }
1790 if let Some(prevc) = prevchar
1792 && let Some(curc) = curchar
1793 {
1794 let kern_key = String::from(prevc) + &String::from(curc);
1795 if let Some(kern) = metric.kerns.get(kern_key.as_str()) {
1796 wd += size as f64 * kern;
1797 }
1798 }
1799 }
1800 }
1801 prevbox = Some(bx);
1803 }
1804 if wd != 0.0 || ht != 0 || dp != 0 || prevspace != 0.0 {
1806 words.push([prevspace, wd, ht as f64, dp as f64]);
1807 }
1808 Ok(words)
1809 }
1810
1811 fn compute_boxes_size_lines(
1815 wrapwidth: Option<i64>,
1816 baseline: i64,
1817 words: &[[f64; 4]],
1818 ) -> Vec<[i64; 4]> {
1819 let mut lines: Vec<[i64; 4]> = Vec::new();
1820 let fuzz = UNITY as f64; let (mut wd, mut ht, mut dp): (f64, i64, i64) = (0.0, 0, 0);
1822 for item in words {
1823 let (space, w, h, d) = (item[0], item[1], item[2] as i64, item[3] as i64);
1824 if space == -1.0 || wrapwidth.is_some_and(|ww| wd + space * 0.5 + w > ww as f64 + fuzz) {
1826 if wd != 0.0 {
1827 lines.push([baseline, kround(wd), ht, dp]);
1828 }
1829 wd = w;
1830 ht = h;
1831 dp = d;
1832 } else {
1833 wd += space + w;
1834 ht = max(ht, h);
1835 dp = max(dp, d);
1836 }
1837 }
1838 if wd != 0.0 || ht != 0 || dp != 0 {
1839 lines.push([baseline, kround(wd), ht, dp]);
1840 }
1841 lines
1842 }
1843
1844 fn compute_boxes_size_stack(vattach: &str, mathaxis: i64, lines: &[[i64; 4]]) -> (i64, i64, i64) {
1848 let nlines = lines.len();
1849 if nlines == 0 {
1850 return (0, 0, 0);
1851 }
1852 if nlines == 1 {
1853 let [_bs, w, h, d] = lines[0];
1854 return (w, h, d);
1855 }
1856 let lineskip = lookup_definition(&T_CS!("\\lineskip"))
1858 .ok()
1859 .flatten()
1860 .and_then(|def| def.value_of(Vec::new()))
1861 .map(|v| v.value_of())
1862 .unwrap_or(0);
1863 let mut wd: i64 = 0;
1864 let mut prevdepth: i64 = -99999;
1865 let mut th: i64 = 0;
1866 for line in lines {
1867 let [bs, w, h, d] = *line;
1868 wd = max(w, wd);
1869 th += h + d;
1870 if prevdepth >= 0 && bs >= 0 {
1871 if prevdepth + h < bs {
1872 th += bs - prevdepth - h;
1873 } else {
1874 th += lineskip;
1875 }
1876 }
1877 prevdepth = if bs >= 0 {
1883 d
1884 } else if bs == -1 {
1885 prevdepth
1886 } else {
1887 -99999
1888 };
1889 }
1890 let (ht, dp) = match vattach {
1891 "middle" => (th / 2 + mathaxis, th / 2 - mathaxis),
1892 "bottom" => {
1893 let d = lines[nlines - 1][3];
1894 (th - d, d)
1895 },
1896 _ => {
1898 let h = lines[0][2];
1899 (h, th - h)
1900 },
1901 };
1902 (wd, ht, dp)
1903 }
1904}
1905
1906fn flatten_for_sizing(_w: &Digested) -> Option<Vec<Digested>> { None }
1912
1913fn is_diff(x: Option<&Cow<str>>, y: Option<&Cow<str>>) -> bool {
1914 x.is_some() && (y.is_none() || (x != y))
1915}
1916
1917fn is_diff_opt_str(x: Option<&str>, y: Option<&str>) -> bool {
1918 x.is_some() && (y.is_none() || (x != y))
1919}
1920
1921fn is_diff_f64(x: Option<f64>, y: Option<f64>) -> bool { x.is_some() && (y.is_none() || (x != y)) }
1922
1923fn is_diff_color(x: Option<&Color>, y: Option<&Color>) -> bool {
1924 x.is_some() && (y.is_none() || (x != y))
1925}
1926
1927fn is_diff_font_color(x: Option<&Color>, y: Option<&Color>) -> bool {
1930 let cx = x.unwrap_or(&DEFCOLOR);
1931 let cy = y.unwrap_or(&DEFCOLOR);
1932 if cx == cy {
1933 return false;
1934 }
1935 cx.to_rgb() != cy.to_rgb()
1936}
1937
1938fn is_diff_font_color_ref(x: Option<&Color>, y: Option<&Color>) -> bool {
1945 let cx = x.unwrap_or(&DEFCOLOR);
1946 let cy = y.unwrap_or(&DEFCOLOR);
1947 cx != cy
1948}
1949
1950pub fn match_font(font1: &str, font2: &str) -> bool {
1954 if let Some(inner) = font1
1956 .strip_prefix("Font[")
1957 .and_then(|s| s.strip_suffix(']'))
1958 {
1959 let comps: Vec<&str> = inner.split(',').collect();
1960 let re_str = format!(
1961 "^Font\\[{}\\]$",
1962 comps
1963 .iter()
1964 .map(|c| if *c == "*" {
1965 "[^,]+".to_string()
1966 } else {
1967 regex::escape(c)
1968 })
1969 .collect::<Vec<_>>()
1970 .join(",")
1971 );
1972 if let Ok(re) = Regex::new(&re_str) {
1973 return re.is_match(font2);
1974 }
1975 }
1976 false
1977}
1978
1979pub fn font_match_xpaths(font: &str) -> String {
1982 if let Some(inner) = font.strip_prefix("Font[").and_then(|s| s.strip_suffix(']')) {
1983 let comps: Vec<&str> = inner.split(',').collect();
1984 let mut frags: Vec<String> = Vec::new();
1986 if !comps.is_empty() && comps[0] != "*" {
1987 frags.push(format!("[{},", comps[0]));
1988 }
1989 if comps.len() > 1 && comps[1] != "*" {
1990 frags.push(format!(",{},", comps[1]));
1991 }
1992 if comps.len() > 2 && comps[2] != "*" {
1993 frags.push(format!(",{},", comps[2]));
1994 }
1995 let mut parts: Vec<String> = vec!["@_font".to_string()];
1996 for frag in frags {
1997 parts.push(format!("contains(@_font,'{frag}')"));
1998 }
1999 parts.join(" and ")
2000 } else {
2001 "@_font".to_string()
2002 }
2003}
2004
2005fn push_fontmap_char(result: &mut String, c: char, _code: u8) {
2019 if c == '\u{1E9E}' {
2021 result.push_str("SS");
2022 return;
2023 }
2024 if is_combining_mark(c) {
2027 result.push('\u{00A0}');
2028 }
2029 result.push(c);
2030}
2031
2032fn is_combining_mark(c: char) -> bool {
2034 matches!(c as u32,
2035 0x0300..=0x036F | 0x1AB0..=0x1AFF | 0x1DC0..=0x1DFF | 0x20D0..=0x20FF | 0xFE20..=0xFE2F )
2041}
2042
2043pub fn decode_str(code: u8, encoding_opt: Option<String>, implicit: bool) -> Option<SymStr> {
2046 if let Some(s) = lookup_multichar_override(code, encoding_opt.as_deref()) {
2048 return Some(arena::pin(s));
2049 }
2050 if let Some(c) = decode(code, encoding_opt, implicit) {
2051 if c == '\u{1E9E}' {
2053 return Some(pin!("SS"));
2054 }
2055 if is_combining_mark(c) {
2058 return Some(arena::pin(format!("\u{00A0}{c}")));
2059 }
2060 Some(arena::pin_char(c))
2061 } else {
2062 None
2063 }
2064}
2065
2066fn lookup_multichar_override(code: u8, encoding_opt: Option<&str>) -> Option<String> {
2069 let encoding = match encoding_opt {
2070 Some(enc) if !enc.is_empty() => enc.to_string(),
2071 _ => {
2072 let font = lookup_font();
2073 font.and_then(|f| f.get_encoding().map(|e| e.to_string()))?
2074 },
2075 };
2076 if encoding.is_empty() {
2077 return None;
2078 }
2079 let _ = preload_font_map(&encoding);
2093 with_value_sym(fontmap_key_syms(&encoding).multichar, |val_opt| {
2094 if let Some(Stored::HashString(map)) = val_opt {
2095 map.get(&code.to_string()).cloned()
2096 } else {
2097 None
2098 }
2099 })
2100}
2101
2102pub fn decode(code: u8, encoding_opt: Option<String>, implicit: bool) -> Option<char> {
2103 let mut font = None;
2104 let encoding = match encoding_opt {
2105 Some(enc) => Cow::Owned(enc),
2106 None => {
2107 font = lookup_font();
2121 if let Some(ref font) = font {
2122 match font.get_encoding() {
2123 None => Cow::Borrowed("OT1"),
2124 Some(encoding) => encoding.clone(),
2125 }
2126 } else {
2127 Cow::Borrowed("OT1")
2128 }
2129 },
2130 };
2131
2132 let mut map: Option<Fontmap> = None;
2133 if !encoding.is_empty() {
2134 if let Some(encmap) = load_font_map(&encoding) {
2136 map = Some(encmap);
2138 if let Some(ref font) = font
2139 && let Some(family) = (*font).get_family()
2140 {
2141 with_value_sym(fontmap_family_key_sym(&encoding, family), |fmap_opt| {
2142 if let Some(fmap) = fmap_opt {
2143 map = fmap.into(); }
2145 });
2146 }
2147 }
2148 }
2149
2150 if implicit {
2151 if let Some(map) = map {
2152 if code < 128 {
2153 match map.get(code as usize) {
2154 None => None,
2155 Some(c) => *c,
2156 }
2157 } else {
2158 Some(code.into())
2159 }
2160 } else {
2161 Some(code.into())
2162 }
2163 } else if let Some(map) = map {
2164 match map.get(code as usize) {
2165 None => None,
2166 Some(c) => *c,
2167 }
2168 } else {
2169 None
2170 }
2171}
2172
2173pub fn decode_string(string: SymStr, encoding_opt: Option<&str>, implicit: bool) -> SymStr {
2174 let empty_sym = pin!("");
2175 if string == empty_sym {
2176 return empty_sym;
2177 }
2178 let mut font = None;
2179 let encoding = match encoding_opt {
2180 None => {
2181 font = lookup_font();
2182 if let Some(ref font) = font {
2183 font.get_encoding().unwrap_or(&Cow::Borrowed(""))
2184 } else {
2185 ""
2186 }
2187 },
2188 Some(encoding) => encoding,
2189 };
2190
2191 let mut map: Option<Fontmap> = None;
2196 if !encoding.is_empty()
2197 && let Some(encmap) = load_font_map(encoding)
2198 {
2199 map = Some(encmap);
2201 if let Some(ref font) = font
2202 && let Some(family) = (*font).get_family()
2203 {
2204 with_value_sym(fontmap_family_key_sym(encoding, family), |fmap_opt| {
2205 if let Some(fmap) = fmap_opt {
2206 map = fmap.into(); }
2208 });
2209 }
2210 }
2211
2212 let multichar_map: Option<HashMap<String, String>> = if !encoding.is_empty() {
2214 with_value_sym(fontmap_key_syms(encoding).multichar, |val_opt| {
2215 if let Some(Stored::HashString(m)) = val_opt {
2216 Some(m.clone())
2217 } else {
2218 None
2219 }
2220 })
2221 } else {
2222 None
2223 };
2224
2225 let mut result_string: String = String::new();
2226 arena::with(string, |str| {
2227 for c in str.chars() {
2228 if implicit {
2229 if let Some(ref map_ref) = map {
2230 let code = c as u16; if code < 128 {
2232 if let Some(ref mc) = multichar_map
2234 && let Some(mc_str) = mc.get(&(code as u8).to_string())
2235 {
2236 result_string.push_str(mc_str);
2237 continue;
2238 }
2239 if let Some(Some(mapc_val)) = map_ref.get(code as usize) {
2240 push_fontmap_char(&mut result_string, *mapc_val, code as u8);
2241 }
2242 } else {
2243 result_string.push(c);
2244 }
2245 } else {
2246 result_string.push(c)
2247 }
2248 } else if let Some(ref map_ref) = map {
2249 let code = c as u8;
2250 if let Some(ref mc) = multichar_map
2252 && let Some(mc_str) = mc.get(&code.to_string())
2253 {
2254 result_string.push_str(mc_str);
2255 continue;
2256 }
2257 if let Some(Some(mapc_val)) = map_ref.get(code as usize) {
2258 push_fontmap_char(&mut result_string, *mapc_val, code);
2259 }
2260 }
2261 }
2262 });
2263 arena::pin(result_string)
2264}
2265
2266pub fn rationalize_font_size(size: &str) -> f64 {
2268 if let Some(symbolic) = FONT_SIZE.get(size) {
2269 *symbolic * defsize()
2270 } else {
2271 size.parse::<f64>().unwrap_or_else(|_| defsize())
2273 }
2274}
2275
2276pub fn relative_font_size(newsize: f64, oldsize: f64) -> String {
2278 s!("{}%", (0.5 + 100.0 * newsize / oldsize).floor())
2279}
2280
2281#[cfg(test)]
2282mod tests {
2283 use super::*;
2284
2285 #[test]
2286 fn relative_font_size_same_is_100() {
2287 assert_eq!(relative_font_size(10.0, 10.0), "100%");
2288 assert_eq!(relative_font_size(12.0, 12.0), "100%");
2289 }
2290
2291 #[test]
2292 fn relative_font_size_doubled_is_200() {
2293 assert_eq!(relative_font_size(20.0, 10.0), "200%");
2294 }
2295
2296 #[test]
2297 fn relative_font_size_half_is_50() {
2298 assert_eq!(relative_font_size(5.0, 10.0), "50%");
2299 }
2300
2301 #[test]
2306 fn defsize_preserves_fractional_nominal_font_size() {
2307 use crate::{
2308 common::float::Float,
2309 state::{State, StateOptions, assign_value, set_state},
2310 };
2311 set_state(State::new(StateOptions::default()));
2312 assert_eq!(defsize(), 10.0, "defsize defaults to 10 when unset");
2314 assign_value("NOMINAL_FONT_SIZE", Float(10.95), None);
2316 assert_eq!(
2317 defsize(),
2318 10.95,
2319 "defsize preserves the fractional 11pt size"
2320 );
2321 assign_value("NOMINAL_FONT_SIZE", Float(12.0), None);
2323 assert_eq!(defsize(), 12.0, "12pt reads back as 12.0");
2324 }
2325
2326 #[test]
2339 fn encoding_is_font_identity_but_not_distance_or_output() {
2340 use crate::state::{State, StateOptions, set_state};
2341 set_state(State::new(StateOptions::default()));
2342
2343 let base = Font::default();
2344 let mut t1 = base.clone();
2345 t1.encoding = Some(Cow::Borrowed("T1"));
2346 let mut ot1 = base.clone();
2347 ot1.encoding = Some(Cow::Borrowed("OT1"));
2348
2349 assert_ne!(
2351 t1, ot1,
2352 "fonts differing only in encoding must not be equal"
2353 );
2354 assert_ne!(
2355 t1.to_hashable(),
2356 ot1.to_hashable(),
2357 "differently-encoded fonts must get distinct `_font` ids"
2358 );
2359
2360 assert_eq!(
2362 t1.distance(&ot1),
2363 0,
2364 "encoding alone must not count as a font-switch distance"
2365 );
2366
2367 let rel = t1.relative_to(&ot1);
2370 assert!(
2371 rel.is_empty(),
2372 "relative_to must not emit @encoding (it leaks onto foreign xhtml); got {rel:?}"
2373 );
2374 }
2375
2376 #[test]
2377 fn match_font_exact_wildcard_tail() {
2378 assert!(match_font("Font[*,*,*,*]", "Font[rm,med,up,10]"));
2383 }
2384
2385 #[test]
2386 fn match_font_exact_match() {
2387 assert!(match_font("Font[rm,med,up,10]", "Font[rm,med,up,10]"));
2388 assert!(!match_font("Font[rm,med,up,10]", "Font[sf,med,up,10]"));
2389 }
2390
2391 #[test]
2392 fn match_font_partial_wildcard() {
2393 assert!(match_font("Font[*,med,up,10]", "Font[rm,med,up,10]"));
2395 assert!(match_font("Font[*,med,up,10]", "Font[sf,med,up,10]"));
2396 assert!(!match_font("Font[*,bold,up,10]", "Font[rm,med,up,10]"));
2398 }
2399
2400 #[test]
2401 fn match_font_malformed_input() {
2402 assert!(!match_font("not_a_font", "Font[rm,med,up,10]"));
2404 }
2405
2406 #[test]
2407 fn font_match_xpaths_all_wildcards_is_attr_only() {
2408 let xp = font_match_xpaths("Font[*,*,*,*]");
2409 assert_eq!(xp, "@_font");
2411 }
2412
2413 #[test]
2414 fn font_match_xpaths_includes_specified_components() {
2415 let xp = font_match_xpaths("Font[rm,bold,*,*]");
2416 assert!(xp.contains("@_font"));
2418 assert!(xp.contains("contains"));
2419 assert!(xp.contains("rm"));
2420 assert!(xp.contains("bold"));
2421 }
2422
2423 #[test]
2424 fn font_match_xpaths_malformed_is_empty_or_fallback() {
2425 let xp = font_match_xpaths("garbage");
2426 let _ = xp;
2430 }
2431}