c2pdf/
text_manipulation.rs1use std::collections::HashMap;
4
5use fontdue::{Font, FontSettings};
6use printpdf::Pt;
7
8pub fn split_into_lines_fontdue<F: Fn(usize) -> Pt>(
10 txt: &str,
11 font: &Font,
12 font_size: f32,
13 max_width: F,
14 cache: &mut std::collections::HashMap<char, f32>,
15) -> Vec<(String, f32)> {
16 let mut lines: Vec<(String, f32)> = vec![];
17 let mut line_buf = String::new();
18 let mut current_line_width = 0.0;
19 let mut max_line_width = max_width(0).0;
21 for ch in txt.chars() {
22 let width = match cache.get(&ch) {
23 Some(w) => *w,
24 None => {
25 let width = font.rasterize(ch, font_size).0.advance_width;
26 cache.insert(ch, width);
27 width
28 }
29 };
30 if (current_line_width + width >= max_line_width)
32 || ((max_line_width - (current_line_width + width) < 30.0) && ch.is_whitespace())
33 {
34 line_buf.push('\n');
36 lines.push((line_buf.trim_start().to_string(), current_line_width));
37 max_line_width = max_width(lines.len()).0;
39 line_buf.clear();
40 current_line_width = 0.0;
41 }
42 line_buf.push(ch);
43 current_line_width += width;
44 }
45 lines.push((line_buf.trim().to_string(), current_line_width));
46 lines
47}
48
49#[derive(Clone)]
51pub struct TextWrapper {
52 rasterize_cache: HashMap<char, f32>,
53 font: Font,
54 font_size: f32,
55}
56
57impl TextWrapper {
58 pub fn new(font_bytes: &[u8], font_size: f32) -> Self {
60 Self {
61 rasterize_cache: HashMap::new(),
62 font: Font::from_bytes(font_bytes, FontSettings::default()).unwrap(),
63 font_size,
64 }
65 }
66
67 pub fn split_into_lines<T: Fn(usize) -> Pt>(
69 &mut self,
70 txt: &str,
71 max_width: T,
72 ) -> Vec<(String, f32)> {
73 split_into_lines_fontdue(
74 txt,
75 &self.font,
76 self.font_size,
77 max_width,
78 &mut self.rasterize_cache,
79 )
80 }
81
82 pub fn get_width(&mut self, txt: &str) -> Pt {
84 let mut total_width = 0.0;
85 for ch in txt.chars() {
86 let char_width = match self.rasterize_cache.get(&ch) {
87 Some(w) => *w,
88 None => {
89 let width = self.font.rasterize(ch, self.font_size).0.advance_width;
90 self.rasterize_cache.insert(ch, width);
91 width
92 }
93 };
94 total_width += char_width;
95 }
96 Pt(total_width)
97 }
98 pub fn font_size(&self) -> f32 {
100 self.font_size
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 const FONT_BYTES: &[u8] = include_bytes!("../../fonts/Helvetica.ttf") as &[u8];
109 const TEXT: &str = "Hello World!! This is a vaguely long string to test string splitting!";
110 #[test]
111 fn splitting_lines() {
112 let result = split_into_lines_fontdue(
113 TEXT,
114 &Font::from_bytes(FONT_BYTES, FontSettings::default()).unwrap(),
115 20.0,
116 |_| Pt(100.0),
117 &mut HashMap::new(),
118 );
119 assert_eq!(result.len(), 7);
120 assert_eq!(
122 result
123 .iter()
124 .map(|x| x.0.clone())
125 .collect::<Vec<String>>()
126 .join("")
127 .replace([' ', '\n'], ""),
128 TEXT.replace(' ', "")
129 );
130 }
131}