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