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
49pub struct TextWrapper {
51 rasterize_cache: HashMap<char, f32>,
52 font: Font,
53 font_size: f32,
54}
55
56impl TextWrapper {
57 pub fn new(font_bytes: &[u8], font_size: f32) -> Self {
59 Self {
60 rasterize_cache: HashMap::new(),
61 font: Font::from_bytes(font_bytes, FontSettings::default()).unwrap(),
62 font_size,
63 }
64 }
65
66 pub fn split_into_lines<T: Fn(usize) -> Pt>(
68 &mut self,
69 txt: &str,
70 max_width: T,
71 ) -> Vec<(String, f32)> {
72 split_into_lines_fontdue(
73 txt,
74 &self.font,
75 self.font_size,
76 max_width,
77 &mut self.rasterize_cache,
78 )
79 }
80
81 pub fn get_width(&mut self, txt: &str) -> Pt {
83 let mut total_width = 0.0;
84 for ch in txt.chars() {
85 let char_width = match self.rasterize_cache.get(&ch) {
86 Some(w) => *w,
87 None => {
88 let width = self.font.rasterize(ch, self.font_size).0.advance_width;
89 self.rasterize_cache.insert(ch, width);
90 width
91 }
92 };
93 total_width += char_width;
94 }
95 Pt(total_width)
96 }
97 pub fn font_size(&self) -> f32 {
99 self.font_size
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 const FONT_BYTES: &[u8] = include_bytes!("../../fonts/Helvetica.ttf") as &[u8];
108 const TEXT: &str = "Hello World!! This is a vaguely long string to test string splitting!";
109 #[test]
110 fn splitting_lines() {
111 let result = split_into_lines_fontdue(
112 TEXT,
113 &Font::from_bytes(FONT_BYTES, FontSettings::default()).unwrap(),
114 20.0,
115 |_| Pt(100.0),
116 &mut HashMap::new(),
117 );
118 assert_eq!(result.len(), 7);
119 assert_eq!(
121 result
122 .iter()
123 .map(|x| x.0.clone())
124 .collect::<Vec<String>>()
125 .join("")
126 .replace(' ', "").replace('\n', ""),
127 TEXT.replace(' ', "")
128 );
129 }
130}