From 7e543a0ee91615ad672e7322e345edb9ef26645b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 17 Mar 2021 00:49:52 +0100 Subject: [PATCH] Add basic error handling --- Cargo.lock | 56 ++++++++++++++++++ Cargo.toml | 1 + src/bin/c2g.rs | 6 +- src/cli.rs | 40 +++++++++---- src/drawer.rs | 151 +++++++++++++++++++++++++++++++++++-------------- src/error.rs | 48 ++++++++++++++++ src/giffer.rs | 63 ++++++++++++++++----- src/lib.rs | 1 + 8 files changed, 296 insertions(+), 70 deletions(-) create mode 100644 src/error.rs diff --git a/Cargo.lock b/Cargo.lock index df257f3..f02d676 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,7 @@ dependencies = [ "pgn-reader", "resvg", "shakmaty", + "thiserror", "tiny-skia", "usvg", ] @@ -525,6 +526,24 @@ dependencies = [ "miniz_oxide 0.3.7", ] +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rayon" version = "1.5.0" @@ -726,6 +745,17 @@ dependencies = [ "siphasher", ] +[[package]] +name = "syn" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "termcolor" version = "1.1.2" @@ -744,6 +774,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.3" @@ -835,6 +885,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + [[package]] name = "usvg" version = "0.14.0" diff --git a/Cargo.toml b/Cargo.toml index 153e524..5fb0a12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,6 @@ log = "0.4" pgn-reader = "0.17" resvg = "0.14" shakmaty = "0.18" +thiserror = "1.0" tiny-skia = "0.5.0" usvg = "0.14" diff --git a/src/bin/c2g.rs b/src/bin/c2g.rs index 5c4c89f..7a7595c 100644 --- a/src/bin/c2g.rs +++ b/src/bin/c2g.rs @@ -1,8 +1,8 @@ -use c2g::cli::Chess2Gif; +use c2g::{cli::Chess2Gif, error::C2GError}; -fn main() { +fn main() -> Result<(), C2GError> { env_logger::init(); let c2g = Chess2Gif::new(); - c2g.run(); + c2g.run() } diff --git a/src/cli.rs b/src/cli.rs index c6434a0..c3b4239 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,6 +5,7 @@ use std::io; use clap::{App, Arg}; use pgn_reader::BufferedReader; +use crate::error::C2GError; use crate::giffer::PGNGiffer; pub struct Chess2Gif<'a> { @@ -17,7 +18,7 @@ impl<'a> Chess2Gif<'a> { Self::new_from(std::env::args_os().into_iter()).unwrap_or_else(|e| e.exit()) } - pub fn new_from(args: I) -> Result + pub fn new_from(args: I) -> Result where I: Iterator, T: Into + Clone, @@ -88,7 +89,11 @@ impl<'a> Chess2Gif<'a> { let matches = app.get_matches_from_safe(args)?; let size = u32::from_str_radix(matches.value_of("size").expect("Size must be defined"), 10) - .unwrap(); + .expect("Size must be a positive number"); + + if size % 8 != 0 { + return Err(C2GError::NotDivisibleBy8); + } let pgn = if matches.value_of("PGN").is_some() { Some(matches.value_of("PGN").unwrap().to_owned()) @@ -107,27 +112,42 @@ impl<'a> Chess2Gif<'a> { let dark: [u8; 4] = clap::values_t_or_exit!(matches, "dark", u8) .try_into() - .unwrap(); + .expect("Invalid dark color"); let light: [u8; 4] = clap::values_t_or_exit!(matches, "light", u8) .try_into() - .unwrap(); + .expect("Invalid light color"); Ok(Chess2Gif { pgn: pgn, - giffer: PGNGiffer::new(pieces_path, font_path, size, output, 100, dark, light), + giffer: PGNGiffer::new(pieces_path, font_path, size, output, 100, dark, light)?, }) } - pub fn run(mut self) { + pub fn run(mut self) -> Result<(), C2GError> { log::info!("Reading PGN"); - if let Some(pgn) = self.pgn { + let result = if let Some(pgn) = self.pgn { let mut reader = BufferedReader::new_cursor(&pgn[..]); - reader.read_game(&mut self.giffer); + match reader.read_game(&mut self.giffer) { + Ok(result) => match result { + // result contains Option> + Some(r) => Ok(r.unwrap()), + None => Ok(()), + }, + Err(e) => Err(C2GError::ReadGame { source: e }), + } } else { let stdin = io::stdin(); let mut reader = BufferedReader::new(stdin); - reader.read_game(&mut self.giffer); - } + match reader.read_game(&mut self.giffer) { + Ok(result) => match result { + // result contains Option> + Some(r) => Ok(r.unwrap()), + None => Ok(()), + }, + Err(e) => Err(C2GError::ReadGame { source: e }), + } + }; log::info!("Done!"); + result } } diff --git a/src/drawer.rs b/src/drawer.rs index bf01864..1c492fd 100644 --- a/src/drawer.rs +++ b/src/drawer.rs @@ -2,8 +2,9 @@ use image::{imageops, ImageBuffer, Rgba, RgbaImage}; use log; use resvg; use shakmaty::{self, File, Move, Pieces, Rank, Role, Square}; +use thiserror::Error; use tiny_skia::{self, Pixmap, PixmapPaint, Transform}; -use usvg::{fontdb, FitTo, Options, Tree}; +use usvg::{self, fontdb, FitTo, Options, Tree}; use crate::utils; @@ -15,6 +16,29 @@ pub struct BoardDrawer { light: Rgba, } +#[derive(Error, Debug)] +pub enum DrawerError { + #[error("Could not load font file")] + LoadFont { + #[from] + source: std::io::Error, + }, + #[error("Could not load piece svg file")] + LoadPieceSVG { + #[from] + source: usvg::Error, + }, + #[error("An image {image:?} is too big to fit in an ImageBuffer")] + ImageTooBig { image: String }, + #[error("SVG {svg:?} failed to be rendered")] + SVGRenderError { svg: String }, + #[error("A correct SVG for {coordinate:?} could not be produced")] + CoordinateSVG { + source: usvg::Error, + coordinate: char, + }, +} + impl BoardDrawer { pub fn new( image_path: String, @@ -22,25 +46,27 @@ impl BoardDrawer { size: u32, dark: [u8; 4], light: [u8; 4], - ) -> Self { + ) -> Result { let mut opt = usvg::Options::default(); if let Some(p) = font_path { let mut fonts = fontdb::Database::new(); - fonts.load_font_file(p); + fonts + .load_font_file(p) + .map_err(|source| DrawerError::LoadFont { source: source })?; opt.fontdb = fonts; opt.font_size = 16.0; // There should only be 1 font in DB opt.font_family = (*(opt.fontdb.faces())[0].family).to_owned(); } - BoardDrawer { + Ok(BoardDrawer { image_path: image_path, svg_options: opt, size: size, dark: image::Rgba(dark), light: image::Rgba(light), - } + }) } pub fn dark_color(&mut self) -> tiny_skia::Color { @@ -79,7 +105,7 @@ impl BoardDrawer { ImageBuffer::from_pixel(self.size / 8, self.size / 8, self.light) } - pub fn draw_position_from_empty(&mut self, pieces: Pieces) -> RgbaImage { + pub fn draw_position_from_empty(&mut self, pieces: Pieces) -> Result { log::debug!("Drawing initial board"); let mut counter = 1; let mut column = ImageBuffer::from_fn(self.size / 8, self.size, |_, y| { @@ -101,21 +127,33 @@ impl BoardDrawer { for (square, piece) in pieces { log::debug!("Initializing {:?} in {:?}", piece, square); - self.draw_piece(&square, &piece.role, piece.color, false, &mut board); + self.draw_piece(&square, &piece.role, piece.color, false, &mut board)?; } - self.draw_ranks(2, 6, &mut board); + self.draw_ranks(2, 6, &mut board)?; - board + Ok(board) } - pub fn draw_ranks(&mut self, from: u32, to: u32, img: &mut RgbaImage) { + pub fn draw_ranks( + &mut self, + from: u32, + to: u32, + img: &mut RgbaImage, + ) -> Result<(), DrawerError> { for n in from..to { let square = shakmaty::Square::new(n * 8); - self.draw_square(&square, img); + self.draw_square(&square, img)?; } + + Ok(()) } - pub fn draw_move(&mut self, _move: &Move, color: shakmaty::Color, img: &mut RgbaImage) { + pub fn draw_move( + &mut self, + _move: &Move, + color: shakmaty::Color, + img: &mut RgbaImage, + ) -> Result<(), DrawerError> { log::debug!("Drawing move: {:?}", _move); match _move { Move::Normal { @@ -125,39 +163,46 @@ impl BoardDrawer { to, promotion, } => { - self.draw_square(from, img); + self.draw_square(from, img)?; let blank_to_square = if capture.is_some() { true } else { false }; if let Some(promoted) = promotion { - self.draw_piece(to, promoted, color, blank_to_square, img); + self.draw_piece(to, promoted, color, blank_to_square, img)?; } else { - self.draw_piece(to, role, color, blank_to_square, img); + self.draw_piece(to, role, color, blank_to_square, img)?; } } Move::EnPassant { from, to } => { - self.draw_square(from, img); - self.draw_piece(to, &Role::Pawn, color, true, img); + self.draw_square(from, img)?; + self.draw_piece(to, &Role::Pawn, color, true, img)?; } Move::Castle { king, rook } => { // King and Rook initial squares, e.g. E1 and H1 respectively. // Need to calculate where the pieces end up before drawing. let offset = if rook.file() > king.file() { 1 } else { -1 }; - self.draw_square(king, img); - self.draw_square(rook, img); + self.draw_square(king, img)?; + self.draw_square(rook, img)?; let rook_square = king.offset(offset * 1).unwrap(); let king_square = king.offset(offset * 2).unwrap(); - self.draw_piece(&king_square, &Role::King, color, true, img); - self.draw_piece(&rook_square, &Role::Rook, color, true, img); + self.draw_piece(&king_square, &Role::King, color, true, img)?; + self.draw_piece(&rook_square, &Role::Rook, color, true, img)?; } Move::Put { role, to } => { - self.draw_piece(to, role, color, true, img); + self.draw_piece(to, role, color, true, img)?; } }; + + Ok(()) } - pub fn square_pixmap(&mut self, height: u32, width: u32, square: &Square) -> Pixmap { + pub fn square_pixmap( + &mut self, + height: u32, + width: u32, + square: &Square, + ) -> Result { let mut pixmap = Pixmap::new(width, height).unwrap(); match square.is_dark() { true => pixmap.fill(self.dark_color()), @@ -172,7 +217,7 @@ impl BoardDrawer { self.size / 32, self.size / 128, self.size / 32 - self.size / 128, - ); + )?; let paint = PixmapPaint::default(); let transform = Transform::default(); pixmap.draw_pixmap( @@ -192,14 +237,14 @@ impl BoardDrawer { self.size / 32, self.size / 128, self.size / 32, - ); + )?; let paint = PixmapPaint::default(); let transform = Transform::default(); pixmap.draw_pixmap(0, 0, rank_pixmap.as_ref(), &paint, transform, None); } } - pixmap + Ok(pixmap) } pub fn piece_image<'a>( @@ -209,7 +254,7 @@ impl BoardDrawer { role: &'a Role, height: u32, width: u32, - ) -> RgbaImage { + ) -> Result { let fit_to = FitTo::Height(height); let file_path = format!( "{}/{}_{}.svg", @@ -217,11 +262,18 @@ impl BoardDrawer { piece_color.char(), role.char() ); - let rtree = Tree::from_file(file_path, &self.svg_options).unwrap(); - let mut pixmap = self.square_pixmap(height, width, square); - resvg::render(&rtree, fit_to, pixmap.as_mut()).unwrap(); - - ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap() + let rtree = Tree::from_file(file_path, &self.svg_options) + .map_err(|source| DrawerError::LoadPieceSVG { source: source })?; + let mut pixmap = self.square_pixmap(height, width, square)?; + resvg::render(&rtree, fit_to, pixmap.as_mut()).ok_or(DrawerError::SVGRenderError { + svg: format!("{}_{}.svg", piece_color.char(), role.char()), + })?; + + ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).ok_or( + DrawerError::ImageTooBig { + image: format!("{}_{}.svg", piece_color.char(), role.char()), + }, + ) } pub fn coordinate_pixmap( @@ -232,7 +284,7 @@ impl BoardDrawer { width: u32, x: u32, y: u32, - ) -> Pixmap { + ) -> Result { log::debug!("Generating svg text: {}", coordinate); let mut pixmap = Pixmap::new(width, height).unwrap(); let (square_color, coord_color) = match square.is_dark() { @@ -261,24 +313,35 @@ impl BoardDrawer { coordinate, ); - let rtree = Tree::from_str(&svg_string, &self.svg_options).unwrap(); + let rtree = Tree::from_str(&svg_string, &self.svg_options).map_err(|source| { + DrawerError::CoordinateSVG { + source: source, + coordinate: coordinate, + } + })?; let fit_to = FitTo::Height(height); - resvg::render(&rtree, fit_to, pixmap.as_mut()).unwrap(); + resvg::render(&rtree, fit_to, pixmap.as_mut()).ok_or(DrawerError::SVGRenderError { + svg: coordinate.to_string(), + })?; - pixmap + Ok(pixmap) } - pub fn draw_square(&mut self, square: &Square, img: &mut RgbaImage) { + pub fn draw_square(&mut self, square: &Square, img: &mut RgbaImage) -> Result<(), DrawerError> { log::debug!("Drawing square: {}", square); - let pixmap = self.square_pixmap(self.size / 8, self.size / 8, square); - let square_img = - ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap(); + let pixmap = self.square_pixmap(self.size / 8, self.size / 8, square)?; + let square_img = ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()) + .ok_or(DrawerError::ImageTooBig { + image: format!("{}x{} square", self.size / 8, self.size / 8), + })?; let x = self.size / 8 * u32::from(square.file()); let y = self.size - self.size / 8 * (u32::from(square.rank()) + 1); imageops::overlay(img, &square_img, x, y); + + Ok(()) } pub fn draw_piece( @@ -288,10 +351,10 @@ impl BoardDrawer { color: shakmaty::Color, blank_target: bool, img: &mut RgbaImage, - ) { + ) -> Result<(), DrawerError> { log::debug!("Drawing {:?} {:?} on {:?}", color, role, square); if blank_target { - self.draw_square(square, img); + self.draw_square(square, img)?; } let x = self.size / 8 * u32::from(square.file()); @@ -299,7 +362,9 @@ impl BoardDrawer { log::debug!("Piece coordinates: ({}, {})", x, y); let height = self.size / 8; - let resized_piece = self.piece_image(color, square, role, height, height); + let resized_piece = self.piece_image(color, square, role, height, height)?; imageops::replace(img, &resized_piece, x, y); + + Ok(()) } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..c897ba1 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,48 @@ +use std::io::{stderr, Write}; +use std::process; + +use clap; +use thiserror::Error; + +use crate::giffer::GifferError; + +#[derive(Error, Debug)] +pub enum C2GError { + #[error("Failed to read PGN chess game")] + ReadGame { + #[from] + source: std::io::Error, + }, + #[error("Failed to produce a GIF")] + GIFRenderingError { + #[from] + source: GifferError, + }, + #[error("Size is not divisible by 8")] + NotDivisibleBy8, + #[error("Clap failed")] + ClapError { + #[from] + source: clap::Error, + }, +} + +impl C2GError { + pub fn exit(&self) -> ! { + match self { + C2GError::ClapError { source: s } => s.exit(), + C2GError::GIFRenderingError { source: _ } => { + writeln!(&mut stderr(), "{}", self).ok(); + process::exit(1); + } + C2GError::ReadGame { source: _ } => { + writeln!(&mut stderr(), "{}", self).ok(); + process::exit(1); + } + C2GError::NotDivisibleBy8 => { + writeln!(&mut stderr(), "{}", self).ok(); + process::exit(1); + } + } + } +} diff --git a/src/giffer.rs b/src/giffer.rs index 8819e9e..52198c8 100644 --- a/src/giffer.rs +++ b/src/giffer.rs @@ -1,12 +1,13 @@ use std::fs; use std::io::BufWriter; -use gif::{Encoder, Frame, Repeat}; +use gif::{self, Encoder, Frame, Repeat}; use log; use pgn_reader::{SanPlus, Skip, Visitor}; use shakmaty::{Chess, Position, Setup}; +use thiserror::Error; -use crate::drawer::BoardDrawer; +use crate::drawer::{BoardDrawer, DrawerError}; pub struct PGNGiffer<'a> { drawer: BoardDrawer, @@ -16,6 +17,24 @@ pub struct PGNGiffer<'a> { frames: Vec>, } +#[derive(Error, Debug)] +pub enum GifferError { + #[error("Failed to create GIF output file")] + CreateOutput { + #[from] + source: std::io::Error, + }, + #[error("A GIF encoder could not be initialized")] + InitializeEncoder { source: gif::EncodingError }, + #[error("A GIF frame could not be encoded")] + FrameEncoding { source: gif::EncodingError }, + #[error("BoardDrawer failed")] + DrawerError { + #[from] + source: DrawerError, + }, +} + impl<'a> PGNGiffer<'a> { pub fn new( image_path: &str, @@ -25,8 +44,9 @@ impl<'a> PGNGiffer<'a> { ms_delay: u16, dark: [u8; 4], light: [u8; 4], - ) -> Self { - let file = fs::File::create(output_path).unwrap(); + ) -> Result { + let file = + fs::File::create(output_path).map_err(|source| GifferError::CreateOutput { source })?; let buffer = BufWriter::with_capacity(1000, file); let drawer = BoardDrawer::new( @@ -35,29 +55,38 @@ impl<'a> PGNGiffer<'a> { board_size as u32, dark, light, - ); + ) + .map_err(|source| GifferError::DrawerError { source: source })?; - let mut encoder = - Encoder::new(buffer, drawer.size() as u16, drawer.size() as u16, &[]).unwrap(); - encoder.set_repeat(Repeat::Infinite).unwrap(); + let mut encoder = Encoder::new(buffer, drawer.size() as u16, drawer.size() as u16, &[]) + .map_err(|source| GifferError::InitializeEncoder { source })?; + encoder + .set_repeat(Repeat::Infinite) + .map_err(|source| GifferError::InitializeEncoder { source })?; - PGNGiffer { + Ok(PGNGiffer { drawer: drawer, position: Chess::default(), encoder: encoder, delay: ms_delay, frames: Vec::new(), - } + }) } } impl<'a> Visitor for PGNGiffer<'a> { - type Result = (); + type Result = Result<(), GifferError>; fn begin_game(&mut self) { log::info!("Rendering initial board"); let pieces = self.position.board().pieces(); - let board = self.drawer.draw_position_from_empty(pieces); + let board = self + .drawer + .draw_position_from_empty(pieces) + .expect(&format!( + "Failed to draw initial position: {}", + self.position.board() + )); let mut frame = Frame::from_rgba_speed( self.drawer.size() as u16, @@ -76,7 +105,9 @@ impl<'a> Visitor for PGNGiffer<'a> { fn san(&mut self, san_plus: SanPlus) { if let Ok(m) = san_plus.san.to_move(&self.position) { let mut board = self.drawer.image_buffer(); - self.drawer.draw_move(&m, self.position.turn(), &mut board); + self.drawer + .draw_move(&m, self.position.turn(), &mut board) + .expect(&format!("Failed to draw move: {}", m)); let mut frame = Frame::from_rgba_speed( self.drawer.size() as u16, @@ -96,7 +127,11 @@ impl<'a> Visitor for PGNGiffer<'a> { (*last).delay = self.delay * 5; } for f in self.frames.iter() { - self.encoder.write_frame(f); + self.encoder + .write_frame(f) + .map_err(|source| GifferError::FrameEncoding { source })?; } + + Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index ed6afea..a99cd31 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,5 +3,6 @@ extern crate clap; pub mod cli; pub mod drawer; +pub mod error; pub mod giffer; pub mod utils;