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::{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.encoding.as_ref(), other.encoding.as_ref()) {
1268 result.insert(
1269 "encoding".to_string(),
1270 (self.encoding.as_ref().unwrap().to_string(), Font {
1271 encoding: self.encoding.clone(),
1272 ..Font::default()
1273 }),
1274 );
1275 }
1276 if is_diff(self.language.as_ref(), other.language.as_ref()) {
1277 result.insert(
1278 "xml:lang".to_string(),
1279 (self.language.as_ref().unwrap().to_string(), Font {
1280 language: self.language.clone(),
1281 ..Font::default()
1282 }),
1283 );
1284 }
1285 let flags = self.flags.unwrap_or(0);
1287 let oflags = other.flags.unwrap_or(0);
1288 if self.mathstyle.is_none()
1289 && flags != 0
1290 && (flags & FLAG_EMPH) != 0
1291 && (oflags == 0 || (oflags & FLAG_EMPH) == 0)
1292 {
1293 result.insert(
1294 "element".to_string(),
1295 ("ltx:emph".to_string(), Font::default()),
1296 );
1297 }
1298 result
1300 }
1301
1302 pub fn purestyle_changes(&self, other: &Font) -> Font {
1303 let mathstyle = self.get_mathstyle();
1304 let othermathstyle = other.get_mathstyle();
1305 let othercolor = other.get_color();
1306 let mut changes = Font {
1307 scale: Some(other.get_size().unwrap() / self.get_size().unwrap()),
1308 bg: other.bg,
1309 opacity: other.opacity.clone(), ..Font::default()
1311 };
1312 if is_diff_font_color(othercolor, Some(&DEFCOLOR)) {
1313 changes.color = Some(othercolor.copied().unwrap_or(DEFCOLOR));
1314 }
1315
1316 if let Some(ms) = mathstyle
1317 && let Some(os) = othermathstyle
1318 {
1319 let ms_str: &str = ms;
1320 let os_str: &str = os;
1321 changes.mathstylestep = Some(*MATH_STYLE_STEP.get(ms_str).unwrap().get(os_str).unwrap());
1322 }
1323 changes
1324 }
1325
1326 pub fn get_metric(&self, c_opt: Option<char>) -> &MetricData {
1331 let family = self.family.as_deref().unwrap_or("serif");
1332 let series = self.series.as_deref().unwrap_or("medium");
1333 let shape = self.shape.as_deref().unwrap_or("upright");
1334 let size = self.size.unwrap_or_else(defsize) as i64;
1335 let mut ch_buf = [0u8; 4];
1339 let ch_key = c_opt.map(|c| c.encode_utf8(&mut ch_buf) as &str);
1340 if let Some(name) = lookup_metric_name(family, series, shape) {
1341 let fullname = format!("{name}{size}");
1342 if let Some(metric) = STDMETRICS.get(fullname.as_str())
1343 && ch_key.is_none_or(|k| metric.sizes.contains_key(k))
1344 {
1345 return metric;
1346 }
1347 let metric = get_metric_for_name(name);
1349 if ch_key.is_none_or(|k| metric.sizes.contains_key(k)) {
1350 return metric;
1351 }
1352 }
1353 if let Some(k) = ch_key {
1355 for name in METRIC_FALLBACKS {
1356 let fullname = format!("{name}{size}");
1357 let metric = STDMETRICS
1358 .get(fullname.as_str())
1359 .unwrap_or_else(|| get_metric_for_name(name));
1360 if metric.sizes.contains_key(k) {
1361 return metric;
1362 }
1363 }
1364 }
1365 get_metric_for_name("cmr")
1366 }
1367
1368 pub fn get_em_width(&self) -> i64 {
1369 let size = self.get_size().unwrap_or_else(defsize);
1370 let m = self.get_metric(None);
1371 (size * m.emwidth).trunc() as i64
1372 }
1373 pub fn get_ex_height(&self) -> i64 {
1374 let size = self.get_size().unwrap_or_else(defsize);
1375 let m = self.get_metric(None);
1376 (size * m.exheight).trunc() as i64
1377 }
1378 pub fn get_mu_width(&self) -> i64 {
1379 let size = self.get_size().unwrap_or_else(defsize);
1380 let m = self.get_metric(None);
1381 (size * m.emwidth / 18.0).trunc() as i64
1382 }
1383
1384 pub fn compute_string_size(
1385 &self,
1386 text: &str,
1387 _options: SymHashMap<Stored>,
1388 ) -> (Dimension, Dimension, Dimension) {
1389 if text.is_empty()
1390 || self
1391 .get_family()
1392 .map(|fam| fam == "nullfont")
1393 .unwrap_or(false)
1394 {
1395 return (
1396 Dimension::default(),
1397 Dimension::default(),
1398 Dimension::default(),
1399 );
1400 }
1401 let size = self.get_size().unwrap_or_else(defsize);
1402 let ismath = self.get_family().map(|fam| fam == "math").unwrap_or(false);
1403 let (mut w, mut h, mut d) = (0, 0, 0);
1404 let mut chars_iter = text.chars().peekable();
1407 let mut ch_buf = [0u8; 4];
1412 let mut kern_buf = [0u8; 8];
1413 while let Some(ch) = chars_iter.next() {
1414 let metric = self.get_metric(Some(ch));
1415 let ch_key = ch.encode_utf8(&mut ch_buf);
1416 let entry_opt = metric.sizes.get(ch_key);
1417 let (cw, ch_sz, cd, ci) = if let Some(entry) = entry_opt {
1418 *entry
1419 } else {
1420 (0.75 * UNITY_F64, 0.7 * UNITY_F64, 0.2 * UNITY_F64, 0.0)
1421 };
1422 w += (cw * size).trunc() as i64;
1423 if let Some(&next_ch) = chars_iter.peek() {
1425 let first_len = ch.encode_utf8(&mut kern_buf).len();
1426 let second_len = next_ch.encode_utf8(&mut kern_buf[first_len..]).len();
1427 let kern_key = std::str::from_utf8(&kern_buf[..first_len + second_len]).unwrap();
1428 if let Some(kern) = metric.kerns.get(kern_key) {
1429 w += (size * kern).trunc() as i64;
1430 }
1431 }
1432 if ismath && ci != 0.0 {
1434 w += (size * ci).trunc() as i64;
1435 }
1436 h = max(h, (ch_sz * size).trunc() as i64);
1437 d = max(d, (cd * size).trunc() as i64);
1438 }
1439 if w == 0 {
1442 w = 1;
1443 }
1444 (Dimension::new(w), Dimension::new(h), Dimension::new(d))
1445 }
1446
1447 pub fn get_nominal_size(&self) -> (Dimension, Dimension, Dimension) {
1450 let size = self.get_size().unwrap_or_else(defsize);
1451 let u = size * UNITY_F64;
1452 (
1453 Dimension::new_f64(0.75 * u),
1454 Dimension::new_f64(0.7 * u),
1455 Dimension::new_f64(0.2 * u),
1456 )
1457 }
1458
1459 pub fn compute_boxes_size(
1479 &self,
1480 boxes: &[Digested],
1481 options: SymHashMap<Stored>,
1482 ) -> Result<(Dimension, Dimension, Dimension)> {
1483 if let [single] = boxes
1493 && !matches!(single.data(), DigestedData::List(_))
1494 {
1495 let mut bx_clone = single.clone();
1496 let (w, h, d, ..) = bx_clone.get_size(None)?;
1497 return Ok((w, h, d));
1498 }
1499 let mode_str = match options.get("mode") {
1501 Some(Stored::String(s)) => arena::with(*s, |s| s.to_string()),
1502 _ => "restricted_horizontal".to_string(),
1503 };
1504 let vattach = match options.get("vattach") {
1506 Some(Stored::String(s)) => arena::with(*s, |s| s.to_string()),
1507 _ => "baseline".to_string(),
1508 };
1509 let para_width: Option<i64> = if mode_str.ends_with("horizontal") {
1515 match options.get("width") {
1516 Some(Stored::Dimension(d)) => Some(d.value_of()),
1517 Some(Stored::Int(i)) => Some(*i),
1518 _ => None,
1519 }
1520 } else {
1521 None
1522 };
1523 let baseline: i64 = match options.get("baseline") {
1525 Some(Stored::Dimension(d)) => d.value_of(),
1526 Some(Stored::Int(i)) => *i,
1527 _ => 12 * UNITY,
1528 };
1529 let mut maxwidth: i64 = 0;
1530 let mut lines: Vec<[i64; 4]> = Vec::new();
1533 if mode_str.ends_with("vertical") {
1534 for bx in boxes {
1540 if bx.has_property("isEmpty") {
1541 continue;
1542 }
1543 if matches!(bx.data(), DigestedData::List(_))
1545 && bx
1546 .get_property("mode")
1547 .map(|v| v.to_string())
1548 .unwrap_or_default()
1549 == "horizontal"
1550 && let Some(w) = bx.get_property("width").and_then(|v| match &*v {
1551 Stored::Dimension(d) => Some(d.value_of()),
1552 Stored::Int(i) => Some(*i),
1553 _ => None,
1554 })
1555 {
1556 if w > maxwidth {
1557 maxwidth = w;
1558 }
1559 let sub_baseline = bx
1560 .get_property("baseline")
1561 .and_then(|v| match &*v {
1562 Stored::Dimension(d) => Some(d.value_of()),
1563 Stored::Int(i) => Some(*i),
1564 _ => None,
1565 })
1566 .unwrap_or(baseline);
1567 lines.extend(self.linebreak_paragraph(&bx.unlist(), w, sub_baseline)?);
1568 continue;
1569 }
1570 let (w, h, d) = self.compute_boxes_size_box(bx)?;
1583 let bs = if bx.get_property_bool("isHorizontalRule") {
1584 -2
1585 } else if bx.get_property_bool("isVerticalSpace") {
1586 -1
1587 } else {
1588 baseline
1589 };
1590 if w != 0 || h != 0 || d != 0 {
1591 lines.push([bs, w, h, d]);
1592 }
1593 }
1594 } else if let Some(w) = para_width {
1595 if w > maxwidth {
1597 maxwidth = w;
1598 }
1599 let flat: Vec<&Digested> = boxes
1600 .iter()
1601 .filter(|b| !b.has_property("isEmpty"))
1602 .collect();
1603 let flat_owned: Vec<Digested> = flat.into_iter().cloned().collect();
1604 lines = self.linebreak_paragraph(&flat_owned, w, baseline)?;
1605 } else {
1606 let filtered: Vec<&Digested> = boxes
1608 .iter()
1609 .filter(|b| !b.has_property("isEmpty"))
1610 .collect();
1611 let words = self.compute_boxes_size_words(&filtered)?;
1612 lines = Self::compute_boxes_size_lines(None, baseline, &words);
1613 }
1614 let size = self.get_size().unwrap_or_else(defsize) as i64;
1616 let mathaxis = size * UNITY / 4;
1617 static SIZE_TRACE: std::sync::LazyLock<bool> =
1618 std::sync::LazyLock::new(|| std::env::var("LXML_SIZE_TRACE").is_ok());
1619 if *SIZE_TRACE {
1620 eprintln!(
1621 "SIZE mode={mode_str} vattach={vattach} baseline={} nboxes={} lines={:?}",
1622 baseline as f64 / 65536.0,
1623 boxes.len(),
1624 lines
1625 .iter()
1626 .map(|l| [
1627 l[0] as f64 / 65536.0,
1628 l[1] as f64 / 65536.0,
1629 l[2] as f64 / 65536.0,
1630 l[3] as f64 / 65536.0
1631 ])
1632 .collect::<Vec<_>>()
1633 );
1634 }
1635 let (mut wd, mut ht, mut dp) = Self::compute_boxes_size_stack(&vattach, mathaxis, &lines);
1636 if wd != 0 && maxwidth != 0 {
1638 wd = maxwidth;
1639 }
1640 if let Some(th) = options.get("totalheight").and_then(|v| match v {
1642 Stored::Dimension(d) => Some(d.value_of()),
1643 Stored::Int(i) => Some(*i),
1644 _ => None,
1645 }) {
1646 let diff = th - ht - dp;
1647 if diff > 0 {
1648 match vattach.as_str() {
1649 "bottom" => ht += diff,
1650 "middle" => {
1651 ht += diff / 2;
1652 dp += diff / 2;
1653 },
1654 _ => dp += diff,
1655 }
1656 }
1657 }
1658 Ok((Dimension::new(wd), Dimension::new(ht), Dimension::new(dp)))
1659 }
1660
1661 fn linebreak_paragraph(
1665 &self,
1666 boxes: &[Digested],
1667 width: i64,
1668 baseline: i64,
1669 ) -> Result<Vec<[i64; 4]>> {
1670 let flat = Self::flatten_paragraph(boxes);
1671 let flat_refs: Vec<&Digested> = flat.iter().collect();
1672 let words = self.compute_boxes_size_words(&flat_refs)?;
1673 Ok(Self::compute_boxes_size_lines(
1674 Some(width),
1675 baseline,
1676 &words,
1677 ))
1678 }
1679
1680 fn flatten_paragraph(boxes: &[Digested]) -> Vec<Digested> {
1684 let mut queue: std::collections::VecDeque<Digested> = boxes.iter().cloned().collect();
1685 let mut out: Vec<Digested> = Vec::new();
1686 while let Some(bx) = queue.pop_front() {
1687 if matches!(bx.data(), DigestedData::List(_))
1688 && bx
1689 .get_property("mode")
1690 .map(|v| v.to_string())
1691 .unwrap_or_default()
1692 == "horizontal"
1693 {
1694 for ib in bx.unlist().into_iter().rev() {
1695 queue.push_front(ib);
1696 }
1697 } else if matches!(bx.data(), DigestedData::Whatsit(_))
1698 && let Some(repl) = flatten_for_sizing(&bx)
1699 {
1700 for ib in repl.into_iter().rev() {
1701 queue.push_front(ib);
1702 }
1703 } else {
1704 out.push(bx);
1705 }
1706 }
1707 out
1708 }
1709
1710 fn compute_boxes_size_box(&self, bx: &Digested) -> Result<(i64, i64, i64)> {
1713 let mut bx_clone = bx.clone();
1715 let (w, h, d, ..) = bx_clone.get_size(None)?;
1716 Ok((w.value_of(), h.value_of(), d.value_of()))
1717 }
1718
1719 fn compute_boxes_size_words(&self, boxes: &[&Digested]) -> Result<Vec<[f64; 4]>> {
1723 let mut words: Vec<[f64; 4]> = Vec::new();
1724 let mut prevbox: Option<&Digested> = None;
1725 let mut prevspace: f64 = 0.0;
1726 let size = self.get_size().unwrap_or_else(defsize) as i64;
1728 let (mut wd, mut ht, mut dp): (f64, i64, i64) = (0.0, 0, 0);
1729 for bx in boxes {
1730 let (w, h, d) = self.compute_boxes_size_box(bx)?;
1731 if bx.get_property_bool("isBreak") {
1733 if bx.get_property_bool("isVerticalSpace") {
1737 ht = max(ht, h);
1738 dp = max(dp, d);
1739 }
1740 if wd != 0.0 || ht != 0 || dp != 0 || prevspace > 0.0 {
1741 words.push([prevspace, wd, ht as f64, dp as f64]);
1742 wd = 0.0;
1743 ht = 0;
1744 dp = 0;
1745 prevspace = -1.0;
1746 } else {
1747 prevspace = -1.0;
1748 }
1749 }
1750 else if bx.get_property_bool("isSpace") && !bx.get_property_bool("isVerticalSpace") {
1752 if wd != 0.0 || ht != 0 || dp != 0 || prevspace < 0.0 {
1753 words.push([prevspace, wd, ht as f64, dp as f64]);
1754 wd = 0.0;
1755 ht = 0;
1756 dp = 0;
1757 prevspace = w as f64;
1758 } else {
1759 prevspace += w as f64;
1760 }
1761 }
1762 else if bx.get_property_bool("isIdeographic") {
1764 if wd != 0.0 {
1765 words.push([prevspace, wd, ht as f64, dp as f64]);
1766 }
1767 words.push([0.0, w as f64, h as f64, d as f64]);
1768 wd = 0.0;
1769 ht = 0;
1770 dp = 0;
1771 prevspace = 0.0;
1772 }
1773 else {
1775 wd += w as f64;
1776 ht = max(ht, h);
1777 dp = max(dp, d);
1778 if let Some(pb) = prevbox
1780 && matches!(pb.data(), DigestedData::TBox(_))
1781 && matches!(bx.data(), DigestedData::TBox(_))
1782 {
1783 let prevchar = pb.get_string()?.chars().last();
1784 let curchar = bx.get_string()?.chars().next();
1785 let metric = self.get_metric(curchar);
1786 if let Some(family) = self.get_family()
1788 && family == "math"
1789 {
1790 wd += self.math_bearing(bx, pb);
1791 }
1792 if let Some(prevc) = prevchar
1794 && let Some(curc) = curchar
1795 {
1796 let kern_key = String::from(prevc) + &String::from(curc);
1797 if let Some(kern) = metric.kerns.get(kern_key.as_str()) {
1798 wd += size as f64 * kern;
1799 }
1800 }
1801 }
1802 }
1803 prevbox = Some(bx);
1805 }
1806 if wd != 0.0 || ht != 0 || dp != 0 || prevspace != 0.0 {
1808 words.push([prevspace, wd, ht as f64, dp as f64]);
1809 }
1810 Ok(words)
1811 }
1812
1813 fn compute_boxes_size_lines(
1817 wrapwidth: Option<i64>,
1818 baseline: i64,
1819 words: &[[f64; 4]],
1820 ) -> Vec<[i64; 4]> {
1821 let mut lines: Vec<[i64; 4]> = Vec::new();
1822 let fuzz = UNITY as f64; let (mut wd, mut ht, mut dp): (f64, i64, i64) = (0.0, 0, 0);
1824 for item in words {
1825 let (space, w, h, d) = (item[0], item[1], item[2] as i64, item[3] as i64);
1826 if space == -1.0 || wrapwidth.is_some_and(|ww| wd + space * 0.5 + w > ww as f64 + fuzz) {
1828 if wd != 0.0 {
1829 lines.push([baseline, kround(wd), ht, dp]);
1830 }
1831 wd = w;
1832 ht = h;
1833 dp = d;
1834 } else {
1835 wd += space + w;
1836 ht = max(ht, h);
1837 dp = max(dp, d);
1838 }
1839 }
1840 if wd != 0.0 || ht != 0 || dp != 0 {
1841 lines.push([baseline, kround(wd), ht, dp]);
1842 }
1843 lines
1844 }
1845
1846 fn compute_boxes_size_stack(vattach: &str, mathaxis: i64, lines: &[[i64; 4]]) -> (i64, i64, i64) {
1850 let nlines = lines.len();
1851 if nlines == 0 {
1852 return (0, 0, 0);
1853 }
1854 if nlines == 1 {
1855 let [_bs, w, h, d] = lines[0];
1856 return (w, h, d);
1857 }
1858 let lineskip = lookup_definition(&T_CS!("\\lineskip"))
1860 .ok()
1861 .flatten()
1862 .and_then(|def| def.value_of(Vec::new()))
1863 .map(|v| v.value_of())
1864 .unwrap_or(0);
1865 let mut wd: i64 = 0;
1866 let mut prevdepth: i64 = -99999;
1867 let mut th: i64 = 0;
1868 for line in lines {
1869 let [bs, w, h, d] = *line;
1870 wd = max(w, wd);
1871 th += h + d;
1872 if prevdepth >= 0 && bs >= 0 {
1873 if prevdepth + h < bs {
1874 th += bs - prevdepth - h;
1875 } else {
1876 th += lineskip;
1877 }
1878 }
1879 prevdepth = if bs >= 0 {
1885 d
1886 } else if bs == -1 {
1887 prevdepth
1888 } else {
1889 -99999
1890 };
1891 }
1892 let (ht, dp) = match vattach {
1893 "middle" => (th / 2 + mathaxis, th / 2 - mathaxis),
1894 "bottom" => {
1895 let d = lines[nlines - 1][3];
1896 (th - d, d)
1897 },
1898 _ => {
1900 let h = lines[0][2];
1901 (h, th - h)
1902 },
1903 };
1904 (wd, ht, dp)
1905 }
1906}
1907
1908fn flatten_for_sizing(_w: &Digested) -> Option<Vec<Digested>> { None }
1914
1915fn is_diff(x: Option<&Cow<str>>, y: Option<&Cow<str>>) -> bool {
1916 x.is_some() && (y.is_none() || (x != y))
1917}
1918
1919fn is_diff_opt_str(x: Option<&str>, y: Option<&str>) -> bool {
1920 x.is_some() && (y.is_none() || (x != y))
1921}
1922
1923fn is_diff_f64(x: Option<f64>, y: Option<f64>) -> bool { x.is_some() && (y.is_none() || (x != y)) }
1924
1925fn is_diff_color(x: Option<&Color>, y: Option<&Color>) -> bool {
1926 x.is_some() && (y.is_none() || (x != y))
1927}
1928
1929fn is_diff_font_color(x: Option<&Color>, y: Option<&Color>) -> bool {
1932 let cx = x.unwrap_or(&DEFCOLOR);
1933 let cy = y.unwrap_or(&DEFCOLOR);
1934 if cx == cy {
1935 return false;
1936 }
1937 cx.to_rgb() != cy.to_rgb()
1938}
1939
1940fn is_diff_font_color_ref(x: Option<&Color>, y: Option<&Color>) -> bool {
1947 let cx = x.unwrap_or(&DEFCOLOR);
1948 let cy = y.unwrap_or(&DEFCOLOR);
1949 cx != cy
1950}
1951
1952pub fn match_font(font1: &str, font2: &str) -> bool {
1956 if let Some(inner) = font1
1958 .strip_prefix("Font[")
1959 .and_then(|s| s.strip_suffix(']'))
1960 {
1961 let comps: Vec<&str> = inner.split(',').collect();
1962 let re_str = format!(
1963 "^Font\\[{}\\]$",
1964 comps
1965 .iter()
1966 .map(|c| if *c == "*" {
1967 "[^,]+".to_string()
1968 } else {
1969 regex::escape(c)
1970 })
1971 .collect::<Vec<_>>()
1972 .join(",")
1973 );
1974 if let Ok(re) = Regex::new(&re_str) {
1975 return re.is_match(font2);
1976 }
1977 }
1978 false
1979}
1980
1981pub fn font_match_xpaths(font: &str) -> String {
1984 if let Some(inner) = font.strip_prefix("Font[").and_then(|s| s.strip_suffix(']')) {
1985 let comps: Vec<&str> = inner.split(',').collect();
1986 let mut frags: Vec<String> = Vec::new();
1988 if !comps.is_empty() && comps[0] != "*" {
1989 frags.push(format!("[{},", comps[0]));
1990 }
1991 if comps.len() > 1 && comps[1] != "*" {
1992 frags.push(format!(",{},", comps[1]));
1993 }
1994 if comps.len() > 2 && comps[2] != "*" {
1995 frags.push(format!(",{},", comps[2]));
1996 }
1997 let mut parts: Vec<String> = vec!["@_font".to_string()];
1998 for frag in frags {
1999 parts.push(format!("contains(@_font,'{frag}')"));
2000 }
2001 parts.join(" and ")
2002 } else {
2003 "@_font".to_string()
2004 }
2005}
2006
2007fn push_fontmap_char(result: &mut String, c: char, _code: u8) {
2021 if c == '\u{1E9E}' {
2023 result.push_str("SS");
2024 return;
2025 }
2026 if is_combining_mark(c) {
2029 result.push('\u{00A0}');
2030 }
2031 result.push(c);
2032}
2033
2034fn is_combining_mark(c: char) -> bool {
2036 matches!(c as u32,
2037 0x0300..=0x036F | 0x1AB0..=0x1AFF | 0x1DC0..=0x1DFF | 0x20D0..=0x20FF | 0xFE20..=0xFE2F )
2043}
2044
2045pub fn decode_str(code: u8, encoding_opt: Option<String>, implicit: bool) -> Option<SymStr> {
2048 if let Some(s) = lookup_multichar_override(code, encoding_opt.as_deref()) {
2050 return Some(arena::pin(s));
2051 }
2052 if let Some(c) = decode(code, encoding_opt, implicit) {
2053 if c == '\u{1E9E}' {
2055 return Some(pin!("SS"));
2056 }
2057 if is_combining_mark(c) {
2060 return Some(arena::pin(format!("\u{00A0}{c}")));
2061 }
2062 Some(arena::pin_char(c))
2063 } else {
2064 None
2065 }
2066}
2067
2068fn lookup_multichar_override(code: u8, encoding_opt: Option<&str>) -> Option<String> {
2071 let encoding = match encoding_opt {
2072 Some(enc) if !enc.is_empty() => enc.to_string(),
2073 _ => {
2074 let font = lookup_font();
2075 font.and_then(|f| f.get_encoding().map(|e| e.to_string()))?
2076 },
2077 };
2078 if encoding.is_empty() {
2079 return None;
2080 }
2081 let _ = preload_font_map(&encoding);
2095 let mapname = format!("{encoding}_fontmap_multichar");
2096 with_value(&mapname, |val_opt| {
2097 if let Some(Stored::HashString(map)) = val_opt {
2098 map.get(&code.to_string()).cloned()
2099 } else {
2100 None
2101 }
2102 })
2103}
2104
2105pub fn decode(code: u8, encoding_opt: Option<String>, implicit: bool) -> Option<char> {
2106 let mut font = None;
2107 let encoding = match encoding_opt {
2108 Some(enc) => Cow::Owned(enc),
2109 None => {
2110 font = lookup_font();
2124 if let Some(ref font) = font {
2125 match font.get_encoding() {
2126 None => Cow::Borrowed("OT1"),
2127 Some(encoding) => encoding.clone(),
2128 }
2129 } else {
2130 Cow::Borrowed("OT1")
2131 }
2132 },
2133 };
2134
2135 let mut map: Option<Fontmap> = None;
2136 if !encoding.is_empty() {
2137 let _ = preload_font_map(&encoding); if let Some(encmap) = load_font_map(&encoding) {
2139 map = Some(encmap);
2141 if let Some(ref font) = font
2142 && let Some(family) = (*font).get_family()
2143 {
2144 with_value(&s!("{encoding}_{family}_fontmap"), |fmap_opt| {
2145 if let Some(fmap) = fmap_opt {
2146 map = fmap.into(); }
2148 });
2149 }
2150 }
2151 }
2152
2153 if implicit {
2154 if let Some(map) = map {
2155 if code < 128 {
2156 match map.get(code as usize) {
2157 None => None,
2158 Some(c) => *c,
2159 }
2160 } else {
2161 Some(code.into())
2162 }
2163 } else {
2164 Some(code.into())
2165 }
2166 } else if let Some(map) = map {
2167 match map.get(code as usize) {
2168 None => None,
2169 Some(c) => *c,
2170 }
2171 } else {
2172 None
2173 }
2174}
2175
2176pub fn decode_string(string: SymStr, encoding_opt: Option<&str>, implicit: bool) -> SymStr {
2177 let empty_sym = pin!("");
2178 if string == empty_sym {
2179 return empty_sym;
2180 }
2181 let mut font = None;
2182 let encoding = match encoding_opt {
2183 None => {
2184 font = lookup_font();
2185 if let Some(ref font) = font {
2186 font.get_encoding().unwrap_or(&Cow::Borrowed(""))
2187 } else {
2188 ""
2189 }
2190 },
2191 Some(encoding) => encoding,
2192 };
2193
2194 let mut map: Option<Fontmap> = None;
2195 if !encoding.is_empty() {
2196 let _ = preload_font_map(encoding); if let Some(encmap) = load_font_map(encoding) {
2198 map = Some(encmap);
2200 if let Some(ref font) = font
2201 && let Some(family) = (*font).get_family()
2202 {
2203 with_value(&s!("{}_{}_fontmap", encoding, family), |fmap_opt| {
2204 if let Some(fmap) = fmap_opt {
2205 map = fmap.into(); }
2207 });
2208 }
2209 }
2210 }
2211
2212 let multichar_map: Option<HashMap<String, String>> = if !encoding.is_empty() {
2214 let mapname = format!("{encoding}_fontmap_multichar");
2215 with_value(&mapname, |val_opt| {
2216 if let Some(Stored::HashString(m)) = val_opt {
2217 Some(m.clone())
2218 } else {
2219 None
2220 }
2221 })
2222 } else {
2223 None
2224 };
2225
2226 let mut result_string: String = String::new();
2227 arena::with(string, |str| {
2228 for c in str.chars() {
2229 if implicit {
2230 if let Some(ref map_ref) = map {
2231 let code = c as u16; if code < 128 {
2233 if let Some(ref mc) = multichar_map
2235 && let Some(mc_str) = mc.get(&(code as u8).to_string())
2236 {
2237 result_string.push_str(mc_str);
2238 continue;
2239 }
2240 if let Some(Some(mapc_val)) = map_ref.get(code as usize) {
2241 push_fontmap_char(&mut result_string, *mapc_val, code as u8);
2242 }
2243 } else {
2244 result_string.push(c);
2245 }
2246 } else {
2247 result_string.push(c)
2248 }
2249 } else if let Some(ref map_ref) = map {
2250 let code = c as u8;
2251 if let Some(ref mc) = multichar_map
2253 && let Some(mc_str) = mc.get(&code.to_string())
2254 {
2255 result_string.push_str(mc_str);
2256 continue;
2257 }
2258 if let Some(Some(mapc_val)) = map_ref.get(code as usize) {
2259 push_fontmap_char(&mut result_string, *mapc_val, code);
2260 }
2261 }
2262 }
2263 });
2264 arena::pin(result_string)
2265}
2266
2267pub fn rationalize_font_size(size: &str) -> f64 {
2269 if let Some(symbolic) = FONT_SIZE.get(size) {
2270 *symbolic * defsize()
2271 } else {
2272 size.parse::<f64>().unwrap_or_else(|_| defsize())
2274 }
2275}
2276
2277pub fn relative_font_size(newsize: f64, oldsize: f64) -> String {
2279 s!("{}%", (0.5 + 100.0 * newsize / oldsize).floor())
2280}
2281
2282#[cfg(test)]
2283mod tests {
2284 use super::*;
2285
2286 #[test]
2287 fn relative_font_size_same_is_100() {
2288 assert_eq!(relative_font_size(10.0, 10.0), "100%");
2289 assert_eq!(relative_font_size(12.0, 12.0), "100%");
2290 }
2291
2292 #[test]
2293 fn relative_font_size_doubled_is_200() {
2294 assert_eq!(relative_font_size(20.0, 10.0), "200%");
2295 }
2296
2297 #[test]
2298 fn relative_font_size_half_is_50() {
2299 assert_eq!(relative_font_size(5.0, 10.0), "50%");
2300 }
2301
2302 #[test]
2307 fn defsize_preserves_fractional_nominal_font_size() {
2308 use crate::{
2309 common::float::Float,
2310 state::{State, StateOptions, assign_value, set_state},
2311 };
2312 set_state(State::new(StateOptions::default()));
2313 assert_eq!(defsize(), 10.0, "defsize defaults to 10 when unset");
2315 assign_value("NOMINAL_FONT_SIZE", Float(10.95), None);
2317 assert_eq!(
2318 defsize(),
2319 10.95,
2320 "defsize preserves the fractional 11pt size"
2321 );
2322 assign_value("NOMINAL_FONT_SIZE", Float(12.0), None);
2324 assert_eq!(defsize(), 12.0, "12pt reads back as 12.0");
2325 }
2326
2327 #[test]
2328 fn match_font_exact_wildcard_tail() {
2329 assert!(match_font("Font[*,*,*,*]", "Font[rm,med,up,10]"));
2334 }
2335
2336 #[test]
2337 fn match_font_exact_match() {
2338 assert!(match_font("Font[rm,med,up,10]", "Font[rm,med,up,10]"));
2339 assert!(!match_font("Font[rm,med,up,10]", "Font[sf,med,up,10]"));
2340 }
2341
2342 #[test]
2343 fn match_font_partial_wildcard() {
2344 assert!(match_font("Font[*,med,up,10]", "Font[rm,med,up,10]"));
2346 assert!(match_font("Font[*,med,up,10]", "Font[sf,med,up,10]"));
2347 assert!(!match_font("Font[*,bold,up,10]", "Font[rm,med,up,10]"));
2349 }
2350
2351 #[test]
2352 fn match_font_malformed_input() {
2353 assert!(!match_font("not_a_font", "Font[rm,med,up,10]"));
2355 }
2356
2357 #[test]
2358 fn font_match_xpaths_all_wildcards_is_attr_only() {
2359 let xp = font_match_xpaths("Font[*,*,*,*]");
2360 assert_eq!(xp, "@_font");
2362 }
2363
2364 #[test]
2365 fn font_match_xpaths_includes_specified_components() {
2366 let xp = font_match_xpaths("Font[rm,bold,*,*]");
2367 assert!(xp.contains("@_font"));
2369 assert!(xp.contains("contains"));
2370 assert!(xp.contains("rm"));
2371 assert!(xp.contains("bold"));
2372 }
2373
2374 #[test]
2375 fn font_match_xpaths_malformed_is_empty_or_fallback() {
2376 let xp = font_match_xpaths("garbage");
2377 let _ = xp;
2381 }
2382}