use chess::{Board, ChessMove, Color, Piece, Square};
use lopdf::{
Document, Object, Stream,
content::{Content, Operation},
dictionary,
};
use std::collections::VecDeque;
use std::str::FromStr;
use crate::{PuzzleTab, config, lang};
pub fn to_pdf(puzzles: &[config::Puzzle], number_of_pages: i32, lang: &lang::Language, path: String) {
let font_data = config::CHESS_ALPHA_BYTES;
let font_stream = Stream::new(dictionary! {}, font_data.to_vec());
let mut doc = Document::with_version("1.7");
let font_stream_id = doc.add_object(font_stream);
let font_descriptor_dict = dictionary! {
"Type" => "FontDescriptor",
"FontName" => "Chess Alpha",
"FontFile2" => font_stream_id,
"FontBBox" => vec![0.into(),0.into(), 1024.into(), 1024.into()],
"Flags" => 33,
};
let font_descriptor_id = doc.add_object(font_descriptor_dict);
let encoding = dictionary! {
"Type" => "Encoding",
"BaseEncoding" => "WinAnsiEncoding",
};
let font_dict = dictionary! {
"Type" => "Font",
"Subtype" => "TrueType",
"BaseFont" => "Chess Alpha",
"FirstChar" => 32,
"LastChar" => 255,
"Widths" => vec![1024.into();256],
"FontDescriptor" => font_descriptor_id,
"Encoding" => doc.add_object(encoding),
"Length1" => font_data.len() as i32
};
let font_id = doc.add_object(font_dict);
let pages_id = doc.new_object_id();
let regular_font_id = doc.add_object(dictionary! {
"Type" => "Font",
"Subtype" => "TrueType",
"BaseFont" => "Arial",
});
let resources_id = doc.add_object(dictionary! {
"Font" => dictionary! {
"Chess Alpha" => font_id,
"Regular" => regular_font_id,
},
});
let num_of_puzzles_to_print;
let num_of_pages;
if (6 * number_of_pages) as usize > puzzles.len() {
num_of_puzzles_to_print = puzzles.len();
num_of_pages = (puzzles.len() as f32 / 6.0).ceil() as usize;
} else {
num_of_puzzles_to_print = (6 * number_of_pages) as usize;
num_of_pages = number_of_pages as usize;
};
let mut page_ids = vec![];
let mut puzzle_index = 0;
for _ in 0..num_of_pages {
let mut ops: Vec<Operation> = vec![];
let mut pos_x = 750;
let mut pos_y = 75;
for i in 0..6 {
if puzzle_index == puzzles.len() {
break;
};
ops.append(&mut gen_diagram_operations(puzzle_index + 1, &puzzles[puzzle_index], pos_x, pos_y, lang));
if i % 2 == 0 {
pos_y = 325;
} else {
pos_y = 75;
pos_x -= 250;
};
puzzle_index += 1;
}
let content = Content { operations: ops };
let content_id = doc.add_object(Stream::new(dictionary! {}, content.encode().unwrap()));
page_ids.push(
doc.add_object(dictionary! {
"Type" => "Page",
"Parent" => pages_id,
"Contents" => content_id,
})
.into(),
);
}
let mut ops: Vec<Operation> = vec![];
let mut pos_x = 800;
let pos_y = 75;
for (puzzle_number, puzzle) in puzzles.iter().enumerate().take(num_of_puzzles_to_print) {
let mut board = Board::from_str(&puzzle.fen).unwrap();
let mut puzzle_moves: VecDeque<&str> = puzzles[puzzle_number].moves.split_whitespace().collect();
let movement = ChessMove::new(
Square::from_str(&String::from(&puzzle_moves[0][..2])).unwrap(),
Square::from_str(&String::from(&puzzle_moves[0][2..4])).unwrap(),
PuzzleTab::check_promotion(puzzle_moves[0]),
);
board = board.make_move_new(movement);
let mut solution = (puzzle_number + 1).to_string() + ") ";
puzzle_moves.pop_front();
let mut half_move_number = 1;
let mut move_label = 1;
if board.side_to_move() == Color::Black {
solution.push_str(" 1. ... ");
half_move_number = 2;
move_label = 2;
}
for chess_move in puzzle_moves {
if half_move_number % 2 == 0 {
solution.push(' ');
solution.push_str(&config::coord_to_san(&board, String::from(chess_move), lang).unwrap());
} else {
solution.push(' ');
solution.push_str(&move_label.to_string());
solution.push_str(". ");
solution.push_str(&config::coord_to_san(&board, String::from(chess_move), lang).unwrap());
move_label += 1;
}
half_move_number += 1;
let movement = ChessMove::new(
Square::from_str(&String::from(&chess_move[..2])).unwrap(),
Square::from_str(&String::from(&chess_move[2..4])).unwrap(),
PuzzleTab::check_promotion(chess_move),
);
board = board.make_move_new(movement);
}
ops.append(&mut vec![
Operation::new("BT", vec![]),
Operation::new("Tf", vec!["Regular".into(), 12.into()]),
Operation::new("rg", vec![0.into(), 0.into(), 0.into()]),
Operation::new("Td", vec![pos_y.into(), pos_x.into()]),
Operation::new("Tj", vec![Object::string_literal(solution)]),
Operation::new("ET", vec![]),
]);
pos_x -= 18;
if pos_x < 18 {
pos_x = 800;
let content = Content { operations: ops };
let content_id = doc.add_object(Stream::new(dictionary! {}, content.encode().unwrap()));
page_ids.push(
doc.add_object(dictionary! {
"Type" => "Page",
"Parent" => pages_id,
"Contents" => content_id,
})
.into(),
);
ops = vec![];
}
}
let content = Content { operations: ops };
let content_id = doc.add_object(Stream::new(dictionary! {}, content.encode().unwrap()));
page_ids.push(
doc.add_object(dictionary! {
"Type" => "Page",
"Parent" => pages_id,
"Contents" => content_id,
})
.into(),
);
let pages = dictionary! {
"Type" => "Pages",
"Count" => Object::Integer(page_ids.len() as i64),
"Kids" => page_ids,
"Resources" => resources_id,
"MediaBox" => vec![0.into(), 0.into(), 600.into(), 850.into()],
};
doc.objects.insert(pages_id, Object::Dictionary(pages));
let catalog_id = doc.add_object(dictionary! {
"Type" => "Catalog",
"Pages" => pages_id,
});
doc.trailer.set("Root", catalog_id);
doc.compress();
let _ = doc.save(path);
}
fn gen_diagram_operations(index: usize, puzzle: &config::Puzzle, start_x: i32, start_y: i32, lang: &lang::Language) -> Vec<Operation> {
let mut board = Board::from_str(&puzzle.fen).unwrap();
let puzzle_moves: Vec<&str> = puzzle.moves.split_whitespace().collect();
let movement = ChessMove::new(
Square::from_str(&String::from(&puzzle_moves[0][..2])).unwrap(),
Square::from_str(&String::from(&puzzle_moves[0][2..4])).unwrap(),
PuzzleTab::check_promotion(puzzle_moves[0]),
);
let (is_white, last_move) = if board.side_to_move() == Color::White {
(
false,
index.to_string() + &lang::tr(lang, "pdf_black_to_move") + &config::coord_to_san(&board, String::from(&puzzle_moves[0][0..4]), lang).unwrap(),
)
} else {
(
true,
index.to_string() + &lang::tr(lang, "pdf_white_to_move") + &config::coord_to_san(&board, String::from(&puzzle_moves[0][0..4]), lang).unwrap(),
)
};
board = board.make_move_new(movement);
let mut ops = vec![
Operation::new("BT", vec![]),
Operation::new("Tf", vec!["Regular".into(), 10.into()]),
Operation::new("rg", vec![0.into(), 0.into(), 0.into()]),
Operation::new("Td", vec![start_y.into(), (start_x + 30).into()]),
Operation::new("Tj", vec![Object::string_literal(last_move)]),
Operation::new("ET", vec![]),
Operation::new("BT", vec![]),
Operation::new("Tf", vec!["Chess Alpha".into(), 25.into()]),
Operation::new("rg", vec![0.into(), 0.into(), 0.into()]),
Operation::new("Td", vec![start_y.into(), start_x.into()]),
];
let ranks;
let files;
if is_white {
ranks = (0..8).rev().collect::<Vec<i32>>();
files = (0..8).collect::<Vec<i32>>();
} else {
ranks = (0..8).collect::<Vec<i32>>();
files = (0..8).rev().collect::<Vec<i32>>();
};
for rank in ranks {
let mut rank_string = String::new();
for file in &files {
let mut new_piece;
let light_square = (rank + file) % 2 != 0;
let square = chess::Square::make_square(chess::Rank::from_index(rank as usize), chess::File::from_index(*file as usize));
let (piece, color) = (board.piece_on(square), board.color_on(square));
if let Some(piece) = piece {
if color.unwrap() == Color::White {
match piece {
Piece::Pawn => new_piece = 'P',
Piece::Rook => new_piece = 'R',
Piece::Knight => new_piece = 'H',
Piece::Bishop => new_piece = 'B',
Piece::Queen => new_piece = 'Q',
Piece::King => new_piece = 'K',
}
if light_square {
new_piece = new_piece.to_lowercase().collect::<Vec<_>>()[0];
}
} else {
match piece {
Piece::Rook => new_piece = 'T',
Piece::Knight => new_piece = 'J',
Piece::Bishop => new_piece = 'N',
Piece::Queen => new_piece = 'W',
Piece::King => new_piece = 'L',
Piece::Pawn => new_piece = 'O',
}
if light_square {
new_piece = new_piece.to_lowercase().collect::<Vec<_>>()[0];
}
}
} else if light_square {
new_piece = ' ';
} else {
new_piece = '+';
}
rank_string.push(new_piece);
}
ops.push(Operation::new("Tj", vec![Object::string_literal(rank_string)]));
ops.push(Operation::new("Td", vec![0.into(), Object::Integer(-25)]));
}
ops.push(Operation::new("ET", vec![]));
ops
}
pub fn to_pgn(puzzles: &[config::Puzzle], lang: &lang::Language, path: String) {
let mut pgn_content = String::new();
for puzzle in puzzles.iter() {
let mut board = Board::from_str(&puzzle.fen).unwrap();
pgn_content.push_str(&format!("[Event \"Chess Puzzle\"]\n"));
pgn_content.push_str(&format!("[Site \"https://lichess.org/training/{}\"]\n", puzzle.puzzle_id));
pgn_content.push_str(&format!("[Date \"{}\"]\n", chrono::Local::now().format("%Y-%m-%d")));
pgn_content.push_str(&format!("[White \"{}\"]\n", if board.side_to_move() == Color::White { "Player" } else { "Opponent" }));
pgn_content.push_str(&format!("[Black \"{}\"]\n", if board.side_to_move() == Color::Black { "Player" } else { "Opponent" }));
pgn_content.push_str(&format!("[Result \"*\"]\n"));
pgn_content.push_str(&format!("[GameID \"{}\"]\n", puzzle.game_url));
pgn_content.push_str(&format!("[FEN \"{}\"]\n", puzzle.fen));
pgn_content.push_str(&format!("[SetUp \"1\"]\n"));
if !puzzle.opening.is_empty() {
pgn_content.push_str(&format!("[Opening \"{}\"]\n", puzzle.opening));
}
pgn_content.push_str(&format!("[PuzzleRating \"{}\"]\n", puzzle.rating));
pgn_content.push_str(&format!("[PuzzleRatingDeviation \"{}\"]\n", puzzle.rating_deviation));
pgn_content.push_str(&format!("[PuzzlePopularity \"{}\"]\n", puzzle.popularity));
pgn_content.push_str(&format!("[PuzzleNbPlays \"{}\"]\n", puzzle.nb_plays));
pgn_content.push_str(&format!("[PuzzleThemes \"{}\"]\n", puzzle.themes));
let puzzle_moves: Vec<&str> = puzzle.moves.split_whitespace().collect();
let mut move_number = 1;
let mut is_white_to_move = board.side_to_move() == Color::White;
let first_move = puzzle_moves[0];
let movement = ChessMove::new(
Square::from_str(&String::from(&first_move[..2])).unwrap(),
Square::from_str(&String::from(&first_move[2..4])).unwrap(),
PuzzleTab::check_promotion(first_move),
);
let san_move = config::coord_to_san(&board, String::from(first_move), lang).unwrap();
if is_white_to_move {
pgn_content.push_str(&format!("{}. {}", move_number, san_move));
} else {
pgn_content.push_str(&format!("{}... {}", move_number, san_move));
move_number += 1;
}
board = board.make_move_new(movement);
is_white_to_move = !is_white_to_move;
for chess_move in puzzle_moves.iter().skip(1) {
if is_white_to_move {
pgn_content.push_str(&format!(" {}. ", move_number));
} else {
pgn_content.push_str(" ");
}
let san_move = config::coord_to_san(&board, String::from(*chess_move), lang).unwrap();
pgn_content.push_str(&san_move);
let movement = ChessMove::new(
Square::from_str(&String::from(&chess_move[..2])).unwrap(),
Square::from_str(&String::from(&chess_move[2..4])).unwrap(),
PuzzleTab::check_promotion(chess_move),
);
board = board.make_move_new(movement);
if !is_white_to_move {
move_number += 1;
}
is_white_to_move = !is_white_to_move;
}
pgn_content.push_str(" *\n\n");
}
std::fs::write(path, pgn_content).expect("Unable to write PGN file");
}