| 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 | |