├── .github └── dependabot.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── backend.rs └── lib.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .*.sw* 5 | backup/* 6 | **/Cargo.lock 7 | **/target 8 | examples/wasm-demo/www/pkg 9 | examples/.ipynb_checkpoints/ 10 | tarpaulin-report.html 11 | .vscode/* -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plotters-cairo" 3 | version = "0.7.0" 4 | authors = ["Hao Hou "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Plotters Cairo backend" 8 | homepage = "https://plotters-rs.github.io" 9 | repository = "https://github.com/plotters-rs/plotters-cairo" 10 | readme = "README.md" 11 | 12 | [dependencies] 13 | cairo-rs = { version = "0.20", default-features = false } 14 | 15 | [dependencies.plotters-backend] 16 | version = "0.3.5" 17 | 18 | [dev-dependencies] 19 | cairo-rs = { version = "0.20", features = ["ps"], default-features = false } 20 | 21 | [dev-dependencies.plotters] 22 | default-features = false 23 | version = "0.3.5" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hao Hou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plotters-cairo - The Cairo/GTK backend for Plotters 2 | 3 | This is a part of plotters project. For more details, please check the following links: 4 | 5 | - For high-level intro of Plotters, see: [Plotters on crates.io](https://crates.io/crates/plotters) 6 | - Check the main repo at [Plotters repo](https://github.com/38/plotters.git) 7 | - For detailed documentation about this crate, check [plotters-backend on docs.rs](https://docs.rs/plotters-backend/) 8 | - You can also visit Plotters [Homepage](https://plotters-rs.github.io) 9 | -------------------------------------------------------------------------------- /src/backend.rs: -------------------------------------------------------------------------------- 1 | use cairo::{Context as CairoContext, FontSlant, FontWeight}; 2 | 3 | use plotters_backend::text_anchor::{HPos, VPos}; 4 | #[allow(unused_imports)] 5 | use plotters_backend::{ 6 | BackendColor, BackendCoord, BackendStyle, BackendTextStyle, DrawingBackend, DrawingErrorKind, 7 | FontStyle, FontTransform, 8 | }; 9 | 10 | /// The drawing backend that is backed with a Cairo context 11 | pub struct CairoBackend<'a> { 12 | context: &'a CairoContext, 13 | width: u32, 14 | height: u32, 15 | init_flag: bool, 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct CairoError; 20 | 21 | impl std::fmt::Display for CairoError { 22 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 23 | write!(fmt, "{:?}", self) 24 | } 25 | } 26 | 27 | impl std::error::Error for CairoError {} 28 | 29 | impl<'a> CairoBackend<'a> { 30 | fn set_color(&self, color: &BackendColor) { 31 | self.context.set_source_rgba( 32 | f64::from(color.rgb.0) / 255.0, 33 | f64::from(color.rgb.1) / 255.0, 34 | f64::from(color.rgb.2) / 255.0, 35 | color.alpha, 36 | ); 37 | } 38 | 39 | fn set_stroke_width(&self, width: u32) { 40 | self.context.set_line_width(f64::from(width)); 41 | } 42 | 43 | fn set_font(&self, font: &S) { 44 | match font.style() { 45 | FontStyle::Normal => self.context.select_font_face( 46 | font.family().as_str(), 47 | FontSlant::Normal, 48 | FontWeight::Normal, 49 | ), 50 | FontStyle::Bold => self.context.select_font_face( 51 | font.family().as_str(), 52 | FontSlant::Normal, 53 | FontWeight::Bold, 54 | ), 55 | FontStyle::Oblique => self.context.select_font_face( 56 | font.family().as_str(), 57 | FontSlant::Oblique, 58 | FontWeight::Normal, 59 | ), 60 | FontStyle::Italic => self.context.select_font_face( 61 | font.family().as_str(), 62 | FontSlant::Italic, 63 | FontWeight::Normal, 64 | ), 65 | }; 66 | self.context.set_font_size(font.size()); 67 | } 68 | 69 | pub fn new(context: &'a CairoContext, (w, h): (u32, u32)) -> Result { 70 | Ok(Self { 71 | context, 72 | width: w, 73 | height: h, 74 | init_flag: false, 75 | }) 76 | } 77 | } 78 | 79 | impl<'a> DrawingBackend for CairoBackend<'a> { 80 | type ErrorType = cairo::Error; 81 | 82 | fn get_size(&self) -> (u32, u32) { 83 | (self.width, self.height) 84 | } 85 | 86 | fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind> { 87 | if !self.init_flag { 88 | let (x0, y0, x1, y1) = self 89 | .context 90 | .clip_extents() 91 | .map_err(DrawingErrorKind::DrawingError)?; 92 | 93 | self.context.scale( 94 | (x1 - x0) / f64::from(self.width), 95 | (y1 - y0) / f64::from(self.height), 96 | ); 97 | 98 | self.init_flag = true; 99 | } 100 | 101 | Ok(()) 102 | } 103 | 104 | fn present(&mut self) -> Result<(), DrawingErrorKind> { 105 | Ok(()) 106 | } 107 | 108 | fn draw_pixel( 109 | &mut self, 110 | point: BackendCoord, 111 | color: BackendColor, 112 | ) -> Result<(), DrawingErrorKind> { 113 | self.context 114 | .rectangle(f64::from(point.0), f64::from(point.1), 1.0, 1.0); 115 | self.context.set_source_rgba( 116 | f64::from(color.rgb.0) / 255.0, 117 | f64::from(color.rgb.1) / 255.0, 118 | f64::from(color.rgb.2) / 255.0, 119 | color.alpha, 120 | ); 121 | 122 | self.context 123 | .fill() 124 | .map_err(DrawingErrorKind::DrawingError)?; 125 | 126 | Ok(()) 127 | } 128 | 129 | fn draw_line( 130 | &mut self, 131 | from: BackendCoord, 132 | to: BackendCoord, 133 | style: &S, 134 | ) -> Result<(), DrawingErrorKind> { 135 | self.set_color(&style.color()); 136 | self.set_stroke_width(style.stroke_width()); 137 | 138 | self.context.move_to(f64::from(from.0), f64::from(from.1)); 139 | self.context.line_to(f64::from(to.0), f64::from(to.1)); 140 | 141 | self.context 142 | .stroke() 143 | .map_err(DrawingErrorKind::DrawingError)?; 144 | 145 | Ok(()) 146 | } 147 | 148 | fn draw_rect( 149 | &mut self, 150 | upper_left: BackendCoord, 151 | bottom_right: BackendCoord, 152 | style: &S, 153 | fill: bool, 154 | ) -> Result<(), DrawingErrorKind> { 155 | self.set_color(&style.color()); 156 | self.set_stroke_width(style.stroke_width()); 157 | 158 | self.context.rectangle( 159 | f64::from(upper_left.0), 160 | f64::from(upper_left.1), 161 | f64::from(bottom_right.0 - upper_left.0), 162 | f64::from(bottom_right.1 - upper_left.1), 163 | ); 164 | 165 | if fill { 166 | self.context 167 | .fill() 168 | .map_err(DrawingErrorKind::DrawingError)?; 169 | } else { 170 | self.context 171 | .stroke() 172 | .map_err(DrawingErrorKind::DrawingError)?; 173 | } 174 | 175 | Ok(()) 176 | } 177 | 178 | fn draw_path>( 179 | &mut self, 180 | path: I, 181 | style: &S, 182 | ) -> Result<(), DrawingErrorKind> { 183 | self.set_color(&style.color()); 184 | self.set_stroke_width(style.stroke_width()); 185 | 186 | let mut path = path.into_iter(); 187 | if let Some((x, y)) = path.next() { 188 | self.context.move_to(f64::from(x), f64::from(y)); 189 | } 190 | 191 | for (x, y) in path { 192 | self.context.line_to(f64::from(x), f64::from(y)); 193 | } 194 | 195 | self.context 196 | .stroke() 197 | .map_err(DrawingErrorKind::DrawingError)?; 198 | 199 | Ok(()) 200 | } 201 | 202 | fn fill_polygon>( 203 | &mut self, 204 | path: I, 205 | style: &S, 206 | ) -> Result<(), DrawingErrorKind> { 207 | self.set_color(&style.color()); 208 | self.set_stroke_width(style.stroke_width()); 209 | 210 | let mut path = path.into_iter(); 211 | 212 | if let Some((x, y)) = path.next() { 213 | self.context.move_to(f64::from(x), f64::from(y)); 214 | 215 | for (x, y) in path { 216 | self.context.line_to(f64::from(x), f64::from(y)); 217 | } 218 | 219 | self.context.close_path(); 220 | self.context 221 | .fill() 222 | .map_err(DrawingErrorKind::DrawingError)?; 223 | } 224 | 225 | Ok(()) 226 | } 227 | 228 | fn draw_circle( 229 | &mut self, 230 | center: BackendCoord, 231 | radius: u32, 232 | style: &S, 233 | fill: bool, 234 | ) -> Result<(), DrawingErrorKind> { 235 | self.set_color(&style.color()); 236 | self.set_stroke_width(style.stroke_width()); 237 | 238 | self.context.new_sub_path(); 239 | self.context.arc( 240 | f64::from(center.0), 241 | f64::from(center.1), 242 | f64::from(radius), 243 | 0.0, 244 | std::f64::consts::PI * 2.0, 245 | ); 246 | 247 | if fill { 248 | self.context 249 | .fill() 250 | .map_err(DrawingErrorKind::DrawingError)?; 251 | } else { 252 | self.context 253 | .stroke() 254 | .map_err(DrawingErrorKind::DrawingError)?; 255 | } 256 | 257 | Ok(()) 258 | } 259 | 260 | fn estimate_text_size( 261 | &self, 262 | text: &str, 263 | font: &S, 264 | ) -> Result<(u32, u32), DrawingErrorKind> { 265 | self.set_font(font); 266 | 267 | let extents = self 268 | .context 269 | .text_extents(text) 270 | .map_err(DrawingErrorKind::DrawingError)?; 271 | 272 | Ok((extents.width() as u32, extents.height() as u32)) 273 | } 274 | 275 | fn draw_text( 276 | &mut self, 277 | text: &str, 278 | style: &S, 279 | pos: BackendCoord, 280 | ) -> Result<(), DrawingErrorKind> { 281 | let color = style.color(); 282 | let (mut x, mut y) = (pos.0, pos.1); 283 | 284 | let degree = match style.transform() { 285 | FontTransform::None => 0.0, 286 | FontTransform::Rotate90 => 90.0, 287 | FontTransform::Rotate180 => 180.0, 288 | FontTransform::Rotate270 => 270.0, 289 | //FontTransform::RotateAngle(angle) => angle as f64, 290 | } / 180.0 291 | * std::f64::consts::PI; 292 | 293 | if degree != 0.0 { 294 | self.context 295 | .save() 296 | .map_err(DrawingErrorKind::DrawingError)?; 297 | self.context.translate(f64::from(x), f64::from(y)); 298 | self.context.rotate(degree); 299 | 300 | x = 0; 301 | y = 0; 302 | } 303 | 304 | self.set_font(style); 305 | self.set_color(&color); 306 | 307 | let extents = self 308 | .context 309 | .text_extents(text) 310 | .map_err(DrawingErrorKind::DrawingError)?; 311 | 312 | let dx = match style.anchor().h_pos { 313 | HPos::Left => 0.0, 314 | HPos::Right => -extents.width(), 315 | HPos::Center => -extents.width() / 2.0, 316 | }; 317 | let dy = match style.anchor().v_pos { 318 | VPos::Top => extents.height(), 319 | VPos::Center => extents.height() / 2.0, 320 | VPos::Bottom => 0.0, 321 | }; 322 | 323 | self.context.move_to( 324 | f64::from(x) + dx - extents.x_bearing(), 325 | f64::from(y) + dy - extents.y_bearing() - extents.height(), 326 | ); 327 | 328 | self.context 329 | .show_text(text) 330 | .map_err(DrawingErrorKind::DrawingError)?; 331 | 332 | if degree != 0.0 { 333 | self.context 334 | .restore() 335 | .map_err(DrawingErrorKind::DrawingError)?; 336 | } 337 | 338 | Ok(()) 339 | } 340 | } 341 | 342 | #[cfg(test)] 343 | mod test { 344 | use super::*; 345 | use plotters::prelude::*; 346 | use plotters_backend::text_anchor::{HPos, Pos, VPos}; 347 | use std::fs; 348 | use std::path::Path; 349 | 350 | static DST_DIR: &str = "target/test/cairo"; 351 | 352 | fn checked_save_file(name: &str, content: &str) { 353 | /* 354 | Please use the PS file to manually verify the results. 355 | 356 | You may want to use Ghostscript to view the file. 357 | */ 358 | assert!(!content.is_empty()); 359 | fs::create_dir_all(DST_DIR).unwrap(); 360 | let file_name = format!("{}.ps", name); 361 | let file_path = Path::new(DST_DIR).join(file_name); 362 | println!("{:?} created", file_path); 363 | fs::write(file_path, &content).unwrap(); 364 | } 365 | 366 | fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) { 367 | let buffer: Vec = vec![]; 368 | let surface = cairo::PsSurface::for_stream(500.0, 500.0, buffer).unwrap(); 369 | let cr = CairoContext::new(&surface).unwrap(); 370 | let root = CairoBackend::new(&cr, (500, 500)) 371 | .unwrap() 372 | .into_drawing_area(); 373 | 374 | // Text could be rendered to different elements if has whitespaces 375 | let mut chart = ChartBuilder::on(&root) 376 | .caption("this-is-a-test", ("sans-serif", 20)) 377 | .set_all_label_area_size(40) 378 | .build_cartesian_2d(0..10, 0..10) 379 | .unwrap(); 380 | 381 | chart 382 | .configure_mesh() 383 | .set_all_tick_mark_size(tick_size) 384 | .draw() 385 | .unwrap(); 386 | 387 | let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap(); 388 | let content = String::from_utf8(buffer).unwrap(); 389 | checked_save_file(test_name, &content); 390 | 391 | // FIXME: through some change in cairo or something the caption no longer 392 | // appears in plaintext so this assertion will fail even though the postscript 393 | // file contains the heading 394 | assert!(content.contains("this-is-a-test")); 395 | } 396 | 397 | #[test] 398 | fn test_draw_mesh_no_ticks() { 399 | draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks"); 400 | } 401 | 402 | #[test] 403 | fn test_draw_mesh_negative_ticks() { 404 | draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks"); 405 | } 406 | 407 | #[test] 408 | fn test_text_draw() { 409 | let buffer: Vec = vec![]; 410 | let (width, height) = (1500, 800); 411 | let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap(); 412 | let cr = CairoContext::new(&surface).unwrap(); 413 | let root = CairoBackend::new(&cr, (width, height)) 414 | .unwrap() 415 | .into_drawing_area(); 416 | let root = root 417 | .titled("Image Title", ("sans-serif", 60).into_font()) 418 | .unwrap(); 419 | 420 | let mut chart = ChartBuilder::on(&root) 421 | .caption("All anchor point positions", ("sans-serif", 20)) 422 | .set_all_label_area_size(40) 423 | .build_cartesian_2d(0..100, 0..50) 424 | .unwrap(); 425 | 426 | chart 427 | .configure_mesh() 428 | .disable_x_mesh() 429 | .disable_y_mesh() 430 | .x_desc("X Axis") 431 | .y_desc("Y Axis") 432 | .draw() 433 | .unwrap(); 434 | 435 | let ((x1, y1), (x2, y2), (x3, y3)) = ((-30, 30), (0, -30), (30, 30)); 436 | 437 | for (dy, trans) in [ 438 | FontTransform::None, 439 | FontTransform::Rotate90, 440 | FontTransform::Rotate180, 441 | FontTransform::Rotate270, 442 | ] 443 | .iter() 444 | .enumerate() 445 | { 446 | for (dx1, h_pos) in [HPos::Left, HPos::Right, HPos::Center].iter().enumerate() { 447 | for (dx2, v_pos) in [VPos::Top, VPos::Center, VPos::Bottom].iter().enumerate() { 448 | let x = 150_i32 + (dx1 as i32 * 3 + dx2 as i32) * 150; 449 | let y = 120 + dy as i32 * 150; 450 | let draw = |x, y, text| { 451 | root.draw(&Circle::new((x, y), 3, &BLACK.mix(0.5))).unwrap(); 452 | let style = TextStyle::from(("sans-serif", 20).into_font()) 453 | .pos(Pos::new(*h_pos, *v_pos)) 454 | .transform(trans.clone()); 455 | root.draw_text(text, &style, (x, y)).unwrap(); 456 | }; 457 | draw(x + x1, y + y1, "dood"); 458 | draw(x + x2, y + y2, "dog"); 459 | draw(x + x3, y + y3, "goog"); 460 | } 461 | } 462 | } 463 | 464 | let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap(); 465 | let content = String::from_utf8(buffer).unwrap(); 466 | checked_save_file("test_text_draw", &content); 467 | 468 | // FIXME: see `draw_mesh_with_custom_ticks` 469 | assert_eq!(content.matches("dog").count(), 36); 470 | assert_eq!(content.matches("dood").count(), 36); 471 | assert_eq!(content.matches("goog").count(), 36); 472 | } 473 | 474 | #[test] 475 | fn test_text_clipping() { 476 | let buffer: Vec = vec![]; 477 | let (width, height) = (500_i32, 500_i32); 478 | let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap(); 479 | let cr = CairoContext::new(&surface).unwrap(); 480 | let root = CairoBackend::new(&cr, (width as u32, height as u32)) 481 | .unwrap() 482 | .into_drawing_area(); 483 | 484 | let style = TextStyle::from(("sans-serif", 20).into_font()) 485 | .pos(Pos::new(HPos::Center, VPos::Center)); 486 | root.draw_text("TOP LEFT", &style, (0, 0)).unwrap(); 487 | root.draw_text("TOP CENTER", &style, (width / 2, 0)) 488 | .unwrap(); 489 | root.draw_text("TOP RIGHT", &style, (width, 0)).unwrap(); 490 | 491 | root.draw_text("MIDDLE LEFT", &style, (0, height / 2)) 492 | .unwrap(); 493 | root.draw_text("MIDDLE RIGHT", &style, (width, height / 2)) 494 | .unwrap(); 495 | 496 | root.draw_text("BOTTOM LEFT", &style, (0, height)).unwrap(); 497 | root.draw_text("BOTTOM CENTER", &style, (width / 2, height)) 498 | .unwrap(); 499 | root.draw_text("BOTTOM RIGHT", &style, (width, height)) 500 | .unwrap(); 501 | 502 | let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap(); 503 | let content = String::from_utf8(buffer).unwrap(); 504 | checked_save_file("test_text_clipping", &content); 505 | } 506 | 507 | #[test] 508 | fn test_series_labels() { 509 | let buffer: Vec = vec![]; 510 | let (width, height) = (500, 500); 511 | let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap(); 512 | let cr = CairoContext::new(&surface).unwrap(); 513 | let root = CairoBackend::new(&cr, (width, height)) 514 | .unwrap() 515 | .into_drawing_area(); 516 | 517 | let mut chart = ChartBuilder::on(&root) 518 | .caption("All series label positions", ("sans-serif", 20)) 519 | .set_all_label_area_size(40) 520 | .build_cartesian_2d(0..50, 0..50) 521 | .unwrap(); 522 | 523 | chart 524 | .configure_mesh() 525 | .disable_x_mesh() 526 | .disable_y_mesh() 527 | .draw() 528 | .unwrap(); 529 | 530 | chart 531 | .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED))) 532 | .expect("Drawing error") 533 | .label("Series 1") 534 | .legend(|(x, y)| Circle::new((x, y), 3, RED.filled())); 535 | 536 | chart 537 | .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE))) 538 | .expect("Drawing error") 539 | .label("Series 2") 540 | .legend(|(x, y)| Circle::new((x, y), 3, BLUE.filled())); 541 | 542 | for pos in vec![ 543 | SeriesLabelPosition::UpperLeft, 544 | SeriesLabelPosition::MiddleLeft, 545 | SeriesLabelPosition::LowerLeft, 546 | SeriesLabelPosition::UpperMiddle, 547 | SeriesLabelPosition::MiddleMiddle, 548 | SeriesLabelPosition::LowerMiddle, 549 | SeriesLabelPosition::UpperRight, 550 | SeriesLabelPosition::MiddleRight, 551 | SeriesLabelPosition::LowerRight, 552 | SeriesLabelPosition::Coordinate(70, 70), 553 | ] 554 | .into_iter() 555 | { 556 | chart 557 | .configure_series_labels() 558 | .border_style(&BLACK.mix(0.5)) 559 | .position(pos) 560 | .draw() 561 | .expect("Drawing error"); 562 | } 563 | 564 | let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap(); 565 | let content = String::from_utf8(buffer).unwrap(); 566 | checked_save_file("test_series_labels", &content); 567 | } 568 | 569 | #[test] 570 | fn test_draw_pixel_alphas() { 571 | let buffer: Vec = vec![]; 572 | let (width, height) = (100_i32, 100_i32); 573 | let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap(); 574 | let cr = CairoContext::new(&surface).unwrap(); 575 | let root = CairoBackend::new(&cr, (width as u32, height as u32)) 576 | .unwrap() 577 | .into_drawing_area(); 578 | 579 | for i in -20..20 { 580 | let alpha = i as f64 * 0.1; 581 | root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha)) 582 | .unwrap(); 583 | } 584 | 585 | let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap(); 586 | let content = String::from_utf8(buffer).unwrap(); 587 | checked_save_file("test_draw_pixel_alphas", &content); 588 | } 589 | } 590 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod backend; 2 | 3 | pub use backend::CairoBackend; 4 | --------------------------------------------------------------------------------