oxidize_pdf/layout/
rich_text.rs1use crate::text::{measure_text, Font};
2use crate::Color;
3use std::fmt::Write;
4
5#[derive(Debug, Clone)]
7pub struct TextSpan {
8 pub text: String,
9 pub font: Font,
10 pub font_size: f64,
11 pub color: Color,
12}
13
14impl TextSpan {
15 pub fn new(text: &str, font: Font, font_size: f64, color: Color) -> Self {
17 Self {
18 text: text.to_string(),
19 font,
20 font_size,
21 color,
22 }
23 }
24
25 pub fn measure_width(&self) -> f64 {
27 measure_text(&self.text, &self.font, self.font_size)
28 }
29}
30
31#[derive(Debug)]
50pub struct RichText {
51 spans: Vec<TextSpan>,
52}
53
54impl RichText {
55 pub fn new(spans: Vec<TextSpan>) -> Self {
57 Self { spans }
58 }
59
60 pub fn total_width(&self) -> f64 {
62 self.spans.iter().map(|s| s.measure_width()).sum()
63 }
64
65 pub fn max_font_size(&self) -> f64 {
67 self.spans
68 .iter()
69 .map(|s| s.font_size)
70 .fold(0.0_f64, f64::max)
71 }
72
73 pub fn spans(&self) -> &[TextSpan] {
75 &self.spans
76 }
77
78 pub(crate) fn render_operations(&self, x: f64, y: f64) -> String {
82 if self.spans.is_empty() {
83 return String::new();
84 }
85
86 let mut ops = String::new();
87 ops.push_str("BT\n");
88 writeln!(&mut ops, "{x:.2} {y:.2} Td").expect("write to String");
89
90 for span in &self.spans {
91 match span.color {
93 Color::Rgb(r, g, b) => {
94 writeln!(&mut ops, "{r:.3} {g:.3} {b:.3} rg").expect("write to String");
95 }
96 Color::Gray(gray) => {
97 writeln!(&mut ops, "{gray:.3} g").expect("write to String");
98 }
99 Color::Cmyk(c, m, y, k) => {
100 writeln!(&mut ops, "{c:.3} {m:.3} {y:.3} {k:.3} k").expect("write to String");
101 }
102 }
103
104 writeln!(
106 &mut ops,
107 "/{} {:.2} Tf",
108 span.font.pdf_name(),
109 span.font_size
110 )
111 .expect("write to String");
112
113 ops.push('(');
115 for ch in span.text.chars() {
116 match ch {
117 '(' => ops.push_str("\\("),
118 ')' => ops.push_str("\\)"),
119 '\\' => ops.push_str("\\\\"),
120 '\n' => ops.push_str("\\n"),
121 '\r' => ops.push_str("\\r"),
122 '\t' => ops.push_str("\\t"),
123 _ => ops.push(ch),
124 }
125 }
126 ops.push_str(") Tj\n");
127 }
128
129 ops.push_str("ET\n");
130 ops
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_empty_rich_text() {
140 let rt = RichText::new(vec![]);
141 assert_eq!(rt.total_width(), 0.0);
142 assert_eq!(rt.max_font_size(), 0.0);
143 assert!(rt.render_operations(0.0, 0.0).is_empty());
144 }
145
146 #[test]
147 fn test_render_operations_contains_bt_et() {
148 let rt = RichText::new(vec![TextSpan::new(
149 "Hello",
150 Font::Helvetica,
151 12.0,
152 Color::black(),
153 )]);
154 let ops = rt.render_operations(50.0, 700.0);
155 assert!(ops.starts_with("BT\n"));
156 assert!(ops.ends_with("ET\n"));
157 assert!(ops.contains("(Hello) Tj"));
158 assert!(ops.contains("/Helvetica 12.00 Tf"));
159 }
160}