Skip to main content

graphitepdf_kit/
text.rs

1use crate::vector::Color;
2
3#[cfg(feature = "tracing")]
4use tracing::instrument;
5
6/// Text alignment options.
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
8pub enum TextAlignment {
9    /// Left-aligned (default).
10    #[default]
11    Left,
12    /// Center-aligned.
13    Center,
14    /// Right-aligned.
15    Right,
16}
17
18/// Text rendering mode.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
20pub enum TextRenderingMode {
21    /// Fill text (default).
22    #[default]
23    Fill,
24    /// Stroke text.
25    Stroke,
26    /// Fill and stroke text.
27    FillStroke,
28    /// Invisible text (clip path).
29    Invisible,
30    /// Fill and add to clip path.
31    FillClip,
32    /// Stroke and add to clip path.
33    StrokeClip,
34    /// Fill, stroke, and add to clip path.
35    FillStrokeClip,
36    /// Add text to clip path.
37    Clip,
38}
39
40/// Font weight options.
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
42pub enum FontWeight {
43    /// Thin (100).
44    Thin,
45    /// Extra light (200).
46    ExtraLight,
47    /// Light (300).
48    Light,
49    /// Normal (400, default).
50    #[default]
51    Normal,
52    /// Medium (500).
53    Medium,
54    /// Semi-bold (600).
55    SemiBold,
56    /// Bold (700).
57    Bold,
58    /// Extra bold (800).
59    ExtraBold,
60    /// Black (900).
61    Black,
62}
63
64/// Builder for constructing text content.
65#[derive(Clone, Debug)]
66pub struct TextBuilder {
67    commands: Vec<String>,
68}
69
70impl TextBuilder {
71    /// Creates a new text builder.
72    pub fn new() -> Self {
73        Self {
74            commands: vec!["BT".to_string()],
75        }
76    }
77
78    /// Sets the current font and font size.
79    pub fn font(mut self, name: &str, size: f64) -> Self {
80        self.commands.push(format!("/{} {} Tf", name, size));
81        self
82    }
83
84    /// Moves to the next line.
85    pub fn next_line(mut self, offset_x: f64, offset_y: f64) -> Self {
86        self.commands.push(format!("{} {} Td", offset_x, offset_y));
87        self
88    }
89
90    /// Moves to an absolute position.
91    pub fn position(mut self, x: f64, y: f64) -> Self {
92        self.commands.push(format!("{} {} Td", x, y));
93        self
94    }
95
96    /// Sets the text matrix and text position matrix.
97    pub fn text_matrix(mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self {
98        self.commands
99            .push(format!("{} {} {} {} {} {} Tm", a, b, c, d, e, f));
100        self
101    }
102
103    /// Adds text to the current position.
104    pub fn text(mut self, text: &str) -> Self {
105        let escaped = text
106            .chars()
107            .map(|c| match c {
108                '(' => "\\(".to_string(),
109                ')' => "\\)".to_string(),
110                '\\' => "\\\\".to_string(),
111                '\n' => "\\n".to_string(),
112                '\r' => "\\r".to_string(),
113                '\t' => "\\t".to_string(),
114                '\x08' => "\\b".to_string(),
115                '\x0c' => "\\f".to_string(),
116                _ => c.to_string(),
117            })
118            .collect::<String>();
119        self.commands.push(format!("({}) Tj", escaped));
120        self
121    }
122
123    /// Adds text and moves to the next line.
124    pub fn text_line(mut self, text: &str) -> Self {
125        let escaped = text
126            .chars()
127            .map(|c| match c {
128                '(' => "\\(".to_string(),
129                ')' => "\\)".to_string(),
130                '\\' => "\\\\".to_string(),
131                '\n' => "\\n".to_string(),
132                '\r' => "\\r".to_string(),
133                '\t' => "\\t".to_string(),
134                '\x08' => "\\b".to_string(),
135                '\x0c' => "\\f".to_string(),
136                _ => c.to_string(),
137            })
138            .collect::<String>();
139        self.commands.push(format!("({}) '", escaped));
140        self
141    }
142
143    /// Adds text with spacing between words and characters.
144    pub fn text_line_spacing(mut self, text: &str, word_spacing: f64, char_spacing: f64) -> Self {
145        let escaped = text
146            .chars()
147            .map(|c| match c {
148                '(' => "\\(".to_string(),
149                ')' => "\\)".to_string(),
150                '\\' => "\\\\".to_string(),
151                '\n' => "\\n".to_string(),
152                '\r' => "\\r".to_string(),
153                '\t' => "\\t".to_string(),
154                '\x08' => "\\b".to_string(),
155                '\x0c' => "\\f".to_string(),
156                _ => c.to_string(),
157            })
158            .collect::<String>();
159        self.commands.push(format!(
160            "{} {} ({}) \"",
161            word_spacing, char_spacing, escaped
162        ));
163        self
164    }
165
166    /// Sets the character spacing.
167    pub fn char_spacing(mut self, spacing: f64) -> Self {
168        self.commands.push(format!("{} Tc", spacing));
169        self
170    }
171
172    /// Sets the word spacing.
173    pub fn word_spacing(mut self, spacing: f64) -> Self {
174        self.commands.push(format!("{} Tw", spacing));
175        self
176    }
177
178    /// Sets the horizontal text scaling (1.0 = normal).
179    pub fn horizontal_scaling(mut self, scale: f64) -> Self {
180        self.commands.push(format!("{} Tz", scale * 100.0));
181        self
182    }
183
184    /// Sets the leading (line spacing).
185    pub fn leading(mut self, leading: f64) -> Self {
186        self.commands.push(format!("{} TL", leading));
187        self
188    }
189
190    /// Sets the text rendering mode.
191    pub fn rendering_mode(mut self, mode: TextRenderingMode) -> Self {
192        let code = match mode {
193            TextRenderingMode::Fill => 0,
194            TextRenderingMode::Stroke => 1,
195            TextRenderingMode::FillStroke => 2,
196            TextRenderingMode::Invisible => 3,
197            TextRenderingMode::FillClip => 4,
198            TextRenderingMode::StrokeClip => 5,
199            TextRenderingMode::FillStrokeClip => 6,
200            TextRenderingMode::Clip => 7,
201        };
202        self.commands.push(format!("{} Tr", code));
203        self
204    }
205
206    /// Sets the text rise (for superscripts/subscripts).
207    pub fn text_rise(mut self, rise: f64) -> Self {
208        self.commands.push(format!("{} Ts", rise));
209        self
210    }
211
212    /// Sets the text color.
213    pub fn set_color(mut self, color: Color) -> Self {
214        self.commands
215            .push(format!("{} {} {} rg", color.r, color.g, color.b));
216        self
217    }
218
219    /// Applies a translation to the text.
220    pub fn translate(mut self, tx: f64, ty: f64) -> Self {
221        self.commands.push(format!("1 0 0 1 {} {} Tm", tx, ty));
222        self
223    }
224
225    /// Applies a rotation to the text (in radians).
226    pub fn rotate(mut self, angle: f64) -> Self {
227        let cos_a = angle.cos();
228        let sin_a = angle.sin();
229        self.commands
230            .push(format!("{} {} {} {} 0 0 Tm", cos_a, sin_a, -sin_a, cos_a));
231        self
232    }
233
234    /// Applies scaling to the text.
235    pub fn scale(mut self, sx: f64, sy: f64) -> Self {
236        self.commands.push(format!("{} 0 0 {} 0 0 Tm", sx, sy));
237        self
238    }
239
240    /// Applies a skew transformation to the text.
241    pub fn skew(mut self, ax: f64, ay: f64) -> Self {
242        let tan_x = ax.tan();
243        let tan_y = ay.tan();
244        self.commands
245            .push(format!("1 {} {} 1 0 0 Tm", tan_x, tan_y));
246        self
247    }
248
249    /// Finishes the text block and returns the content bytes.
250    #[cfg_attr(feature = "tracing", instrument)]
251    pub fn finish(self) -> Vec<u8> {
252        let mut cmds = self.commands;
253        cmds.push("ET".to_string());
254        let mut content = cmds.join("\n");
255        content.push('\n');
256        content.into_bytes()
257    }
258
259    /// Extends this text builder with another's commands.
260    pub fn extend(mut self, other: TextBuilder) -> Self {
261        self.commands.extend(other.commands);
262        self
263    }
264}
265
266impl Default for TextBuilder {
267    fn default() -> Self {
268        Self::new()
269    }
270}