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