1use crate::charset::{self, Charset};
6use serde::{Deserialize, Serialize};
7
8pub const DEFAULT_FONT_NAME: &str = "Times-Roman";
10
11pub const DEFAULT_FONT_SIZE: f64 = 14.0;
13
14pub const DEFAULT_LABEL_FONT_SIZE: f64 = 11.0;
16
17pub const MIN_FONT_SIZE: f64 = 1.0;
19
20pub const LINESPACING: f64 = 1.20;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
25pub enum TextJustify {
26 #[default]
28 Center,
29 Left,
31 Right,
33}
34
35impl TextJustify {
36 #[inline]
38 pub const fn as_char(self) -> char {
39 match self {
40 Self::Center => 'n',
41 Self::Left => 'l',
42 Self::Right => 'r',
43 }
44 }
45
46 #[inline]
48 pub const fn from_char(c: char) -> Self {
49 match c {
50 'l' => Self::Left,
51 'r' => Self::Right,
52 _ => Self::Center,
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct TextLineLayout {
60 pub text: String,
62 pub width: f64,
64 pub height: f64,
66 pub justify: TextJustify,
68}
69
70#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
72pub struct TextLabelLayout {
73 pub lines: Vec<TextLineLayout>,
75 pub line_widths: Vec<f64>,
77 pub width: f64,
79 pub height: f64,
81 pub font_name: String,
83 pub font_size: f64,
85 #[serde(default)]
88 pub source: String,
89}
90
91#[derive(Debug, Clone, Default, PartialEq, Eq)]
93pub struct LabelSubstContext<'a> {
94 pub node_name: &'a str,
95 pub graph_name: &'a str,
96 pub tail_name: &'a str,
97 pub head_name: &'a str,
98 pub is_edge: bool,
99 pub is_directed: bool,
100 pub tail_port: Option<&'a str>,
101 pub head_port: Option<&'a str>,
102 pub label_text: &'a str,
103 pub pen_color: Option<&'a str>,
106 pub is_node: bool,
108 pub has_label: bool,
111}
112
113impl<'a> LabelSubstContext<'a> {
114 #[inline]
116 pub const fn for_node(node_name: &'a str) -> Self {
117 Self {
118 node_name,
119 graph_name: "",
120 tail_name: "",
121 head_name: "",
122 is_edge: false,
123 is_directed: true,
124 tail_port: None,
125 head_port: None,
126 label_text: "",
127 pen_color: None,
128 is_node: true,
129 has_label: false,
130 }
131 }
132}
133
134pub(crate) struct FontFamilyMetrics {
137 pub(crate) font_names: &'static [&'static str],
139 pub(crate) units_per_em: f64,
141 pub(crate) widths_regular: [i16; 128],
142 pub(crate) widths_bold: [i16; 128],
143 pub(crate) widths_italic: [i16; 128],
144 pub(crate) widths_bold_italic: [i16; 128],
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub struct PostscriptFontAlias {
150 pub name: &'static str,
151 pub family: &'static str,
152 pub weight: Option<&'static str>,
153 pub stretch: Option<&'static str>,
154 pub style: Option<&'static str>,
155 pub xfig_code: i32,
156 pub svg_font_family: &'static str,
157 pub svg_font_weight: Option<&'static str>,
158 pub svg_font_style: Option<&'static str>,
159}
160
161#[rustfmt::skip]
162#[path = "font_tables.rs"]
163mod font_tables;
164
165pub const POSTSCRIPT_FONT_ALIASES: &[PostscriptFontAlias] = &font_tables::POSTSCRIPT_ALIASES;
168
169fn strcasecmp(a: &str, b: &str) -> std::cmp::Ordering {
171 let la = a.bytes().map(|c| c.to_ascii_lowercase());
172 let lb = b.bytes().map(|c| c.to_ascii_lowercase());
173 la.cmp(lb)
174}
175
176pub fn translate_postscript_fontname(font_name: &str) -> Option<&'static PostscriptFontAlias> {
179 POSTSCRIPT_FONT_ALIASES
180 .binary_search_by(|alias| strcasecmp(alias.name, font_name))
181 .ok()
182 .map(|i| &POSTSCRIPT_FONT_ALIASES[i])
183}
184
185fn font_name_equal_permissive(a: &str, b: &str) -> bool {
188 let a_iter = a
189 .bytes()
190 .filter(u8::is_ascii_alphabetic)
191 .map(|c| c.to_ascii_lowercase());
192 let b_iter = b
193 .bytes()
194 .filter(u8::is_ascii_alphabetic)
195 .map(|c| c.to_ascii_lowercase());
196 a_iter.eq(b_iter)
197}
198
199fn font_in_list_permissive(value: &str, list: &[&str]) -> bool {
201 list.iter()
202 .any(|entry| font_name_equal_permissive(value, entry))
203}
204
205fn get_metrics_for_font_family(font_name: &str) -> &'static FontFamilyMetrics {
208 font_tables::FONT_METRICS
209 .iter()
210 .find(|m| font_in_list_permissive(font_name, m.font_names))
211 .unwrap_or_else(|| {
212 let warning = format!(
213 "Warning: no hard-coded metrics for '{font_name}'. Falling back to 'Times' metrics\n"
214 );
215 if crate::diag::reporting() && crate::diag::emit_once(&warning) {
217 crate::diag::agwarningf(&warning);
218 }
219 &font_tables::FONT_METRICS[0]
220 })
221}
222
223fn get_metrics_for_font_variant(
225 family_metrics: &'static FontFamilyMetrics,
226 bold: bool,
227 italic: bool,
228) -> &'static [i16; 128] {
229 if bold && italic {
230 &family_metrics.widths_bold_italic
231 } else if bold {
232 &family_metrics.widths_bold
233 } else if italic {
234 &family_metrics.widths_italic
235 } else {
236 &family_metrics.widths_regular
237 }
238}
239
240#[inline]
244fn estimate_character_width_canonical(variant_metrics: &[i16; 128], byte: u8) -> u16 {
245 use std::sync::atomic::{AtomicBool, Ordering};
246 static NON_ASCII_WARNED: AtomicBool = AtomicBool::new(false);
247 static NO_WIDTH_WARNED: AtomicBool = AtomicBool::new(false);
248 let character = if byte >= 128 {
249 if crate::diag::reporting() && !NON_ASCII_WARNED.swap(true, Ordering::Relaxed) {
250 eprintln!(
251 "Warning: Warning: no value for width of non-ASCII character {byte}. Falling \
252 back to width of space character"
253 );
254 }
255 b' '
256 } else {
257 byte
258 };
259 let width = variant_metrics[usize::from(character)];
260 if width == -1 {
261 if crate::diag::reporting() && !NO_WIDTH_WARNED.swap(true, Ordering::Relaxed) {
262 eprintln!(
263 "Warning: Warning: no value for width of ASCII character {character}. Falling \
264 back to 0"
265 );
266 }
267 0
268 } else {
269 width as u16
270 }
271}
272
273pub fn estimate_text_width_1pt(font_name: &str, text: &str, bold: bool, italic: bool) -> f64 {
280 let family_metrics = get_metrics_for_font_family(font_name);
281 let variant_metrics = get_metrics_for_font_variant(family_metrics, bold, italic);
282 let text_width_canonical: u32 = charset::encode(text)
283 .iter()
284 .map(|&b| u32::from(estimate_character_width_canonical(variant_metrics, b)))
285 .sum();
286 f64::from(text_width_canonical) / family_metrics.units_per_em
287}
288
289fn html_entity(s: &[u8]) -> (u32, usize) {
293 let at = |i: usize| s.get(i).copied().unwrap_or(0);
294 if at(0) == b'#' {
295 let mut n: u32 = 0;
296 let mut byte;
297 let mut i;
298 if at(1) == b'x' || at(1) == b'X' {
299 i = 2;
300 loop {
301 byte = at(i);
302 if i >= 8 {
303 break;
304 }
305 let d = match byte {
306 b'A'..=b'F' => byte - b'A' + 10,
307 b'a'..=b'f' => byte - b'a' + 10,
308 b'0'..=b'9' => byte - b'0',
309 _ => break,
310 };
311 n = n.wrapping_mul(16).wrapping_add(u32::from(d));
312 i += 1;
313 }
314 } else {
315 i = 1;
316 loop {
317 byte = at(i);
318 if i >= 8 {
319 break;
320 }
321 if byte.is_ascii_digit() {
322 n = n.wrapping_mul(10).wrapping_add(u32::from(byte - b'0'));
323 } else {
324 break;
325 }
326 i += 1;
327 }
328 }
329 if i < 8 && byte == b';' {
332 (n, i + 1)
333 } else {
334 (0, 0)
335 }
336 } else {
337 for i in 0..font_tables::ENTITY_NAME_LENGTH_MAX {
338 let byte = at(i);
339 if byte == 0 {
340 break;
341 }
342 if byte == b';' {
343 let key = &s[..i];
344 if let Ok(idx) = font_tables::HTML_ENTITIES
345 .binary_search_by(|(name, _)| name.as_bytes().cmp(key))
346 {
347 return (font_tables::HTML_ENTITIES[idx].1, i + 1);
348 }
349 break;
350 }
351 }
352 (0, 0)
353 }
354}
355
356fn push_entity_bytes(out: &mut Vec<u8>, v: u32) {
359 if v < 0x7F {
360 out.push(v as u8);
361 } else if v < 0x07FF {
362 out.push(((v >> 6) | 0xC0) as u8);
363 out.push(((v & 0x3F) | 0x80) as u8);
364 } else {
365 out.push(((v >> 12) | 0xE0) as u8);
366 out.push((((v >> 6) & 0x3F) | 0x80) as u8);
367 out.push(((v & 0x3F) | 0x80) as u8);
368 }
369}
370
371pub fn html_entity_utf8(text: &str) -> String {
376 if !text.contains('&') && !charset::has_raw(text) {
377 return text.to_string();
378 }
379 let s = charset::encode(text);
380 let s: &[u8] = &s;
381 let at = |i: usize| s.get(i).copied().unwrap_or(0);
382 let mut out = Vec::with_capacity(s.len());
383 let mut i = 0;
384 while i < s.len() {
385 let mut c = s[i];
386 i += 1;
387 let uc: i32 = if c < 0xC0 {
388 0
389 } else if c < 0xE0 {
390 1
391 } else if c < 0xF0 {
392 2
393 } else if c < 0xF8 {
394 3
395 } else {
396 charset::warn_invalid_utf8(|g| {
397 format!(
398 "Warning: UTF8 codes > 4 bytes are not currently supported (graph {g}) - \
399 treated as Latin-1. Perhaps \"-Gcharset=latin1\" is needed?\n"
400 )
401 });
402 c = cvt_and_append(c, &mut out);
403 -1
404 };
405 if uc == 0 && c == b'&' {
406 let (v, used) = html_entity(&s[i..]);
407 i += used;
408 if v != 0 {
409 if v < 0x7F {
410 c = v as u8;
411 } else if v < 0x07FF {
412 out.push(((v >> 6) | 0xC0) as u8);
413 c = ((v & 0x3F) | 0x80) as u8;
414 } else {
415 out.push(((v >> 12) | 0xE0) as u8);
416 out.push((((v >> 6) & 0x3F) | 0x80) as u8);
417 c = ((v & 0x3F) | 0x80) as u8;
418 }
419 }
420 } else {
421 for _ in 0..uc.max(0) {
422 if at(i) & 0xC0 == 0x80 {
423 out.push(c);
424 c = at(i);
425 i += 1;
426 } else {
427 charset::warn_invalid_utf8(|g| {
428 format!(
429 "Warning: Invalid {}-byte UTF8 found in input of graph {g} - \
430 treated as Latin-1. Perhaps \"-Gcharset=latin1\" is needed?\n",
431 uc + 1
432 )
433 });
434 c = cvt_and_append(c, &mut out);
435 break;
436 }
437 }
438 }
439 out.push(c);
440 }
441 charset::decode(out)
442}
443
444fn cvt_and_append(c: u8, out: &mut Vec<u8>) -> u8 {
447 let mut conv = Vec::with_capacity(3);
448 push_entity_bytes(&mut conv, u32::from(c));
449 let last = conv.pop().unwrap_or(c);
450 out.extend_from_slice(&conv);
451 last
452}
453
454pub fn latin1_to_utf8(text: &str) -> String {
458 let s = charset::encode(text);
459 let s: &[u8] = &s;
460 let mut out = Vec::with_capacity(s.len());
461 let mut i = 0;
462 while i < s.len() {
463 let mut v = u32::from(s[i]);
464 i += 1;
465 if v == u32::from(b'&') {
466 let (ev, used) = html_entity(&s[i..]);
467 i += used;
468 v = if ev == 0 { u32::from(b'&') } else { ev };
469 }
470 push_entity_bytes(&mut out, v);
471 }
472 charset::decode(out)
473}
474
475pub fn convert_label_text(text: &str, cs: Charset) -> String {
478 match cs {
479 Charset::Latin1 => latin1_to_utf8(text),
480 Charset::Utf8 | Charset::Big5 => html_entity_utf8(text),
481 }
482}
483
484pub fn decode_html_entities(text: &str) -> String {
487 html_entity_utf8(text)
488}
489
490pub fn estimate_text_size(text: &str, font_name: &str, font_size: f64) -> (f64, f64) {
493 estimate_text_size_styled(text, font_name, font_size, false, false)
494}
495
496pub fn estimate_text_size_styled(
499 text: &str,
500 font_name: &str,
501 font_size: f64,
502 bold: bool,
503 italic: bool,
504) -> (f64, f64) {
505 let width = font_size * estimate_text_width_1pt(font_name, text, bold, italic);
506 let height = font_size * LINESPACING;
507 (width, height)
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
513enum SubstObjKind {
514 Graph,
515 Node,
516 Edge,
517}
518
519impl LabelSubstContext<'_> {
520 fn obj_kind(&self) -> SubstObjKind {
521 if self.is_edge {
522 SubstObjKind::Edge
523 } else if self.is_node || !self.node_name.is_empty() {
524 SubstObjKind::Node
525 } else {
526 SubstObjKind::Graph
527 }
528 }
529}
530
531fn strdup_and_subst_obj0(s: &str, ctx: &LabelSubstContext<'_>, esc_backslash: bool) -> String {
539 if !s.contains('\\') {
540 return s.to_string();
541 }
542 let kind = ctx.obj_kind();
543 let g_str = ctx.graph_name;
544 let n_str = if kind == SubstObjKind::Node {
545 ctx.node_name
546 } else {
547 "\\N"
548 };
549 let (t_str, h_str) = if kind == SubstObjKind::Edge {
550 (ctx.tail_name, ctx.head_name)
551 } else {
552 ("\\T", "\\H")
553 };
554 let l_str = if !ctx.has_label && ctx.label_text.is_empty() {
555 "\\L"
556 } else {
557 ctx.label_text
558 };
559 let mut out = String::with_capacity(s.len());
560 let mut chars = s.chars();
561 while let Some(c) = chars.next() {
562 if c != '\\' {
563 out.push(c);
564 continue;
565 }
566 let Some(next) = chars.clone().next() else {
567 out.push('\\');
568 continue;
569 };
570 chars.next();
571 match next {
572 'G' => out.push_str(g_str),
573 'N' => out.push_str(n_str),
574 'E' => {
575 if kind == SubstObjKind::Edge {
576 out.push_str(t_str);
577 if let Some(tp) = ctx.tail_port.filter(|p| !p.is_empty()) {
578 out.push(':');
579 out.push_str(tp);
580 }
581 out.push_str(if ctx.is_directed { "->" } else { "--" });
582 out.push_str(h_str);
583 if let Some(hp) = ctx.head_port.filter(|p| !p.is_empty()) {
584 out.push(':');
585 out.push_str(hp);
586 }
587 }
588 }
589 'T' => out.push_str(t_str),
590 'H' => out.push_str(h_str),
591 'L' => out.push_str(l_str),
592 '\\' if esc_backslash => out.push('\\'),
593 other => {
594 out.push('\\');
595 out.push(other);
596 }
597 }
598 }
599 out
600}
601
602pub fn substitute_object_escapes(label: &str, ctx: &LabelSubstContext<'_>) -> String {
606 strdup_and_subst_obj0(label, ctx, false)
607}
608
609pub fn strdup_and_subst_obj(s: &str, ctx: &LabelSubstContext<'_>) -> String {
612 strdup_and_subst_obj0(s, ctx, true)
613}
614
615fn storeline(layout: &mut TextLabelLayout, line: String, terminator: TextJustify) {
619 let (width, height) = if line.is_empty() {
620 (0.0, (layout.font_size * LINESPACING).trunc())
621 } else {
622 estimate_text_size(&line, &layout.font_name, layout.font_size)
623 };
624 layout.width = layout.width.max(width);
625 layout.height += height;
626 layout.line_widths.push(width);
627 layout.lines.push(TextLineLayout {
628 text: line,
629 width,
630 height,
631 justify: terminator,
632 });
633}
634
635pub fn make_simple_label(text: &str, font_name: &str, font_size: f64) -> TextLabelLayout {
639 make_simple_label_cs(text, font_name, font_size, Charset::Utf8)
640}
641
642pub fn make_simple_label_cs(
645 text: &str,
646 font_name: &str,
647 font_size: f64,
648 cs: Charset,
649) -> TextLabelLayout {
650 let mut layout = TextLabelLayout {
651 font_name: font_name.to_string(),
652 font_size,
653 ..TextLabelLayout::default()
654 };
655 if text.is_empty() {
656 return layout;
657 }
658 let bytes = charset::encode(text);
659 let s: &[u8] = &bytes;
660 let big5 = cs == Charset::Big5;
661 let mut line: Vec<u8> = Vec::new();
662 let mut pending_nul = false;
665 let take = |line: &mut Vec<u8>| charset::decode(std::mem::take(line));
666 let mut i = 0;
667 while i < s.len() {
668 let c = s[i];
669 i += 1;
670 if big5 && (0xA1..=0xFE).contains(&c) {
671 line.push(c);
672 match s.get(i) {
673 Some(&n) => {
674 line.push(n);
675 i += 1;
676 }
677 None => break,
679 }
680 } else if c == b'\\' {
681 match s.get(i) {
682 Some(&j @ (b'n' | b'l' | b'r')) => {
683 i += 1;
684 storeline(
685 &mut layout,
686 take(&mut line),
687 TextJustify::from_char(char::from(j)),
688 );
689 pending_nul = false;
690 }
691 Some(&other) => {
692 i += 1;
693 if !pending_nul {
694 line.push(other);
695 }
696 }
697 None => pending_nul = true,
698 }
699 } else if c == b'\n' {
700 storeline(&mut layout, take(&mut line), TextJustify::Center);
701 pending_nul = false;
702 } else if !pending_nul {
703 line.push(c);
704 }
705 }
706 if !line.is_empty() || pending_nul {
707 storeline(&mut layout, take(&mut line), TextJustify::Center);
708 }
709 layout
710}
711
712pub fn make_text_label(
716 label: &str,
717 ctx: &LabelSubstContext<'_>,
718 font_name: &str,
719 font_size: f64,
720 cs: Charset,
721) -> TextLabelLayout {
722 let substituted = strdup_and_subst_obj0(label, ctx, false);
725 let converted = convert_label_text(&substituted, cs);
726 let mut layout = make_simple_label_cs(&converted, font_name, font_size, cs);
727 layout.source = converted;
728 layout
729}
730
731pub fn charset_is_latin1(charset: Option<&str>) -> bool {
735 Charset::from_attr(charset) == Charset::Latin1
736}
737
738pub fn measure_multiline_label(
740 label: &str,
741 node_name: &str,
742 font_name: &str,
743 font_size: f64,
744) -> TextLabelLayout {
745 let ctx = LabelSubstContext::for_node(node_name);
746 measure_multiline_label_with_context(label, &ctx, font_name, font_size)
747}
748
749pub fn measure_multiline_label_with_context(
752 label: &str,
753 ctx: &LabelSubstContext<'_>,
754 font_name: &str,
755 font_size: f64,
756) -> TextLabelLayout {
757 make_text_label(label, ctx, font_name, font_size, charset::current())
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763
764 #[test]
765 fn test_exact_character_widths_times_helvetica_courier() {
766 let w_times_a = estimate_text_width_1pt("Times-Roman", "A", false, false);
768 assert!((w_times_a - 1479.0 / 2048.0).abs() < 1e-12);
769
770 let w_times_bold_a = estimate_text_width_1pt("Times-Bold", "A", false, false);
774 assert!((w_times_bold_a - w_times_a).abs() < 1e-12);
775 let w_times_italic_a = estimate_text_width_1pt("Times", "A", false, true);
776 assert!((w_times_italic_a - 1251.0 / 2048.0).abs() < 1e-12);
777
778 let w_helv_a = estimate_text_width_1pt("Helvetica", "A", false, false);
780 assert!((w_helv_a - 1366.0 / 2048.0).abs() < 1e-12);
781
782 let w_helv_bold_a = estimate_text_width_1pt("Helvetica-Bold", "A", false, false);
784 assert!((w_helv_bold_a - w_times_a).abs() < 1e-12);
785 let w_helv_bold_a = estimate_text_width_1pt("Helvetica", "A", true, false);
786 assert!((w_helv_bold_a - 1479.0 / 2048.0).abs() < 1e-12);
787
788 let w_cour = estimate_text_width_1pt("Courier", "Hello", false, false);
790 assert!((w_cour - (5.0 * 1229.0 / 2048.0)).abs() < 1e-12);
791 let w_cour_bold = estimate_text_width_1pt("Courier New", "Hello", true, false);
792 assert!((w_cour_bold - (5.0 * 1229.0 / 2048.0)).abs() < 1e-12);
793
794 let w_symbol_a = estimate_text_width_1pt("Symbol", "A", false, false);
796 assert!((w_symbol_a - w_times_a).abs() < 1e-12);
797 }
798
799 #[test]
800 fn test_estimate_text_size_points() {
801 let (w, h) = estimate_text_size("abc", "Courier", 14.0);
802 let expected_w = 14.0 * (3.0 * 1229.0 / 2048.0);
803 let expected_h = 14.0 * 1.20;
804 assert!((w - expected_w).abs() < 1e-9);
805 assert!((h - expected_h).abs() < 1e-9);
806 }
807
808 #[test]
809 fn test_measure_multiline_label_escapes_and_justifications() {
810 let layout =
811 measure_multiline_label("\\N\\nline two\\lright\\r", "node_1", "Courier", 10.0);
812 assert_eq!(layout.lines.len(), 3);
813 assert_eq!(layout.line_widths.len(), 3);
814
815 assert_eq!(layout.lines[0].text, "node_1");
816 assert_eq!(layout.lines[0].justify, TextJustify::Center);
817
818 assert_eq!(layout.lines[1].text, "line two");
819 assert_eq!(layout.lines[1].justify, TextJustify::Left);
820
821 assert_eq!(layout.lines[2].text, "right");
822 assert_eq!(layout.lines[2].justify, TextJustify::Right);
823
824 let char_w = 10.0 * (1229.0 / 2048.0);
826 assert!((layout.line_widths[0] - 6.0 * char_w).abs() < 1e-9);
827 assert!((layout.line_widths[1] - 8.0 * char_w).abs() < 1e-9);
828 assert!((layout.line_widths[2] - 5.0 * char_w).abs() < 1e-9);
829 assert!((layout.width - 8.0 * char_w).abs() < 1e-9);
830 assert!((layout.height - 3.0 * 10.0 * LINESPACING).abs() < 1e-9);
831 }
832
833 #[test]
834 fn test_measure_multiline_label_context_escapes() {
835 let ctx = LabelSubstContext {
836 node_name: "n1",
837 graph_name: "G_main",
838 tail_name: "src",
839 head_name: "dst",
840 is_edge: true,
841 is_directed: true,
842 tail_port: None,
843 head_port: None,
844 label_text: "",
845 pen_color: None,
846 is_node: false,
847 has_label: false,
848 };
849 let layout = measure_multiline_label_with_context(
850 "\\G: \\T -> \\H (\\E)",
851 &ctx,
852 "Times-Roman",
853 14.0,
854 );
855 assert_eq!(layout.lines.len(), 1);
856 assert_eq!(layout.lines[0].text, "G_main: src -> dst (src->dst)");
857 }
858
859 #[test]
860 fn utf8_is_measured_per_byte_like_c() {
861 let (w, _) = estimate_text_size("é€x", "Times-Roman", 14.0);
863 assert!((w - 24.5).abs() < 1e-9);
864 }
865
866 #[test]
867 fn empty_line_height_is_truncated_like_c() {
868 let l = make_simple_label("a\\n\\nb", "Times-Roman", 14.0);
869 assert_eq!(l.lines.len(), 3);
870 assert!((l.lines[1].height - 16.0).abs() < 1e-12);
871 assert!((l.height - (16.8 + 16.0 + 16.8)).abs() < 1e-9);
872 }
873
874 #[test]
875 fn html_entities_follow_c_rules() {
876 assert_eq!(html_entity_utf8("a&b"), "a&b");
877 assert_eq!(html_entity_utf8("AB"), "AB");
878 assert_eq!(html_entity_utf8("&bogus;"), "&bogus;");
879 assert_eq!(html_entity_utf8("�x"), "&x");
880 assert_eq!(html_entity_utf8("α"), "\u{3b1}");
881 }
882
883 #[test]
884 fn charset_conversions_run_on_input_bytes() {
885 let raw = |b: &[u8]| charset::decode(b.to_vec());
886 assert_eq!(
888 latin1_to_utf8(&raw(b"caf\xe9 é")),
889 "caf\u{e9} \u{e9}"
890 );
891 assert_eq!(latin1_to_utf8("\u{e9}"), "\u{c3}\u{a9}");
892 assert_eq!(html_entity_utf8(&raw(b"caf\xe9 x")), "caf\u{e9} x");
894 assert_eq!(html_entity_utf8(&raw(b"\xff")), "\u{ff}");
895 let out = html_entity_utf8(&raw(b"\xe4\xbcz"));
898 assert_eq!(&*charset::encode(&out), b"\xe4\xc2\xbcz");
899 let w = |t: &str| estimate_text_width_1pt("Times-Roman", t, false, false);
901 assert_eq!(w(&raw(b"\xa4")), w(" "));
902 }
903
904 #[test]
905 fn big5_pairs_bytes_in_make_simple_label() {
906 let l = make_simple_label_cs("\u{4e2d}\\nX\\lY", "Times-Roman", 14.0, Charset::Big5);
908 assert_eq!(l.lines.len(), 2);
909 assert_eq!(l.lines[0].text, "\u{4e2d}\\nX");
910 let u = make_simple_label_cs("\u{4e2d}\\nX\\lY", "Times-Roman", 14.0, Charset::Utf8);
911 assert_eq!(u.lines.len(), 3);
912 }
913
914 #[test]
915 fn subst_leaves_inapplicable_escapes() {
916 let ctx = LabelSubstContext::for_node("n");
917 let l = make_text_label("\\N \\T \\L", &ctx, "Times-Roman", 14.0, Charset::Utf8);
918 assert_eq!(l.lines[0].text, "n T L");
919 }
920}