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