1 use {
2 Path,
3 PathCommand,
4 PathSegment,
5 WriteBuffer,
6 WriteOptions,
7 };
8
9 struct PrevCmd {
10 cmd: PathCommand,
11 absolute: bool,
12 implicit: bool,
13 }
14
15 impl WriteBuffer for Path {
write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec<u8>)16 fn write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec<u8>) {
17 if self.is_empty() {
18 return;
19 }
20
21 let mut prev_cmd: Option<PrevCmd> = None;
22 let mut prev_coord_has_dot = false;
23
24 for seg in self.iter() {
25 let is_written = write_cmd(seg, &mut prev_cmd, opt, buf);
26 write_segment(seg, is_written, &mut prev_coord_has_dot, opt, buf);
27 }
28
29 if !opt.use_compact_path_notation {
30 let len = buf.len();
31 buf.truncate(len - 1);
32 }
33 }
34 }
35
write_cmd( seg: &PathSegment, prev_cmd: &mut Option<PrevCmd>, opt: &WriteOptions, buf: &mut Vec<u8> ) -> bool36 fn write_cmd(
37 seg: &PathSegment,
38 prev_cmd: &mut Option<PrevCmd>,
39 opt: &WriteOptions,
40 buf: &mut Vec<u8>
41 ) -> bool {
42 let mut print_cmd = true;
43 if opt.remove_duplicated_path_commands {
44 // check that previous command is the same as current
45 if let Some(ref pcmd) = *prev_cmd {
46 // MoveTo commands can't be skipped
47 if pcmd.cmd != PathCommand::MoveTo {
48 if seg.cmd() == pcmd.cmd && seg.is_absolute() == pcmd.absolute {
49 print_cmd = false;
50 }
51 }
52 }
53 }
54
55 let mut is_implicit = false;
56 if opt.use_implicit_lineto_commands {
57
58 let check_implicit = || {
59 if let Some(ref pcmd) = *prev_cmd {
60 if seg.is_absolute() != pcmd.absolute {
61 return false;
62 }
63
64 if pcmd.implicit {
65 if seg.cmd() == PathCommand::LineTo {
66 return true;
67 }
68 } else if pcmd.cmd == PathCommand::MoveTo
69 && seg.cmd() == PathCommand::LineTo {
70 // if current segment is LineTo and previous was MoveTo
71 return true;
72 }
73 }
74
75 false
76 };
77
78 if check_implicit() {
79 is_implicit = true;
80 print_cmd = false;
81 }
82 }
83
84 *prev_cmd = Some(PrevCmd {
85 cmd: seg.cmd(),
86 absolute: seg.is_absolute(),
87 implicit: is_implicit,
88 });
89
90 if !print_cmd {
91 // we do not update 'prev_cmd' if we do not wrote it
92 return false;
93 }
94
95 write_cmd_char(seg, buf);
96
97 if !(seg.cmd() == PathCommand::ClosePath || opt.use_compact_path_notation) {
98 buf.push(b' ');
99 }
100
101 true
102 }
103
write_cmd_char(seg: &PathSegment, buf: &mut Vec<u8>)104 pub fn write_cmd_char(seg: &PathSegment, buf: &mut Vec<u8>) {
105 let cmd: u8 = if seg.is_absolute() {
106 match seg.cmd() {
107 PathCommand::MoveTo => b'M',
108 PathCommand::LineTo => b'L',
109 PathCommand::HorizontalLineTo => b'H',
110 PathCommand::VerticalLineTo => b'V',
111 PathCommand::CurveTo => b'C',
112 PathCommand::SmoothCurveTo => b'S',
113 PathCommand::Quadratic => b'Q',
114 PathCommand::SmoothQuadratic => b'T',
115 PathCommand::EllipticalArc => b'A',
116 PathCommand::ClosePath => b'Z',
117 }
118 } else {
119 match seg.cmd() {
120 PathCommand::MoveTo => b'm',
121 PathCommand::LineTo => b'l',
122 PathCommand::HorizontalLineTo => b'h',
123 PathCommand::VerticalLineTo => b'v',
124 PathCommand::CurveTo => b'c',
125 PathCommand::SmoothCurveTo => b's',
126 PathCommand::Quadratic => b'q',
127 PathCommand::SmoothQuadratic => b't',
128 PathCommand::EllipticalArc => b'a',
129 PathCommand::ClosePath => b'z',
130 }
131 };
132 buf.push(cmd);
133 }
134
write_segment( data: &PathSegment, is_written: bool, prev_coord_has_dot: &mut bool, opt: &WriteOptions, buf: &mut Vec<u8> )135 pub fn write_segment(
136 data: &PathSegment,
137 is_written: bool,
138 prev_coord_has_dot: &mut bool,
139 opt: &WriteOptions,
140 buf: &mut Vec<u8>
141 ) {
142 match *data {
143 PathSegment::MoveTo { x, y, .. }
144 | PathSegment::LineTo { x, y, .. }
145 | PathSegment::SmoothQuadratic { x, y, .. } => {
146 write_coords(&[x, y], is_written, prev_coord_has_dot, opt, buf);
147 }
148
149 PathSegment::HorizontalLineTo { x, .. } => {
150 write_coords(&[x], is_written, prev_coord_has_dot, opt, buf);
151 }
152
153 PathSegment::VerticalLineTo { y, .. } => {
154 write_coords(&[y], is_written, prev_coord_has_dot, opt, buf);
155 }
156
157 PathSegment::CurveTo { x1, y1, x2, y2, x, y, .. } => {
158 write_coords(&[x1, y1, x2, y2, x, y], is_written,
159 prev_coord_has_dot, opt, buf);
160 }
161
162 PathSegment::SmoothCurveTo { x2, y2, x, y, .. } => {
163 write_coords(&[x2, y2, x, y], is_written, prev_coord_has_dot, opt, buf);
164 }
165
166 PathSegment::Quadratic { x1, y1, x, y, .. } => {
167 write_coords(&[x1, y1, x, y], is_written, prev_coord_has_dot, opt, buf);
168 }
169
170 PathSegment::EllipticalArc { rx, ry, x_axis_rotation, large_arc, sweep, x, y, .. } => {
171 write_coords(&[rx, ry, x_axis_rotation], is_written,
172 prev_coord_has_dot, opt, buf);
173
174 if opt.use_compact_path_notation {
175 // flags must always have a space before it
176 buf.push(b' ');
177 }
178
179 write_flag(large_arc, buf);
180 if !opt.join_arc_to_flags {
181 buf.push(b' ');
182 }
183 write_flag(sweep, buf);
184 if !opt.join_arc_to_flags {
185 buf.push(b' ');
186 }
187
188 // reset, because flags can't have dots
189 *prev_coord_has_dot = false;
190
191 // 'is_explicit_cmd' is always 'true'
192 // because it's relevant only for first coordinate of the segment
193 write_coords(&[x, y], true, prev_coord_has_dot, opt, buf);
194 }
195 PathSegment::ClosePath { .. } => {
196 if !opt.use_compact_path_notation {
197 buf.push(b' ');
198 }
199 }
200 }
201 }
202
write_coords( coords: &[f64], is_explicit_cmd: bool, prev_coord_has_dot: &mut bool, opt: &WriteOptions, buf: &mut Vec<u8> )203 fn write_coords(
204 coords: &[f64],
205 is_explicit_cmd: bool,
206 prev_coord_has_dot: &mut bool,
207 opt: &WriteOptions,
208 buf: &mut Vec<u8>
209 ) {
210 if opt.use_compact_path_notation {
211 for (i, num) in coords.iter().enumerate() {
212 let start_pos = buf.len() - 1;
213
214 num.write_buf_opt(opt, buf);
215
216 let c = buf[start_pos + 1];
217
218 let write_space = if !*prev_coord_has_dot && c == b'.' {
219 !(i == 0 && is_explicit_cmd)
220 } else if i == 0 && is_explicit_cmd {
221 false
222 } else if (c as char).is_digit(10) {
223 true
224 } else {
225 false
226 };
227
228 if write_space {
229 buf.insert(start_pos + 1, b' ');
230 }
231
232 *prev_coord_has_dot = false;
233 for c in buf.iter().skip(start_pos) {
234 if *c == b'.' {
235 *prev_coord_has_dot = true;
236 break;
237 }
238 }
239 }
240 } else {
241 for num in coords.iter() {
242 num.write_buf_opt(opt, buf);
243 buf.push(b' ');
244 }
245 }
246 }
247
write_flag(flag: bool, buf: &mut Vec<u8>)248 fn write_flag(flag: bool, buf: &mut Vec<u8>) {
249 buf.push(if flag { b'1' } else { b'0' });
250 }
251
252 impl_display!(Path);
253 impl_debug_from_display!(Path);
254
255 #[cfg(test)]
256 mod tests {
257 use std::str::FromStr;
258
259 use super::*;
260 use WriteOptions;
261
262 #[test]
write_1()263 fn write_1() {
264 let mut path = Path::new();
265 path.push(PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 });
266 path.push(PathSegment::LineTo { abs: true, x: 10.0, y: 20.0 });
267 assert_eq!(path.to_string(), "M 10 20 L 10 20");
268 }
269
270 #[test]
write_2()271 fn write_2() {
272 let path = Path::from_str("M 10 20 l 10 20").unwrap();
273 assert_eq!(path.to_string(), "M 10 20 l 10 20");
274 }
275
276 #[test]
write_3()277 fn write_3() {
278 let path = Path::from_str(
279 "M 10 20 L 30 40 H 50 V 60 C 70 80 90 100 110 120 \
280 S 130 140 150 160 Q 170 180 190 200 T 210 220 \
281 A 50 50 30 1 1 230 240 Z").unwrap();
282 assert_eq!(path.to_string(),
283 "M 10 20 L 30 40 H 50 V 60 C 70 80 90 100 110 120 \
284 S 130 140 150 160 Q 170 180 190 200 T 210 220 \
285 A 50 50 30 1 1 230 240 Z");
286 }
287
288 #[test]
write_4()289 fn write_4() {
290 let path = Path::from_str(
291 "m 10 20 l 30 40 h 50 v 60 c 70 80 90 100 110 120 \
292 s 130 140 150 160 q 170 180 190 200 t 210 220 \
293 a 50 50 30 1 1 230 240 z").unwrap();
294 assert_eq!(path.to_string(),
295 "m 10 20 l 30 40 h 50 v 60 c 70 80 90 100 110 120 \
296 s 130 140 150 160 q 170 180 190 200 t 210 220 \
297 a 50 50 30 1 1 230 240 z");
298 }
299
300 #[test]
write_5()301 fn write_5() {
302 let path = Path::from_str("").unwrap();
303 assert_eq!(path.to_string(), "");
304 }
305
306 macro_rules! test_write_opt {
307 ($name:ident, $in_text:expr, $out_text:expr, $flag:ident) => (
308 #[test]
309 fn $name() {
310 let path = Path::from_str($in_text).unwrap();
311
312 let mut opt = WriteOptions::default();
313 opt.$flag = true;
314
315 assert_eq!(path.with_write_opt(&opt).to_string(), $out_text);
316 }
317 )
318 }
319
320 test_write_opt!(write_6,
321 "M 10 20 L 30 40 L 50 60 l 70 80",
322 "M 10 20 L 30 40 50 60 l 70 80",
323 remove_duplicated_path_commands);
324
325 test_write_opt!(write_7,
326 "M 10 20 30 40 50 60",
327 "M 10 20 L 30 40 50 60",
328 remove_duplicated_path_commands);
329
330 test_write_opt!(write_8,
331 "M 10 20 L 30 40",
332 "M10 20L30 40",
333 use_compact_path_notation);
334
335 test_write_opt!(write_9,
336 "M 10 20 V 30 H 40 V 50 H 60 Z",
337 "M10 20V30H40V50H60Z",
338 use_compact_path_notation);
339
340 #[test]
write_10()341 fn write_10() {
342 let path = Path::from_str("M 10 -20 A 5.5 0.3 -4 1 1 0 -0.1").unwrap();
343
344 let mut opt = WriteOptions::default();
345 opt.use_compact_path_notation = true;
346 opt.join_arc_to_flags = true;
347 opt.remove_leading_zero = true;
348
349 assert_eq!(path.with_write_opt(&opt).to_string(), "M10-20A5.5.3-4 110-.1");
350 }
351
352 test_write_opt!(write_11,
353 "M 10-10 a 1 1 0 1 1 -1 1",
354 "M10-10a1 1 0 1 1 -1 1",
355 use_compact_path_notation);
356
357 test_write_opt!(write_12,
358 "M 10-10 a 1 1 0 1 1 0.1 1",
359 "M10-10a1 1 0 1 1 0.1 1",
360 use_compact_path_notation);
361
362 test_write_opt!(write_13,
363 "M 10 20 L 30 40 L 50 60 H 10",
364 "M 10 20 30 40 50 60 H 10",
365 use_implicit_lineto_commands);
366
367 // should be ignored, because of different 'absolute' values
368 test_write_opt!(write_14,
369 "M 10 20 l 30 40 L 50 60",
370 "M 10 20 l 30 40 L 50 60",
371 use_implicit_lineto_commands);
372
373 test_write_opt!(write_15,
374 "M 10 20 L 30 40 l 50 60 L 50 60",
375 "M 10 20 30 40 l 50 60 L 50 60",
376 use_implicit_lineto_commands);
377
378 test_write_opt!(write_16,
379 "M 10 20 L 30 40 l 50 60",
380 "M 10 20 30 40 l 50 60",
381 use_implicit_lineto_commands);
382
383 test_write_opt!(write_17,
384 "M 10 20 L 30 40 L 50 60 M 10 20 L 30 40 L 50 60",
385 "M 10 20 30 40 50 60 M 10 20 30 40 50 60",
386 use_implicit_lineto_commands);
387
388 #[test]
write_18()389 fn write_18() {
390 let path = Path::from_str("M 10 20 L 30 40 L 50 60 M 10 20 L 30 40 L 50 60").unwrap();
391
392 let mut opt = WriteOptions::default();
393 opt.use_implicit_lineto_commands = true;
394 opt.remove_duplicated_path_commands = true;
395
396 assert_eq!(path.with_write_opt(&opt).to_string(), "M 10 20 30 40 50 60 M 10 20 30 40 50 60");
397 }
398
399 #[test]
write_19()400 fn write_19() {
401 let path = Path::from_str("m10 20 A 10 10 0 1 0 0 0 A 2 2 0 1 0 2 0").unwrap();
402
403 let mut opt = WriteOptions::default();
404 opt.use_compact_path_notation = true;
405 opt.remove_duplicated_path_commands = true;
406 opt.remove_leading_zero = true;
407
408 // may generate as 'm10 20A10 10 0 1 0 0 0 2 2 0 1 0 2 0' <- two spaces
409
410 assert_eq!(path.with_write_opt(&opt).to_string(), "m10 20A10 10 0 1 0 0 0 2 2 0 1 0 2 0");
411 }
412
413 #[test]
write_20()414 fn write_20() {
415 let path = Path::from_str("M 0.1 0.1 L 1 0.1 2 -0.1").unwrap();
416
417 let mut opt = WriteOptions::default();
418 opt.use_compact_path_notation = true;
419 opt.remove_duplicated_path_commands = true;
420 opt.remove_leading_zero = true;
421
422 assert_eq!(path.with_write_opt(&opt).to_string(), "M.1.1L1 .1 2-.1");
423 }
424
425 test_write_opt!(write_21,
426 "M 10 20 M 30 40 M 50 60 L 30 40",
427 "M 10 20 M 30 40 M 50 60 L 30 40",
428 remove_duplicated_path_commands);
429 }
430