1#[cfg(feature = "write")]
7use lopdf::content::Operation;
8#[cfg(feature = "write")]
9use lopdf::Object;
10
11#[cfg(feature = "write")]
13#[derive(Debug, Clone, Copy)]
14pub struct AppearanceColor {
15 pub r: f64,
16 pub g: f64,
17 pub b: f64,
18}
19
20#[cfg(feature = "write")]
21impl AppearanceColor {
22 pub fn new(r: f64, g: f64, b: f64) -> Self {
23 Self { r, g, b }
24 }
25
26 pub fn fill_ops(&self) -> Operation {
28 Operation::new(
29 "rg",
30 vec![
31 Object::Real(self.r as f32),
32 Object::Real(self.g as f32),
33 Object::Real(self.b as f32),
34 ],
35 )
36 }
37
38 pub fn stroke_ops(&self) -> Operation {
40 Operation::new(
41 "RG",
42 vec![
43 Object::Real(self.r as f32),
44 Object::Real(self.g as f32),
45 Object::Real(self.b as f32),
46 ],
47 )
48 }
49}
50
51#[cfg(feature = "write")]
56pub struct AppearanceStreamBuilder {
57 ops: Vec<Operation>,
58 width: f64,
59 height: f64,
60}
61
62#[cfg(feature = "write")]
63impl AppearanceStreamBuilder {
64 pub fn new(width: f64, height: f64) -> Self {
66 Self {
67 ops: Vec::new(),
68 width,
69 height,
70 }
71 }
72
73 pub fn save_state(&mut self) -> &mut Self {
75 self.ops.push(Operation::new("q", vec![]));
76 self
77 }
78
79 pub fn restore_state(&mut self) -> &mut Self {
81 self.ops.push(Operation::new("Q", vec![]));
82 self
83 }
84
85 pub fn set_fill_color(&mut self, color: &AppearanceColor) -> &mut Self {
87 self.ops.push(color.fill_ops());
88 self
89 }
90
91 pub fn set_stroke_color(&mut self, color: &AppearanceColor) -> &mut Self {
93 self.ops.push(color.stroke_ops());
94 self
95 }
96
97 pub fn set_line_width(&mut self, width: f64) -> &mut Self {
99 self.ops
100 .push(Operation::new("w", vec![Object::Real(width as f32)]));
101 self
102 }
103
104 pub fn set_dash_pattern(&mut self, dash: &[f64], phase: f64) -> &mut Self {
106 let arr: Vec<Object> = dash.iter().map(|&d| Object::Real(d as f32)).collect();
107 self.ops.push(Operation::new(
108 "d",
109 vec![Object::Array(arr), Object::Real(phase as f32)],
110 ));
111 self
112 }
113
114 pub fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) -> &mut Self {
116 self.ops.push(Operation::new(
117 "re",
118 vec![
119 Object::Real(x as f32),
120 Object::Real(y as f32),
121 Object::Real(w as f32),
122 Object::Real(h as f32),
123 ],
124 ));
125 self
126 }
127
128 pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
130 self.ops.push(Operation::new(
131 "m",
132 vec![Object::Real(x as f32), Object::Real(y as f32)],
133 ));
134 self
135 }
136
137 pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
139 self.ops.push(Operation::new(
140 "l",
141 vec![Object::Real(x as f32), Object::Real(y as f32)],
142 ));
143 self
144 }
145
146 pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
148 self.ops.push(Operation::new(
149 "c",
150 vec![
151 Object::Real(x1 as f32),
152 Object::Real(y1 as f32),
153 Object::Real(x2 as f32),
154 Object::Real(y2 as f32),
155 Object::Real(x3 as f32),
156 Object::Real(y3 as f32),
157 ],
158 ));
159 self
160 }
161
162 pub fn close_path(&mut self) -> &mut Self {
164 self.ops.push(Operation::new("h", vec![]));
165 self
166 }
167
168 pub fn stroke(&mut self) -> &mut Self {
170 self.ops.push(Operation::new("S", vec![]));
171 self
172 }
173
174 pub fn fill(&mut self) -> &mut Self {
176 self.ops.push(Operation::new("f", vec![]));
177 self
178 }
179
180 pub fn fill_and_stroke(&mut self) -> &mut Self {
182 self.ops.push(Operation::new("B", vec![]));
183 self
184 }
185
186 pub fn close_fill_and_stroke(&mut self) -> &mut Self {
188 self.ops.push(Operation::new("b", vec![]));
189 self
190 }
191
192 pub fn filled_rect(&mut self, color: &AppearanceColor) -> &mut Self {
194 self.save_state();
195 self.set_fill_color(color);
196 self.rect(0.0, 0.0, self.width, self.height);
197 self.fill();
198 self.restore_state();
199 self
200 }
201
202 pub fn stroked_rect(&mut self, color: &AppearanceColor, line_width: f64) -> &mut Self {
204 let half = line_width / 2.0;
205 self.save_state();
206 self.set_stroke_color(color);
207 self.set_line_width(line_width);
208 self.rect(
209 half,
210 half,
211 self.width - line_width,
212 self.height - line_width,
213 );
214 self.stroke();
215 self.restore_state();
216 self
217 }
218
219 pub fn filled_stroked_rect(
221 &mut self,
222 fill: &AppearanceColor,
223 stroke: &AppearanceColor,
224 line_width: f64,
225 ) -> &mut Self {
226 let half = line_width / 2.0;
227 self.save_state();
228 self.set_fill_color(fill);
229 self.set_stroke_color(stroke);
230 self.set_line_width(line_width);
231 self.rect(
232 half,
233 half,
234 self.width - line_width,
235 self.height - line_width,
236 );
237 self.fill_and_stroke();
238 self.restore_state();
239 self
240 }
241
242 pub fn ellipse(&mut self) -> &mut Self {
244 let k = 0.5523;
247 let cx = self.width / 2.0;
248 let cy = self.height / 2.0;
249 let rx = cx;
250 let ry = cy;
251
252 self.move_to(cx + rx, cy);
253 self.curve_to(cx + rx, cy + ry * k, cx + rx * k, cy + ry, cx, cy + ry);
254 self.curve_to(cx - rx * k, cy + ry, cx - rx, cy + ry * k, cx - rx, cy);
255 self.curve_to(cx - rx, cy - ry * k, cx - rx * k, cy - ry, cx, cy - ry);
256 self.curve_to(cx + rx * k, cy - ry, cx + rx, cy - ry * k, cx + rx, cy);
257 self.close_path();
258 self
259 }
260
261 pub fn line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) -> &mut Self {
263 self.move_to(x1, y1);
264 self.line_to(x2, y2);
265 self
266 }
267
268 pub fn text(
270 &mut self,
271 text: &str,
272 font_name: &str,
273 font_size: f64,
274 x: f64,
275 y: f64,
276 color: &AppearanceColor,
277 ) -> &mut Self {
278 self.save_state();
279 self.set_fill_color(color);
280 self.ops.push(Operation::new("BT", vec![]));
281 self.ops.push(Operation::new(
282 "Tf",
283 vec![
284 Object::Name(font_name.as_bytes().to_vec()),
285 Object::Real(font_size as f32),
286 ],
287 ));
288 self.ops.push(Operation::new(
289 "Td",
290 vec![Object::Real(x as f32), Object::Real(y as f32)],
291 ));
292 self.ops.push(Operation::new(
293 "Tj",
294 vec![Object::String(
295 text.as_bytes().to_vec(),
296 lopdf::StringFormat::Literal,
297 )],
298 ));
299 self.ops.push(Operation::new("ET", vec![]));
300 self.restore_state();
301 self
302 }
303
304 pub fn ops_push_raw(&mut self, op: Operation) -> &mut Self {
306 self.ops.push(op);
307 self
308 }
309
310 pub fn encode(self) -> Result<Vec<u8>, String> {
312 lopdf::content::Content {
313 operations: self.ops,
314 }
315 .encode()
316 .map_err(|e| format!("{e}"))
317 }
318
319 pub fn width(&self) -> f64 {
321 self.width
322 }
323
324 pub fn height(&self) -> f64 {
326 self.height
327 }
328}
329
330#[cfg(all(test, feature = "write"))]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn encode_simple_rect() {
336 let mut builder = AppearanceStreamBuilder::new(100.0, 50.0);
337 let red = AppearanceColor::new(1.0, 0.0, 0.0);
338 builder.stroked_rect(&red, 1.0);
339 let bytes = builder.encode().unwrap();
340 let s = String::from_utf8_lossy(&bytes);
341 assert!(s.contains("RG"), "should contain stroke color");
342 assert!(s.contains("re"), "should contain rectangle");
343 assert!(s.contains("S"), "should contain stroke");
344 }
345
346 #[test]
347 fn encode_filled_rect() {
348 let mut builder = AppearanceStreamBuilder::new(80.0, 40.0);
349 let yellow = AppearanceColor::new(1.0, 1.0, 0.0);
350 builder.filled_rect(&yellow);
351 let bytes = builder.encode().unwrap();
352 let s = String::from_utf8_lossy(&bytes);
353 assert!(s.contains("rg"), "should contain fill color");
354 assert!(s.contains("f"), "should contain fill");
355 }
356
357 #[test]
358 fn encode_ellipse() {
359 let mut builder = AppearanceStreamBuilder::new(60.0, 60.0);
360 let blue = AppearanceColor::new(0.0, 0.0, 1.0);
361 builder.save_state();
362 builder.set_stroke_color(&blue);
363 builder.set_line_width(1.0);
364 builder.ellipse();
365 builder.stroke();
366 builder.restore_state();
367 let bytes = builder.encode().unwrap();
368 let s = String::from_utf8_lossy(&bytes);
369 assert!(s.contains("c"), "should contain bezier curves");
370 }
371
372 #[test]
373 fn encode_text() {
374 let mut builder = AppearanceStreamBuilder::new(200.0, 20.0);
375 let black = AppearanceColor::new(0.0, 0.0, 0.0);
376 builder.text("Hello World", "F1", 12.0, 2.0, 4.0, &black);
377 let bytes = builder.encode().unwrap();
378 let s = String::from_utf8_lossy(&bytes);
379 assert!(s.contains("BT"), "should contain begin text");
380 assert!(s.contains("Tj"), "should contain show text");
381 assert!(s.contains("ET"), "should contain end text");
382 }
383}