# This file has been copied and slightly modified from python-chess library, # Copyright (C) 2016-2020 Niklas Fiekas . # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # Piece vector graphics are copyright (C) Colin M.L. Burnett # and also licensed under the # GNU General Public License. import chess import math import xml.etree.ElementTree as ET from typing import Iterable, Optional, Tuple, Union SQUARE_SIZE = 45 MARGIN = 20 PIECES = { "b": """""", # noqa: E501 "k": """""", # noqa: E501 "n": """""", # noqa: E501 "p": """""", # noqa: E501 "q": """""", # noqa: E501 "r": """""", # noqa: E501 "B": """""", # noqa: E501 "K": """""", # noqa: E501 "N": """""", # noqa: E501 "P": """""", # noqa: E501 "Q": """""", # noqa: E501 "R": """""", # noqa: E501 } PIECES = { "b": """""", # noqa: E501 "k": """""", # noqa: E501 "n": """""", # noqa: E501 "p": """""", # noqa: E501 "q": """""", # noqa: E501 "r": """""", # noqa: E501 "B": """""", # noqa: E501 "K": """""", # noqa: E501 "N": """""", # noqa: E501 "P": """""", # noqa: E501 "Q": """""", # noqa: E501 "R": """""", # noqa: E501 } XX = """""" # noqa: E501 CHECK_GRADIENT = """""" # noqa: E501 DEFAULT_COLORS = { "square light": "#ffce9e", "square dark": "#d18b47", "square dark lastmove": "#aaa23b", "square light lastmove": "#cdd16a", } class Arrow: """Details of an arrow to be drawn.""" def __init__(self, tail: chess.Square, head: chess.Square, *, color: str = "#888", annotation: str = '') -> None: self.tail = tail self.head = head self.color = color self.annotation = annotation class SvgWrapper(str): def _repr_svg_(self) -> "SvgWrapper": return self def _svg(viewbox: int, size: Optional[int]) -> ET.Element: svg = ET.Element("svg", { "xmlns": "http://www.w3.org/2000/svg", "version": "1.1", "xmlns:xlink": "http://www.w3.org/1999/xlink", "viewBox": f"0 0 {viewbox:d} {viewbox:d}", }) if size is not None: svg.set("width", str(size)) svg.set("height", str(size)) return svg def _text(content: str, x: int, y: int, width: int, height: int) -> ET.Element: t = ET.Element("text", { "x": str(x + width // 2), "y": str(y + height // 2), "font-size": str(max(1, int(min(width, height) * 0.7))), "text-anchor": "middle", "alignment-baseline": "middle", }) t.text = content return t def piece(piece: chess.Piece, size: Optional[int] = None) -> str: """ Renders the given :class:`chess.Piece` as an SVG image. >>> import chess >>> import chess.svg >>> >>> chess.svg.piece(chess.Piece.from_symbol("R")) # doctest: +SKIP .. image:: ../docs/wR.svg """ svg = _svg(SQUARE_SIZE, size) svg.append(ET.fromstring(PIECES[piece.symbol()])) return SvgWrapper(ET.tostring(svg).decode("utf-8")) def board(board: Optional[chess.BaseBoard] = None, *, squares: Optional[chess.IntoSquareSet] = None, flipped: bool = False, coordinates: bool = True, lastmove: Optional[chess.Move] = None, check: Optional[chess.Square] = None, arrows: Iterable[Union[Arrow, Tuple[chess.Square, chess.Square]]] = (), size: Optional[int] = None, style: Optional[str] = None, square_colors: Iterable[str] = (), #TODO: remove as it is not needed anymore only_pieces: bool = False) -> str: """ Renders a board with pieces and/or selected squares as an SVG image. :param board: A :class:`chess.BaseBoard` for a chessboard with pieces or ``None`` (the default) for a chessboard without pieces. :param squares: A :class:`chess.SquareSet` with selected squares. :param flipped: Pass ``True`` to flip the board. :param coordinates: Pass ``False`` to disable coordinates in the margin. :param lastmove: A :class:`chess.Move` to be highlighted. :param check: A square to be marked as check. :param arrows: A list of :class:`~chess.svg.Arrow` objects like ``[chess.svg.Arrow(chess.E2, chess.E4)]`` or a list of tuples like ``[(chess.E2, chess.E4)]``. An arrow from a square pointing to the same square is drawn as a circle, like ``[(chess.E2, chess.E2)]``. :param size: The size of the image in pixels (e.g., ``400`` for a 400 by 400 board) or ``None`` (the default) for no size limit. :param style: A CSS stylesheet to include in the SVG image. >>> import chess >>> import chess.svg >>> >>> board = chess.Board("8/8/8/8/4N3/8/8/8 w - - 0 1") >>> squares = board.attacks(chess.E4) >>> chess.svg.board(board=board, squares=squares) # doctest: +SKIP .. image:: ../docs/Ne4.svg """ margin = MARGIN if coordinates else 0 svg = _svg(8 * SQUARE_SIZE + 2 * margin, size) if style: ET.SubElement(svg, "style").text = style defs = ET.SubElement(svg, "defs") if board: for piece_color in chess.COLORS: for piece_type in chess.PIECE_TYPES: if board.pieces_mask(piece_type, piece_color): defs.append(ET.fromstring(PIECES[chess.Piece(piece_type, piece_color).symbol()])) squares = chess.SquareSet(squares) if squares else chess.SquareSet() if squares: defs.append(ET.fromstring(XX)) if check is not None and not only_pieces: defs.append(ET.fromstring(CHECK_GRADIENT)) for square, bb in enumerate(chess.BB_SQUARES): file_index = chess.square_file(square) rank_index = chess.square_rank(square) x = (file_index if not flipped else 7 - file_index) * SQUARE_SIZE + margin y = (7 - rank_index if not flipped else rank_index) * SQUARE_SIZE + margin cls = ["square", "light" if chess.BB_LIGHT_SQUARES & bb else "dark"] if lastmove and square in [lastmove.from_square, lastmove.to_square]: cls.append("lastmove") if square_colors == (): fill_color = DEFAULT_COLORS[" ".join(cls)] else: fill_color = square_colors[square] cls.append(chess.SQUARE_NAMES[square]) if not only_pieces: ET.SubElement(svg, "rect", { "x": str(x), "y": str(y), "width": str(SQUARE_SIZE), "height": str(SQUARE_SIZE), "class": " ".join(cls), "stroke": "none", "fill": fill_color, }) if square == check: ET.SubElement(svg, "rect", { "x": str(x), "y": str(y), "width": str(SQUARE_SIZE), "height": str(SQUARE_SIZE), "class": "check", "fill": "url(#check_gradient)", }) # Render pieces. if board is not None: piece = board.piece_at(square) if piece: ET.SubElement(svg, "use", { "xlink:href": f"#{chess.COLOR_NAMES[piece.color]}-{chess.PIECE_NAMES[piece.piece_type]}", "transform": f"translate({x:d}, {y:d})", }) # Render selected squares. if squares is not None and square in squares: #ET.SubElement(svg, "use", { # "xlink:href": "#xx", # "x": str(x), # "y": str(y), #}) ET.SubElement(svg, "rect", { "x": str(x), "y": str(y), "width": str(SQUARE_SIZE), "height": str(SQUARE_SIZE), "class": "check", "fill": "none", "stroke": "#FF0000", "stroke-width": "5.0", "rx": "2.5", "opacity": "0.60" }) if coordinates: for file_index, file_name in enumerate(chess.FILE_NAMES): x = (file_index if not flipped else 7 - file_index) * SQUARE_SIZE + margin svg.append(_text(file_name, x, 0, SQUARE_SIZE, margin)) svg.append(_text(file_name, x, margin + 8 * SQUARE_SIZE, SQUARE_SIZE, margin)) for rank_index, rank_name in enumerate(chess.RANK_NAMES): y = (7 - rank_index if not flipped else rank_index) * SQUARE_SIZE + margin svg.append(_text(rank_name, 0, y, margin, SQUARE_SIZE)) svg.append(_text(rank_name, margin + 8 * SQUARE_SIZE, y, margin, SQUARE_SIZE)) for arrow in arrows: try: tail, head, color, annotation = arrow.tail, arrow.head, arrow.color, arrow.annotation # type: ignore except AttributeError: tail, head = arrow # type: ignore color = "#888" annotation = '' tail_file = chess.square_file(tail) tail_rank = chess.square_rank(tail) head_file = chess.square_file(head) head_rank = chess.square_rank(head) xtail = margin + (tail_file + 0.5 if not flipped else 7.5 - tail_file) * SQUARE_SIZE ytail = margin + (7.5 - tail_rank if not flipped else tail_rank + 0.5) * SQUARE_SIZE xhead = margin + (head_file + 0.5 if not flipped else 7.5 - head_file) * SQUARE_SIZE yhead = margin + (7.5 - head_rank if not flipped else head_rank + 0.5) * SQUARE_SIZE if (head_file, head_rank) == (tail_file, tail_rank): ET.SubElement(svg, "circle", { "cx": str(xhead), "cy": str(yhead), "r": str(SQUARE_SIZE * 0.9 / 2), "stroke-width": str(SQUARE_SIZE * 0.1), "stroke": color, "fill": "none", "opacity": "0.5", "class": "circle", }) else: # marker_size = 0.75 * SQUARE_SIZE # marker_margin = 0.1 * SQUARE_SIZE marker_size = 0.5 * SQUARE_SIZE marker_margin = 0.05 * SQUARE_SIZE dx, dy = xhead - xtail, yhead - ytail hypot = math.hypot(dx, dy) shaft_x = xhead - dx * (marker_size + marker_margin) / hypot shaft_y = yhead - dy * (marker_size + marker_margin) / hypot xtip = xhead - dx * marker_margin / hypot ytip = yhead - dy * marker_margin / hypot x_annot = xtail + (shaft_x - xtail) / 2 y_annot = ytail + (shaft_y - ytail) / 2 x_annot = xhead - dx * 0.74 * SQUARE_SIZE / hypot # - (xtip - xtail)*(SQUARE_SIZE/2) y_annot = yhead - dy * 0.74 * SQUARE_SIZE / hypot # - (ytip - ytail)*(SQUARE_SIZE/2) ET.SubElement(svg, "line", { "x1": str(xtail), "y1": str(ytail), "x2": str(shaft_x), "y2": str(shaft_y), "stroke": color, "stroke-width": str(SQUARE_SIZE * 0.15), "opacity": "0.5", "stroke-linecap": "butt", "class": "arrow", }) marker = [(xtip, ytip), (shaft_x + dy * 0.5 * marker_size / hypot, shaft_y - dx * 0.5 * marker_size / hypot), (shaft_x - dy * 0.5 * marker_size / hypot, shaft_y + dx * 0.5 * marker_size / hypot)] ET.SubElement(svg, "polygon", { "points": " ".join(str(x) + "," + str(y) for x, y in marker), "fill": color, "opacity": "0.5", "class": "arrow", }) for arrow in arrows: try: tail, head, color, annotation = arrow.tail, arrow.head, arrow.color, arrow.annotation # type: ignore except AttributeError: tail, head = arrow # type: ignore color = "#888" annotation = '' tail_file = chess.square_file(tail) tail_rank = chess.square_rank(tail) head_file = chess.square_file(head) head_rank = chess.square_rank(head) xtail = margin + (tail_file + 0.5 if not flipped else 7.5 - tail_file) * SQUARE_SIZE ytail = margin + (7.5 - tail_rank if not flipped else tail_rank + 0.5) * SQUARE_SIZE xhead = margin + (head_file + 0.5 if not flipped else 7.5 - head_file) * SQUARE_SIZE yhead = margin + (7.5 - head_rank if not flipped else head_rank + 0.5) * SQUARE_SIZE marker_size = 0.5 * SQUARE_SIZE marker_margin = 0.05 * SQUARE_SIZE dx, dy = xhead - xtail, yhead - ytail hypot = math.hypot(dx, dy) shaft_x = xhead - dx * (marker_size + marker_margin) / hypot shaft_y = yhead - dy * (marker_size + marker_margin) / hypot xtip = xhead - dx * marker_margin / hypot ytip = yhead - dy * marker_margin / hypot x_annot = xhead - dx * 0.74 * SQUARE_SIZE / hypot y_annot = yhead - dy * 0.74 * SQUARE_SIZE / hypot if annotation != '': ET.SubElement(svg, "circle", { "cx": str(x_annot), "cy": str(y_annot), "r": str(SQUARE_SIZE * 0.175), #"r": str(SQUARE_SIZE * 0.2), "stroke-width": str(SQUARE_SIZE * 0.01), "stroke": '#000000', "fill": color, "opacity": "1.0", "class": "circle", }) #style = get_style("'BundledDejavuSans'", str(SQUARE_SIZE * 0.1)) annot = ET.SubElement(svg, "text", { "x": str(x_annot), "y": str(y_annot), "font-size": str(SQUARE_SIZE * 0.2), # max(1, int(min(SQUARE_SIZE, SQUARE_SIZE) * 0.3))), "text-anchor": "middle", "dominant-baseline": "middle" #"alignment-baseline": "middle" }) annot.text = annotation return SvgWrapper(ET.tostring(svg).decode("utf-8"))