ppt_rs/generator/slide_content/
ink_annotations.rs1#[derive(Clone, Debug, Copy, PartialEq, Eq, Default)]
8pub enum PenTip {
9 #[default]
10 Ball,
11 Flat,
12}
13
14impl PenTip {
15 pub fn to_xml(&self) -> &'static str {
16 match self {
17 PenTip::Ball => "ball",
18 PenTip::Flat => "flat",
19 }
20 }
21}
22
23#[derive(Clone, Debug)]
25pub struct InkPen {
26 pub color: String,
27 pub width: u32,
28 pub tip: PenTip,
29 pub opacity: f32,
30}
31
32impl InkPen {
33 pub fn new(color: &str, width: u32) -> Self {
35 Self {
36 color: color.trim_start_matches('#').to_uppercase(),
37 width,
38 tip: PenTip::default(),
39 opacity: 1.0,
40 }
41 }
42
43 pub fn tip(mut self, tip: PenTip) -> Self {
44 self.tip = tip;
45 self
46 }
47
48 pub fn opacity(mut self, opacity: f32) -> Self {
50 self.opacity = crate::core::clamp_unit_interval(opacity as f64) as f32;
51 self
52 }
53
54 pub fn red() -> Self {
56 Self::new("FF0000", 50)
57 }
58
59 pub fn blue() -> Self {
61 Self::new("0000FF", 50)
62 }
63
64 pub fn black() -> Self {
66 Self::new("000000", 50)
67 }
68
69 pub fn highlighter() -> Self {
71 Self::new("FFFF00", 300).tip(PenTip::Flat).opacity(0.5)
72 }
73}
74
75#[derive(Clone, Debug, Copy, PartialEq)]
77pub struct InkPoint {
78 pub x: f64,
79 pub y: f64,
80}
81
82impl InkPoint {
83 pub fn new(x: f64, y: f64) -> Self {
84 Self { x, y }
85 }
86}
87
88#[derive(Clone, Debug)]
90pub struct InkStroke {
91 pub points: Vec<InkPoint>,
92 pub pen: InkPen,
93}
94
95impl InkStroke {
96 pub fn new(pen: InkPen) -> Self {
97 Self {
98 points: Vec::new(),
99 pen,
100 }
101 }
102
103 pub fn add_point(mut self, x: f64, y: f64) -> Self {
105 self.points.push(InkPoint::new(x, y));
106 self
107 }
108
109 pub fn add_points(mut self, points: &[(f64, f64)]) -> Self {
111 for &(x, y) in points {
112 self.points.push(InkPoint::new(x, y));
113 }
114 self
115 }
116
117 pub fn len(&self) -> usize {
119 self.points.len()
120 }
121
122 pub fn is_empty(&self) -> bool {
123 self.points.is_empty()
124 }
125
126 fn points_str(&self) -> String {
128 self.points
129 .iter()
130 .map(|p| format!("{} {}", p.x as i64, p.y as i64))
131 .collect::<Vec<_>>()
132 .join(" ")
133 }
134
135 pub fn to_xml(&self, trace_id: u32) -> String {
137 format!(
138 "<ink:trace contextRef=\"#{}\" brushRef=\"#br{}\" id=\"{}\">{}</ink:trace>",
139 trace_id,
140 trace_id,
141 trace_id,
142 self.points_str(),
143 )
144 }
145}
146
147#[derive(Clone, Debug, Default)]
149pub struct InkAnnotations {
150 strokes: Vec<InkStroke>,
151}
152
153impl InkAnnotations {
154 pub fn new() -> Self {
155 Self::default()
156 }
157
158 pub fn add_stroke(&mut self, stroke: InkStroke) {
160 self.strokes.push(stroke);
161 }
162
163 pub fn strokes(&self) -> &[InkStroke] {
165 &self.strokes
166 }
167
168 pub fn len(&self) -> usize {
170 self.strokes.len()
171 }
172
173 pub fn is_empty(&self) -> bool {
174 self.strokes.is_empty()
175 }
176
177 pub fn clear(&mut self) {
179 self.strokes.clear();
180 }
181
182 pub fn part_xml(&self) -> String {
184 if self.strokes.is_empty() {
185 return String::new();
186 }
187
188 let mut xml = String::from(
189 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
190<ink:ink xmlns:ink="http://www.w3.org/2003/InkML">"#,
191 );
192
193 for (i, stroke) in self.strokes.iter().enumerate() {
195 let opacity_attr = if (stroke.pen.opacity - 1.0).abs() > 0.01 {
196 format!(r#" transparency="{:.0}""#, (1.0 - stroke.pen.opacity) * 255.0)
197 } else {
198 String::new()
199 };
200 xml.push_str(&format!(
201 "<ink:brush id=\"br{}\" color=\"#{}\" width=\"{}\" tip=\"{}\"{} />\n",
202 i, stroke.pen.color, stroke.pen.width, stroke.pen.tip.to_xml(), opacity_attr,
203 ));
204 }
205
206 for (i, stroke) in self.strokes.iter().enumerate() {
208 xml.push_str(&stroke.to_xml(i as u32));
209 }
210
211 xml.push_str("</ink:ink>");
212 xml
213 }
214
215 pub fn to_xml(&self) -> String {
217 if self.strokes.is_empty() {
218 return String::new();
219 }
220 format!(
221 r#"<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:Choice Requires="p14"><p:contentPart xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:id="{}"/></mc:Choice></mc:AlternateContent>"#,
222 self.rel_id()
223 )
224 }
225
226 pub fn rel_id(&self) -> String {
228 "rId5".to_string()
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn test_pen_tip_default() {
238 assert_eq!(PenTip::default(), PenTip::Ball);
239 assert_eq!(PenTip::Ball.to_xml(), "ball");
240 assert_eq!(PenTip::Flat.to_xml(), "flat");
241 }
242
243 #[test]
244 fn test_ink_pen_new() {
245 let pen = InkPen::new("FF0000", 50);
246 assert_eq!(pen.color, "FF0000");
247 assert_eq!(pen.width, 50);
248 assert_eq!(pen.tip, PenTip::Ball);
249 assert!((pen.opacity - 1.0).abs() < f32::EPSILON);
250 }
251
252 #[test]
253 fn test_ink_pen_presets() {
254 let red = InkPen::red();
255 assert_eq!(red.color, "FF0000");
256 let blue = InkPen::blue();
257 assert_eq!(blue.color, "0000FF");
258 let black = InkPen::black();
259 assert_eq!(black.color, "000000");
260 }
261
262 #[test]
263 fn test_ink_pen_highlighter() {
264 let h = InkPen::highlighter();
265 assert_eq!(h.color, "FFFF00");
266 assert_eq!(h.tip, PenTip::Flat);
267 assert!((h.opacity - 0.5).abs() < f32::EPSILON);
268 assert_eq!(h.width, 300);
269 }
270
271 #[test]
272 fn test_ink_pen_opacity_clamp() {
273 let pen = InkPen::new("000000", 10).opacity(2.0);
274 assert!((pen.opacity - 1.0).abs() < f32::EPSILON);
275 let pen2 = InkPen::new("000000", 10).opacity(-1.0);
276 assert!((pen2.opacity - 0.0).abs() < f32::EPSILON);
277 }
278
279 #[test]
280 fn test_ink_point() {
281 let p = InkPoint::new(100.0, 200.0);
282 assert!((p.x - 100.0).abs() < f64::EPSILON);
283 assert!((p.y - 200.0).abs() < f64::EPSILON);
284 }
285
286 #[test]
287 fn test_ink_stroke_new() {
288 let stroke = InkStroke::new(InkPen::black());
289 assert!(stroke.is_empty());
290 assert_eq!(stroke.len(), 0);
291 }
292
293 #[test]
294 fn test_ink_stroke_add_points() {
295 let stroke = InkStroke::new(InkPen::red())
296 .add_point(0.0, 0.0)
297 .add_point(100.0, 100.0)
298 .add_point(200.0, 50.0);
299 assert_eq!(stroke.len(), 3);
300 assert!(!stroke.is_empty());
301 }
302
303 #[test]
304 fn test_ink_stroke_add_points_batch() {
305 let stroke = InkStroke::new(InkPen::blue())
306 .add_points(&[(0.0, 0.0), (50.0, 50.0), (100.0, 0.0)]);
307 assert_eq!(stroke.len(), 3);
308 }
309
310 #[test]
311 fn test_ink_stroke_xml() {
312 let stroke = InkStroke::new(InkPen::black())
313 .add_point(10.0, 20.0)
314 .add_point(30.0, 40.0);
315 let xml = stroke.to_xml(0);
316 assert!(xml.contains("ink:trace"));
317 assert!(xml.contains("10 20"));
318 assert!(xml.contains("30 40"));
319 }
320
321 #[test]
322 fn test_ink_annotations_new() {
323 let ann = InkAnnotations::new();
324 assert!(ann.is_empty());
325 assert_eq!(ann.len(), 0);
326 }
327
328 #[test]
329 fn test_ink_annotations_add() {
330 let mut ann = InkAnnotations::new();
331 ann.add_stroke(InkStroke::new(InkPen::red()).add_point(0.0, 0.0));
332 ann.add_stroke(InkStroke::new(InkPen::blue()).add_point(10.0, 10.0));
333 assert_eq!(ann.len(), 2);
334 }
335
336 #[test]
337 fn test_ink_annotations_clear() {
338 let mut ann = InkAnnotations::new();
339 ann.add_stroke(InkStroke::new(InkPen::black()).add_point(0.0, 0.0));
340 ann.clear();
341 assert!(ann.is_empty());
342 }
343
344 #[test]
345 fn test_ink_annotations_xml_empty() {
346 let ann = InkAnnotations::new();
347 assert_eq!(ann.to_xml(), "");
348 }
349
350 #[test]
351 fn test_ink_annotations_xml() {
352 let mut ann = InkAnnotations::new();
353 ann.add_stroke(
354 InkStroke::new(InkPen::red())
355 .add_point(0.0, 0.0)
356 .add_point(100.0, 100.0),
357 );
358 let slide_xml = ann.to_xml();
359 assert!(slide_xml.contains("mc:AlternateContent"));
360 assert!(slide_xml.contains("p:contentPart"));
361
362 let part_xml = ann.part_xml();
363 assert!(part_xml.contains("ink:ink"));
364 assert!(part_xml.contains("ink:brush"));
365 assert!(part_xml.contains("ink:trace"));
366 assert!(part_xml.contains("FF0000"));
367 }
368
369 #[test]
370 fn test_ink_annotations_xml_highlighter() {
371 let mut ann = InkAnnotations::new();
372 ann.add_stroke(
373 InkStroke::new(InkPen::highlighter())
374 .add_point(0.0, 0.0)
375 .add_point(500.0, 0.0),
376 );
377 let xml = ann.part_xml();
378 assert!(xml.contains("FFFF00"));
379 assert!(xml.contains("flat"));
380 assert!(xml.contains("transparency"));
381 }
382}