1 | /*! |
2 | The SVG image drawing backend |
3 | */ |
4 | |
5 | use plotters_backend::{ |
6 | text_anchor::{HPos, VPos}, |
7 | BackendColor, BackendCoord, BackendStyle, BackendTextStyle, DrawingBackend, DrawingErrorKind, |
8 | FontStyle, FontTransform, |
9 | }; |
10 | |
11 | use std::fmt::Write as _; |
12 | use std::fs::File; |
13 | #[allow (unused_imports)] |
14 | use std::io::Cursor; |
15 | use std::io::{BufWriter, Error, Write}; |
16 | use std::path::Path; |
17 | |
18 | fn make_svg_color(color: BackendColor) -> String { |
19 | let (r, g, b) = color.rgb; |
20 | return format!("#{:02X}{:02X}{:02X}" , r, g, b); |
21 | } |
22 | |
23 | fn make_svg_opacity(color: BackendColor) -> String { |
24 | return format!("{}" , color.alpha); |
25 | } |
26 | |
27 | enum Target<'a> { |
28 | File(String, &'a Path), |
29 | Buffer(&'a mut String), |
30 | // TODO: At this point we won't make the breaking change |
31 | // so the u8 buffer is still supported. But in 0.3, we definitely |
32 | // should get rid of this. |
33 | #[cfg (feature = "deprecated_items" )] |
34 | U8Buffer(String, &'a mut Vec<u8>), |
35 | } |
36 | |
37 | impl Target<'_> { |
38 | fn get_mut(&mut self) -> &mut String { |
39 | match self { |
40 | Target::File(ref mut buf, _) => buf, |
41 | Target::Buffer(buf) => buf, |
42 | #[cfg (feature = "deprecated_items" )] |
43 | Target::U8Buffer(ref mut buf, _) => buf, |
44 | } |
45 | } |
46 | } |
47 | |
48 | enum SVGTag { |
49 | Svg, |
50 | Circle, |
51 | Line, |
52 | Polygon, |
53 | Polyline, |
54 | Rectangle, |
55 | Text, |
56 | #[allow (dead_code)] |
57 | Image, |
58 | } |
59 | |
60 | impl SVGTag { |
61 | fn to_tag_name(&self) -> &'static str { |
62 | match self { |
63 | SVGTag::Svg => "svg" , |
64 | SVGTag::Circle => "circle" , |
65 | SVGTag::Line => "line" , |
66 | SVGTag::Polyline => "polyline" , |
67 | SVGTag::Rectangle => "rect" , |
68 | SVGTag::Text => "text" , |
69 | SVGTag::Image => "image" , |
70 | SVGTag::Polygon => "polygon" , |
71 | } |
72 | } |
73 | } |
74 | |
75 | /// The SVG image drawing backend |
76 | pub struct SVGBackend<'a> { |
77 | target: Target<'a>, |
78 | size: (u32, u32), |
79 | tag_stack: Vec<SVGTag>, |
80 | saved: bool, |
81 | } |
82 | |
83 | impl<'a> SVGBackend<'a> { |
84 | fn escape_and_push(buf: &mut String, value: &str) { |
85 | value.chars().for_each(|c| match c { |
86 | '<' => buf.push_str("<" ), |
87 | '>' => buf.push_str(">" ), |
88 | '&' => buf.push_str("&" ), |
89 | '"' => buf.push_str(""" ), |
90 | ' \'' => buf.push_str("'" ), |
91 | other => buf.push(other), |
92 | }); |
93 | } |
94 | fn open_tag(&mut self, tag: SVGTag, attr: &[(&str, &str)], close: bool) { |
95 | let buf = self.target.get_mut(); |
96 | buf.push('<' ); |
97 | buf.push_str(tag.to_tag_name()); |
98 | for (key, value) in attr { |
99 | buf.push(' ' ); |
100 | buf.push_str(key); |
101 | buf.push_str("= \"" ); |
102 | Self::escape_and_push(buf, value); |
103 | buf.push(' \"' ); |
104 | } |
105 | if close { |
106 | buf.push_str("/> \n" ); |
107 | } else { |
108 | self.tag_stack.push(tag); |
109 | buf.push_str("> \n" ); |
110 | } |
111 | } |
112 | |
113 | fn close_tag(&mut self) -> bool { |
114 | if let Some(tag) = self.tag_stack.pop() { |
115 | let buf = self.target.get_mut(); |
116 | buf.push_str("</" ); |
117 | buf.push_str(tag.to_tag_name()); |
118 | buf.push_str("> \n" ); |
119 | return true; |
120 | } |
121 | false |
122 | } |
123 | |
124 | fn init_svg_file(&mut self, size: (u32, u32)) { |
125 | self.open_tag( |
126 | SVGTag::Svg, |
127 | &[ |
128 | ("width" , &format!("{}" , size.0)), |
129 | ("height" , &format!("{}" , size.1)), |
130 | ("viewBox" , &format!("0 0 {} {}" , size.0, size.1)), |
131 | ("xmlns" , "http://www.w3.org/2000/svg" ), |
132 | ], |
133 | false, |
134 | ); |
135 | } |
136 | |
137 | /// Create a new SVG drawing backend |
138 | pub fn new<T: AsRef<Path> + ?Sized>(path: &'a T, size: (u32, u32)) -> Self { |
139 | let mut ret = Self { |
140 | target: Target::File(String::default(), path.as_ref()), |
141 | size, |
142 | tag_stack: vec![], |
143 | saved: false, |
144 | }; |
145 | |
146 | ret.init_svg_file(size); |
147 | ret |
148 | } |
149 | |
150 | /// Create a new SVG drawing backend and store the document into a u8 vector |
151 | #[cfg (feature = "deprecated_items" )] |
152 | #[deprecated ( |
153 | note = "This will be replaced by `with_string`, consider use `with_string` to avoid breaking change in the future" |
154 | )] |
155 | pub fn with_buffer(buf: &'a mut Vec<u8>, size: (u32, u32)) -> Self { |
156 | let mut ret = Self { |
157 | target: Target::U8Buffer(String::default(), buf), |
158 | size, |
159 | tag_stack: vec![], |
160 | saved: false, |
161 | }; |
162 | |
163 | ret.init_svg_file(size); |
164 | |
165 | ret |
166 | } |
167 | |
168 | /// Create a new SVG drawing backend and store the document into a String buffer |
169 | pub fn with_string(buf: &'a mut String, size: (u32, u32)) -> Self { |
170 | let mut ret = Self { |
171 | target: Target::Buffer(buf), |
172 | size, |
173 | tag_stack: vec![], |
174 | saved: false, |
175 | }; |
176 | |
177 | ret.init_svg_file(size); |
178 | |
179 | ret |
180 | } |
181 | } |
182 | |
183 | impl<'a> DrawingBackend for SVGBackend<'a> { |
184 | type ErrorType = Error; |
185 | |
186 | fn get_size(&self) -> (u32, u32) { |
187 | self.size |
188 | } |
189 | |
190 | fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<Error>> { |
191 | Ok(()) |
192 | } |
193 | |
194 | fn present(&mut self) -> Result<(), DrawingErrorKind<Error>> { |
195 | if !self.saved { |
196 | while self.close_tag() {} |
197 | match self.target { |
198 | Target::File(ref buf, path) => { |
199 | let outfile = File::create(path).map_err(DrawingErrorKind::DrawingError)?; |
200 | let mut outfile = BufWriter::new(outfile); |
201 | outfile |
202 | .write_all(buf.as_ref()) |
203 | .map_err(DrawingErrorKind::DrawingError)?; |
204 | } |
205 | Target::Buffer(_) => {} |
206 | #[cfg (feature = "deprecated_items" )] |
207 | Target::U8Buffer(ref actual, ref mut target) => { |
208 | target.clear(); |
209 | target.extend_from_slice(actual.as_bytes()); |
210 | } |
211 | } |
212 | self.saved = true; |
213 | } |
214 | Ok(()) |
215 | } |
216 | |
217 | fn draw_pixel( |
218 | &mut self, |
219 | point: BackendCoord, |
220 | color: BackendColor, |
221 | ) -> Result<(), DrawingErrorKind<Error>> { |
222 | if color.alpha == 0.0 { |
223 | return Ok(()); |
224 | } |
225 | self.open_tag( |
226 | SVGTag::Rectangle, |
227 | &[ |
228 | ("x" , &format!("{}" , point.0)), |
229 | ("y" , &format!("{}" , point.1)), |
230 | ("width" , "1" ), |
231 | ("height" , "1" ), |
232 | ("stroke" , "none" ), |
233 | ("opacity" , &make_svg_opacity(color)), |
234 | ("fill" , &make_svg_color(color)), |
235 | ], |
236 | true, |
237 | ); |
238 | Ok(()) |
239 | } |
240 | |
241 | fn draw_line<S: BackendStyle>( |
242 | &mut self, |
243 | from: BackendCoord, |
244 | to: BackendCoord, |
245 | style: &S, |
246 | ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
247 | if style.color().alpha == 0.0 { |
248 | return Ok(()); |
249 | } |
250 | self.open_tag( |
251 | SVGTag::Line, |
252 | &[ |
253 | ("opacity" , &make_svg_opacity(style.color())), |
254 | ("stroke" , &make_svg_color(style.color())), |
255 | ("stroke-width" , &format!("{}" , style.stroke_width())), |
256 | ("x1" , &format!("{}" , from.0)), |
257 | ("y1" , &format!("{}" , from.1)), |
258 | ("x2" , &format!("{}" , to.0)), |
259 | ("y2" , &format!("{}" , to.1)), |
260 | ], |
261 | true, |
262 | ); |
263 | Ok(()) |
264 | } |
265 | |
266 | fn draw_rect<S: BackendStyle>( |
267 | &mut self, |
268 | upper_left: BackendCoord, |
269 | bottom_right: BackendCoord, |
270 | style: &S, |
271 | fill: bool, |
272 | ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
273 | if style.color().alpha == 0.0 { |
274 | return Ok(()); |
275 | } |
276 | |
277 | let (fill, stroke) = if !fill { |
278 | ("none" .to_string(), make_svg_color(style.color())) |
279 | } else { |
280 | (make_svg_color(style.color()), "none" .to_string()) |
281 | }; |
282 | |
283 | self.open_tag( |
284 | SVGTag::Rectangle, |
285 | &[ |
286 | ("x" , &format!("{}" , upper_left.0)), |
287 | ("y" , &format!("{}" , upper_left.1)), |
288 | ("width" , &format!("{}" , bottom_right.0 - upper_left.0)), |
289 | ("height" , &format!("{}" , bottom_right.1 - upper_left.1)), |
290 | ("opacity" , &make_svg_opacity(style.color())), |
291 | ("fill" , &fill), |
292 | ("stroke" , &stroke), |
293 | ], |
294 | true, |
295 | ); |
296 | |
297 | Ok(()) |
298 | } |
299 | |
300 | fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>( |
301 | &mut self, |
302 | path: I, |
303 | style: &S, |
304 | ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
305 | if style.color().alpha == 0.0 { |
306 | return Ok(()); |
307 | } |
308 | self.open_tag( |
309 | SVGTag::Polyline, |
310 | &[ |
311 | ("fill" , "none" ), |
312 | ("opacity" , &make_svg_opacity(style.color())), |
313 | ("stroke" , &make_svg_color(style.color())), |
314 | ("stroke-width" , &format!("{}" , style.stroke_width())), |
315 | ( |
316 | "points" , |
317 | &path.into_iter().fold(String::new(), |mut s, (x, y)| { |
318 | write!(s, "{},{} " , x, y).ok(); |
319 | s |
320 | }), |
321 | ), |
322 | ], |
323 | true, |
324 | ); |
325 | Ok(()) |
326 | } |
327 | |
328 | fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>( |
329 | &mut self, |
330 | path: I, |
331 | style: &S, |
332 | ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
333 | if style.color().alpha == 0.0 { |
334 | return Ok(()); |
335 | } |
336 | self.open_tag( |
337 | SVGTag::Polygon, |
338 | &[ |
339 | ("opacity" , &make_svg_opacity(style.color())), |
340 | ("fill" , &make_svg_color(style.color())), |
341 | ( |
342 | "points" , |
343 | &path.into_iter().fold(String::new(), |mut s, (x, y)| { |
344 | write!(s, "{},{} " , x, y).ok(); |
345 | s |
346 | }), |
347 | ), |
348 | ], |
349 | true, |
350 | ); |
351 | Ok(()) |
352 | } |
353 | |
354 | fn draw_circle<S: BackendStyle>( |
355 | &mut self, |
356 | center: BackendCoord, |
357 | radius: u32, |
358 | style: &S, |
359 | fill: bool, |
360 | ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
361 | if style.color().alpha == 0.0 { |
362 | return Ok(()); |
363 | } |
364 | let (stroke, fill) = if !fill { |
365 | (make_svg_color(style.color()), "none" .to_string()) |
366 | } else { |
367 | ("none" .to_string(), make_svg_color(style.color())) |
368 | }; |
369 | self.open_tag( |
370 | SVGTag::Circle, |
371 | &[ |
372 | ("cx" , &format!("{}" , center.0)), |
373 | ("cy" , &format!("{}" , center.1)), |
374 | ("r" , &format!("{}" , radius)), |
375 | ("opacity" , &make_svg_opacity(style.color())), |
376 | ("fill" , &fill), |
377 | ("stroke" , &stroke), |
378 | ("stroke-width" , &format!("{}" , style.stroke_width())), |
379 | ], |
380 | true, |
381 | ); |
382 | Ok(()) |
383 | } |
384 | |
385 | fn draw_text<S: BackendTextStyle>( |
386 | &mut self, |
387 | text: &str, |
388 | style: &S, |
389 | pos: BackendCoord, |
390 | ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
391 | let color = style.color(); |
392 | if color.alpha == 0.0 { |
393 | return Ok(()); |
394 | } |
395 | |
396 | let (x0, y0) = pos; |
397 | let text_anchor = match style.anchor().h_pos { |
398 | HPos::Left => "start" , |
399 | HPos::Right => "end" , |
400 | HPos::Center => "middle" , |
401 | }; |
402 | |
403 | let dy = match style.anchor().v_pos { |
404 | VPos::Top => "0.76em" , |
405 | VPos::Center => "0.5ex" , |
406 | VPos::Bottom => "-0.5ex" , |
407 | }; |
408 | |
409 | #[cfg (feature = "debug" )] |
410 | { |
411 | let ((fx0, fy0), (fx1, fy1)) = |
412 | font.layout_box(text).map_err(DrawingErrorKind::FontError)?; |
413 | let x0 = match style.anchor().h_pos { |
414 | HPos::Left => x0, |
415 | HPos::Center => x0 - fx1 / 2 + fx0 / 2, |
416 | HPos::Right => x0 - fx1 + fx0, |
417 | }; |
418 | let y0 = match style.anchor().v_pos { |
419 | VPos::Top => y0, |
420 | VPos::Center => y0 - fy1 / 2 + fy0 / 2, |
421 | VPos::Bottom => y0 - fy1 + fy0, |
422 | }; |
423 | self.draw_rect( |
424 | (x0, y0), |
425 | (x0 + fx1 - fx0, y0 + fy1 - fy0), |
426 | &crate::prelude::RED, |
427 | false, |
428 | ) |
429 | .unwrap(); |
430 | self.draw_circle((x0, y0), 2, &crate::prelude::RED, false) |
431 | .unwrap(); |
432 | } |
433 | |
434 | let mut attrs = vec![ |
435 | ("x" , format!("{}" , x0)), |
436 | ("y" , format!("{}" , y0)), |
437 | ("dy" , dy.to_owned()), |
438 | ("text-anchor" , text_anchor.to_string()), |
439 | ("font-family" , style.family().as_str().to_string()), |
440 | ("font-size" , format!("{}" , style.size() / 1.24)), |
441 | ("opacity" , make_svg_opacity(color)), |
442 | ("fill" , make_svg_color(color)), |
443 | ]; |
444 | |
445 | match style.style() { |
446 | FontStyle::Normal => {} |
447 | FontStyle::Bold => attrs.push(("font-weight" , "bold" .to_string())), |
448 | other_style => attrs.push(("font-style" , other_style.as_str().to_string())), |
449 | }; |
450 | |
451 | let trans = style.transform(); |
452 | match trans { |
453 | FontTransform::Rotate90 => { |
454 | attrs.push(("transform" , format!("rotate(90, {}, {})" , x0, y0))) |
455 | } |
456 | FontTransform::Rotate180 => { |
457 | attrs.push(("transform" , format!("rotate(180, {}, {})" , x0, y0))); |
458 | } |
459 | FontTransform::Rotate270 => { |
460 | attrs.push(("transform" , format!("rotate(270, {}, {})" , x0, y0))); |
461 | } |
462 | _ => {} |
463 | } |
464 | |
465 | self.open_tag( |
466 | SVGTag::Text, |
467 | attrs |
468 | .iter() |
469 | .map(|(a, b)| (*a, b.as_ref())) |
470 | .collect::<Vec<_>>() |
471 | .as_ref(), |
472 | false, |
473 | ); |
474 | |
475 | Self::escape_and_push(self.target.get_mut(), text); |
476 | self.target.get_mut().push(' \n' ); |
477 | |
478 | self.close_tag(); |
479 | |
480 | Ok(()) |
481 | } |
482 | |
483 | #[cfg (all(not(target_arch = "wasm32" ), feature = "image" ))] |
484 | fn blit_bitmap<'b>( |
485 | &mut self, |
486 | pos: BackendCoord, |
487 | (w, h): (u32, u32), |
488 | src: &'b [u8], |
489 | ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
490 | use image::codecs::png::PngEncoder; |
491 | use image::ImageEncoder; |
492 | |
493 | let mut data = vec![0; 0]; |
494 | |
495 | { |
496 | let cursor = Cursor::new(&mut data); |
497 | |
498 | let encoder = PngEncoder::new(cursor); |
499 | |
500 | let color = image::ColorType::Rgb8; |
501 | |
502 | encoder.write_image(src, w, h, color).map_err(|e| { |
503 | DrawingErrorKind::DrawingError(Error::new( |
504 | std::io::ErrorKind::Other, |
505 | format!("Image error: {}" , e), |
506 | )) |
507 | })?; |
508 | } |
509 | |
510 | let padding = (3 - data.len() % 3) % 3; |
511 | for _ in 0..padding { |
512 | data.push(0); |
513 | } |
514 | |
515 | let mut rem_bits = 0; |
516 | let mut rem_num = 0; |
517 | |
518 | fn cvt_base64(from: u8) -> char { |
519 | (if from < 26 { |
520 | b'A' + from |
521 | } else if from < 52 { |
522 | b'a' + from - 26 |
523 | } else if from < 62 { |
524 | b'0' + from - 52 |
525 | } else if from == 62 { |
526 | b'+' |
527 | } else { |
528 | b'/' |
529 | }) |
530 | .into() |
531 | } |
532 | |
533 | let mut buf = String::new(); |
534 | buf.push_str("data:png;base64," ); |
535 | |
536 | for byte in data { |
537 | let value = (rem_bits << (6 - rem_num)) | (byte >> (rem_num + 2)); |
538 | rem_bits = byte & ((1 << (2 + rem_num)) - 1); |
539 | rem_num += 2; |
540 | |
541 | buf.push(cvt_base64(value)); |
542 | if rem_num == 6 { |
543 | buf.push(cvt_base64(rem_bits)); |
544 | rem_bits = 0; |
545 | rem_num = 0; |
546 | } |
547 | } |
548 | |
549 | for _ in 0..padding { |
550 | buf.pop(); |
551 | buf.push('=' ); |
552 | } |
553 | |
554 | self.open_tag( |
555 | SVGTag::Image, |
556 | &[ |
557 | ("x" , &format!("{}" , pos.0)), |
558 | ("y" , &format!("{}" , pos.1)), |
559 | ("width" , &format!("{}" , w)), |
560 | ("height" , &format!("{}" , h)), |
561 | ("href" , buf.as_str()), |
562 | ], |
563 | true, |
564 | ); |
565 | |
566 | Ok(()) |
567 | } |
568 | } |
569 | |
570 | impl Drop for SVGBackend<'_> { |
571 | fn drop(&mut self) { |
572 | if !self.saved { |
573 | // drop should not panic, so we ignore a failed present |
574 | let _ = self.present(); |
575 | } |
576 | } |
577 | } |
578 | |
579 | #[cfg (test)] |
580 | mod test { |
581 | use super::*; |
582 | use plotters::element::Circle; |
583 | use plotters::prelude::{ |
584 | ChartBuilder, Color, IntoDrawingArea, IntoFont, SeriesLabelPosition, TextStyle, BLACK, |
585 | BLUE, RED, WHITE, |
586 | }; |
587 | use plotters::style::text_anchor::{HPos, Pos, VPos}; |
588 | use std::fs; |
589 | use std::path::Path; |
590 | |
591 | static DST_DIR: &str = "target/test/svg" ; |
592 | |
593 | fn checked_save_file(name: &str, content: &str) { |
594 | /* |
595 | Please use the SVG file to manually verify the results. |
596 | */ |
597 | assert!(!content.is_empty()); |
598 | fs::create_dir_all(DST_DIR).unwrap(); |
599 | let file_name = format!("{}.svg" , name); |
600 | let file_path = Path::new(DST_DIR).join(file_name); |
601 | println!("{:?} created" , file_path); |
602 | fs::write(file_path, &content).unwrap(); |
603 | } |
604 | |
605 | fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) { |
606 | let mut content: String = Default::default(); |
607 | { |
608 | let root = SVGBackend::with_string(&mut content, (500, 500)).into_drawing_area(); |
609 | |
610 | let mut chart = ChartBuilder::on(&root) |
611 | .caption("This is a test" , ("sans-serif" , 20u32)) |
612 | .set_all_label_area_size(40u32) |
613 | .build_cartesian_2d(0..10, 0..10) |
614 | .unwrap(); |
615 | |
616 | chart |
617 | .configure_mesh() |
618 | .set_all_tick_mark_size(tick_size) |
619 | .draw() |
620 | .unwrap(); |
621 | } |
622 | |
623 | checked_save_file(test_name, &content); |
624 | |
625 | assert!(content.contains("This is a test" )); |
626 | } |
627 | |
628 | #[test] |
629 | fn test_draw_mesh_no_ticks() { |
630 | draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks" ); |
631 | } |
632 | |
633 | #[test] |
634 | fn test_draw_mesh_negative_ticks() { |
635 | draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks" ); |
636 | } |
637 | |
638 | #[test] |
639 | fn test_text_alignments() { |
640 | let mut content: String = Default::default(); |
641 | { |
642 | let mut root = SVGBackend::with_string(&mut content, (500, 500)); |
643 | |
644 | let style = TextStyle::from(("sans-serif" , 20).into_font()) |
645 | .pos(Pos::new(HPos::Right, VPos::Top)); |
646 | root.draw_text("right-align" , &style, (150, 50)).unwrap(); |
647 | |
648 | let style = style.pos(Pos::new(HPos::Center, VPos::Top)); |
649 | root.draw_text("center-align" , &style, (150, 150)).unwrap(); |
650 | |
651 | let style = style.pos(Pos::new(HPos::Left, VPos::Top)); |
652 | root.draw_text("left-align" , &style, (150, 200)).unwrap(); |
653 | } |
654 | |
655 | checked_save_file("test_text_alignments" , &content); |
656 | |
657 | for svg_line in content.split("</text>" ) { |
658 | if let Some(anchor_and_rest) = svg_line.split("text-anchor= \"" ).nth(1) { |
659 | if anchor_and_rest.starts_with("end" ) { |
660 | assert!(anchor_and_rest.contains("right-align" )) |
661 | } |
662 | if anchor_and_rest.starts_with("middle" ) { |
663 | assert!(anchor_and_rest.contains("center-align" )) |
664 | } |
665 | if anchor_and_rest.starts_with("start" ) { |
666 | assert!(anchor_and_rest.contains("left-align" )) |
667 | } |
668 | } |
669 | } |
670 | } |
671 | |
672 | #[test] |
673 | fn test_text_draw() { |
674 | let mut content: String = Default::default(); |
675 | { |
676 | let root = SVGBackend::with_string(&mut content, (1500, 800)).into_drawing_area(); |
677 | let root = root |
678 | .titled("Image Title" , ("sans-serif" , 60).into_font()) |
679 | .unwrap(); |
680 | |
681 | let mut chart = ChartBuilder::on(&root) |
682 | .caption("All anchor point positions" , ("sans-serif" , 20u32)) |
683 | .set_all_label_area_size(40u32) |
684 | .build_cartesian_2d(0..100i32, 0..50i32) |
685 | .unwrap(); |
686 | |
687 | chart |
688 | .configure_mesh() |
689 | .disable_x_mesh() |
690 | .disable_y_mesh() |
691 | .x_desc("X Axis" ) |
692 | .y_desc("Y Axis" ) |
693 | .draw() |
694 | .unwrap(); |
695 | |
696 | let ((x1, y1), (x2, y2), (x3, y3)) = ((-30, 30), (0, -30), (30, 30)); |
697 | |
698 | for (dy, trans) in [ |
699 | FontTransform::None, |
700 | FontTransform::Rotate90, |
701 | FontTransform::Rotate180, |
702 | FontTransform::Rotate270, |
703 | ] |
704 | .iter() |
705 | .enumerate() |
706 | { |
707 | for (dx1, h_pos) in [HPos::Left, HPos::Right, HPos::Center].iter().enumerate() { |
708 | for (dx2, v_pos) in [VPos::Top, VPos::Center, VPos::Bottom].iter().enumerate() { |
709 | let x = 150_i32 + (dx1 as i32 * 3 + dx2 as i32) * 150; |
710 | let y = 120 + dy as i32 * 150; |
711 | let draw = |x, y, text| { |
712 | root.draw(&Circle::new((x, y), 3, &BLACK.mix(0.5))).unwrap(); |
713 | let style = TextStyle::from(("sans-serif" , 20).into_font()) |
714 | .pos(Pos::new(*h_pos, *v_pos)) |
715 | .transform(trans.clone()); |
716 | root.draw_text(text, &style, (x, y)).unwrap(); |
717 | }; |
718 | draw(x + x1, y + y1, "dood" ); |
719 | draw(x + x2, y + y2, "dog" ); |
720 | draw(x + x3, y + y3, "goog" ); |
721 | } |
722 | } |
723 | } |
724 | } |
725 | |
726 | checked_save_file("test_text_draw" , &content); |
727 | |
728 | assert_eq!(content.matches("dog" ).count(), 36); |
729 | assert_eq!(content.matches("dood" ).count(), 36); |
730 | assert_eq!(content.matches("goog" ).count(), 36); |
731 | } |
732 | |
733 | #[test] |
734 | fn test_text_clipping() { |
735 | let mut content: String = Default::default(); |
736 | { |
737 | let (width, height) = (500_i32, 500_i32); |
738 | let root = SVGBackend::with_string(&mut content, (width as u32, height as u32)) |
739 | .into_drawing_area(); |
740 | |
741 | let style = TextStyle::from(("sans-serif" , 20).into_font()) |
742 | .pos(Pos::new(HPos::Center, VPos::Center)); |
743 | root.draw_text("TOP LEFT" , &style, (0, 0)).unwrap(); |
744 | root.draw_text("TOP CENTER" , &style, (width / 2, 0)) |
745 | .unwrap(); |
746 | root.draw_text("TOP RIGHT" , &style, (width, 0)).unwrap(); |
747 | |
748 | root.draw_text("MIDDLE LEFT" , &style, (0, height / 2)) |
749 | .unwrap(); |
750 | root.draw_text("MIDDLE RIGHT" , &style, (width, height / 2)) |
751 | .unwrap(); |
752 | |
753 | root.draw_text("BOTTOM LEFT" , &style, (0, height)).unwrap(); |
754 | root.draw_text("BOTTOM CENTER" , &style, (width / 2, height)) |
755 | .unwrap(); |
756 | root.draw_text("BOTTOM RIGHT" , &style, (width, height)) |
757 | .unwrap(); |
758 | } |
759 | |
760 | checked_save_file("test_text_clipping" , &content); |
761 | } |
762 | |
763 | #[test] |
764 | fn test_series_labels() { |
765 | let mut content = String::default(); |
766 | { |
767 | let (width, height) = (500, 500); |
768 | let root = SVGBackend::with_string(&mut content, (width, height)).into_drawing_area(); |
769 | |
770 | let mut chart = ChartBuilder::on(&root) |
771 | .caption("All series label positions" , ("sans-serif" , 20u32)) |
772 | .set_all_label_area_size(40u32) |
773 | .build_cartesian_2d(0..50i32, 0..50i32) |
774 | .unwrap(); |
775 | |
776 | chart |
777 | .configure_mesh() |
778 | .disable_x_mesh() |
779 | .disable_y_mesh() |
780 | .draw() |
781 | .unwrap(); |
782 | |
783 | chart |
784 | .draw_series(std::iter::once(Circle::new((5, 15), 5u32, &RED))) |
785 | .expect("Drawing error" ) |
786 | .label("Series 1" ) |
787 | .legend(|(x, y)| Circle::new((x, y), 3u32, RED.filled())); |
788 | |
789 | chart |
790 | .draw_series(std::iter::once(Circle::new((5, 15), 10u32, &BLUE))) |
791 | .expect("Drawing error" ) |
792 | .label("Series 2" ) |
793 | .legend(|(x, y)| Circle::new((x, y), 3u32, BLUE.filled())); |
794 | |
795 | for pos in vec![ |
796 | SeriesLabelPosition::UpperLeft, |
797 | SeriesLabelPosition::MiddleLeft, |
798 | SeriesLabelPosition::LowerLeft, |
799 | SeriesLabelPosition::UpperMiddle, |
800 | SeriesLabelPosition::MiddleMiddle, |
801 | SeriesLabelPosition::LowerMiddle, |
802 | SeriesLabelPosition::UpperRight, |
803 | SeriesLabelPosition::MiddleRight, |
804 | SeriesLabelPosition::LowerRight, |
805 | SeriesLabelPosition::Coordinate(70, 70), |
806 | ] |
807 | .into_iter() |
808 | { |
809 | chart |
810 | .configure_series_labels() |
811 | .border_style(&BLACK.mix(0.5)) |
812 | .position(pos) |
813 | .draw() |
814 | .expect("Drawing error" ); |
815 | } |
816 | } |
817 | |
818 | checked_save_file("test_series_labels" , &content); |
819 | } |
820 | |
821 | #[test] |
822 | fn test_draw_pixel_alphas() { |
823 | let mut content = String::default(); |
824 | { |
825 | let (width, height) = (100_i32, 100_i32); |
826 | let root = SVGBackend::with_string(&mut content, (width as u32, height as u32)) |
827 | .into_drawing_area(); |
828 | root.fill(&WHITE).unwrap(); |
829 | |
830 | for i in -20..20 { |
831 | let alpha = i as f64 * 0.1; |
832 | root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha)) |
833 | .unwrap(); |
834 | } |
835 | } |
836 | |
837 | checked_save_file("test_draw_pixel_alphas" , &content); |
838 | } |
839 | } |
840 | |