Creating Chess Board SVGs, PNGs, and GIFs
Tags: webdev, javascript, svg, chess
Published: 2023-11-10 18:16:45

The most flexible way to turn a string into an image is through a URL that knows what image to return. Hoping that a friction-less way to serve chess images would be viable, I decided to see how quickly a cheap server could convert a FEN string of any chess position into a lightweight image.

## Chess Notation Basics

The standard notation for storing position information is Forsyth-Edwards Notation. A FEN string looks like rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR followed by a space and some other information that is not relevant to creating a board image.

![The opening position via https://rnbqkbnr.com/rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y9au7o6fr4gcj48iftdb.png)
<figcaption>The opening position via https://rnbqkbnr.com/rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR.png</figcaption>

- Lowercase letters are black pieces and uppercase letters are white pieces.
- Letters represent the expected pieces except for n which is the knight.
- Numbers represent the number of consecutive empty spaces.
- The / separates rows.

Parsing these strings is relatively simple, with just a bit of work to handle the numbers. Keep track of the current column by moving to the next column after each piece and moving the given number of columns when hitting an integer. Move to the next row when the column is greater than 7 until all 8 rows are complete.

```javascript
let row = 0, col = 0;
for (var i=0; i<fen.length; i++) {
  switch (fen[i]) {
    case 'P': case 'N': case 'B':
    case 'R': case 'Q': case 'K':
    case 'p': case 'n': case 'b':
    case 'r': case 'q': case 'k': {
      //the piece is fen[i], row is row, col is col
      //turn that info into a graphic
      col++;
      if (col > 7) {
        col = 0;
        row++;
      }
      break;
    }
    case '1': case '2': case '3': case '4':
    case '5': case '6': case '7': case '8': {
      //move the given number of cols
      col += parseInt(fen[i]);
      if (col > 7) {
        col = 0;
        row++;
      }
    }
    //skip over other chars like /
  }
  if (row > 7) { break;}
}
```

## Image Types

There are several options for the type of image to generate. By default, static images will be SVG since it is lightweight, very simple to create, and nicely scales to any size.

Many social media apps do not display SVG so a PNG option is important for compatibility. Finally, I want to create GIFs so that it is possible to animate a few moves.

## SVG Creation

Creating SVGs only requires manipulating strings, so any back-end setup works easily. All we need is a blank board and paths for each piece. Then every time we hit a piece as we loop through the FEN string just add the appropriate paths and translate to the correct row and column.

```javascript
case 'r': case 'n': case 'b': case 'q': case 'k': case 'p': {
    svg += `<g transform="translate(${col*45} ${row*45})">${pieces[fen[i]]}</g>`;
    //add code here to update row, col as above
    break;
}
case 'R': case 'N': case 'B': case 'Q': case 'K': case 'P': {
    svg += `<g transform="translate(${col*45} ${row*45})" class="w">${this.pieces[this.fen[i].toLowerCase()]}</g>`;
    //each square is 45 units wide/tall
    break;
}
```

Any collection of SVG pieces (I assume raster images would work if necessary) can work by wrapping each piece within a group that gets transformed to fit into the desired square.

A blank board can be as simple as using a checkerboard pattern for the background, but you can also add a border with grid labels or anything else. As long as you know the size of each square, it is trivial to add the pieces on top in the right locations.

```html
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"  viewBox="-9 -9 378 378">
  <defs>
    <pattern id="bg" x="0" y="0" width="90" height="90" patternUnits="userSpaceOnUse">
      <rect fill="rgb(190,190,190)" x="0" y="0" width="90" height="90"/>
      <rect fill="rgb(150,100,75)" x="45" y="0" width="45" height="45"/>
      <rect fill="rgb(150,100,75)" x="0" y="45" width="45" height="45"/>
    </pattern>
  </defs>
  <rect x="-9" y="-9" width="100%" height="100%" fill="black"/>
  <rect x="0" y="0" width="360" height="360" fill="url(#bg)" fill-opacity="0.75"></rect>
<!--Add pieces here-->
</svg>
```

There are no dependencies so this algorithm can run client-side or server-side, and the image takes less than a millisecond to generate while compressing down to about 2 KB. For the production server, I ended up using [OpenResty](https://openresty.org/en/) (an extension of Nginx) with Lua but the process is the same.

## PNG Creation

There are many ways to convert an SVG to a PNG either with a browser's built-in renderer or a tool like rsvg-convert. The fastest approach, though, is to pre-render the pixel arrays for each piece and then splice them together at runtime.

For simplicity, I will be generating PNGs with JavaScript/Node and the [Sharp image library](https://www.npmjs.com/package/sharp). Any library that can convert between pixel arrays and image files will make the process quite straightforward.

![All 24 possible occupied squares](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cf01wl4oxg99ukwzugj3.jpg)
<figcaption>All 24 possible occupied squares</figcaption>

There are 6 pieces in each color, and then each needs to be on dark and light backgrounds for a total of 24 possibilities.

I have created 80px by 80px images of each piece to fit into 90px squares with a bit of padding. I split each image into 80 Uint8Arrays, one for each row, and create a map to these arrays that I can call when needed.

```javascript
//each square is 90px and I only want to replace the center 80x80 so offset by 5
//the board is 720px wide and each pixel uses 3 cells (R,G,B)
var minRow = 5 + 90 * row;
var minCol = 5 + 90 * col;
for (var rr = 0; rr < 80; rr++) {
  //arr is a Uint8Array that starts as a blank board.
  //inputPNGs finds the correct row for the correct piece
  //then insert that array at the correct starting index with arr.set()
  arr.set(inputPNGs[p][rr], (rr + minRow) * 720 * 3 + minCol * 3);
}
```

When the server launches, I use Sharp to create arrays for the board and each piece. Then each time I need a new board image, Sharp generates a PNG from the generated Uint8Array. It is also possible to create JPGs, WEBPs, or any other common format as well as changing the size and other settings.

The whole process only takes a few milliseconds, but for maximum speed and minimum file size I ended up using a more complex process with Rust/Actix for the production server.

## Animated GIFs

For GIFs, I do require users to generate them in a browser and then upload them to a GIF server. Compared to the static images, I don't think there is a performant method of creating GIFs on demand server-side.

With SVG paths, it is possible to create a really nice transition between positions. Create a path that starts with the initial piece, moves along the board to the next position, and then finishes with the piece in the new location. By animating how much of this path is visible at any time a nice smooth movement is generated.

![A gif of the fool's mate](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ezsasjrvutp3l755ywjv.gif)
<figcaption>A gif of the fool's mate</figcaption>

It takes a bit of computation to get the timing right for a smooth animation, but after that work the [gif.js](https://jnordberg.github.io/gif.js/) and [gifsicle](https://github.com/renzhezhilu/gifsicle-wasm-browser) libraries can output a fairly lightweight GIF for downloading and sharing.

The first step is to take a bunch of snapshots at regular intervals. We need to generate the appropriate SVG for each frame, render it to a canvas, and then convert the canvas to a PNG.

Once we have an array of images, follow the documentation for gif.js to create the GIF. These GIFs are a bit large, but gifsicle will significantly reduce the size. By using the “-O3” compression and only 32 colors (plenty for our chess images) the size is reduced by at least 80%.

## Web Component

Although it requires a bit of JavaScript, another simple way to add a board image to a web page is with a custom web component. Since I already had a function to generate the SVG, I basically just needed to add some [web component boilerplate](https://developer.mozilla.org/en-US/docs/Web/API/Web_Components).

The library zips to about 3 KB and makes it possible to generate or update multiple board images without additional requests. The other advantage is the option to style the border, change the colors of the squares, and even add some arrows.

```html
<chess-board fen="rnbqkbnr/8/8/8/8/8/8/RNBQKBNR" arrows="a1a5 b1c3,g8f6" dark="blue" light="gray" border="12 black white"></chess-board>
<chess-board fen="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" light="orange" border="8 black black"></chess-board>
<script src="https://rnbqkbnr.com/chess-board.js"></script>
```

![The boards produced by the HTML above](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gi59di9d6fhecwqzbs3t.jpg)
<figcaption>The boards produced by the HTML above.</figcaption>

## Code Repositories

If you want custom board images or to see the full code, you can view my [chess code portfolio](https://triplelog.com/code#tag-rnbqkbnr) and follow the links to fork on Replit or view all of [my repls](https://replit.com/@triplelog).

There are separate repls to create the [SVGs](https://replit.com/@triplelog/Chess-Board-Images-SVG), [PNGs](https://replit.com/@triplelog/Chess-Board-Image-API) (and [render piece/board images](https://replit.com/@triplelog/Chess-Piece-PNGs) from SVG), [GIFs](https://replit.com/@triplelog/Chess-GIFs), and [web components](https://replit.com/@triplelog/Chess-Board-Web-Component).

If you want an image of a chess position, simply paste or type a FEN string (with or without the info about castling, etc.) after the domain [https://rnbqkbnr.com](https://rnbqkbnr.com) anywhere URLs are accepted: browsers, many apps and websites, or within an HTML img element. If you need a PNG, then just add .png to the end. 

Since I'm not really active in the chess community, I am open to any suggestions from people who know what features would be most helpful.