]>
Commit | Line | Data |
---|---|---|
49aad941 FG |
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 | ||
4b012472 | 11 | use std::fmt::Write as _; |
49aad941 FG |
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; | |
49aad941 FG |
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 | } |