zenozeng / svgcanvas

Draw on SVG using Canvas's 2D Context API
MIT License
65 stars 19 forks source link

Create svg without document #23

Open jassler opened 2 years ago

jassler commented 2 years ago

The chess-image-generator package uses node-canvas to create an image of a chess position. Unfortunately I'm a little unhappy with the results, as the chess pieces, which are stored as pngs, become a little blurry on the canvas (see for example here: https://bucket-lichess.vercel.app/api/board?fen=8/6p1/2prppp1/3k4/5PP1/2R5/PP5P/6K1&squares=d8,d6).

Is it possible to use the svgcanvas library to create an svg and convert it to a png image (all on the server)?

I tried to do the following, however it resulted in a document is not defined error which I am not sure how to solve.

const ChessImageGenerator = require('chess-image-generator')
import { createCanvas } from 'canvas'
import { Context } from 'svgcanvas'
const sharp = require('sharp')

// ig <=> ChessImageGenerator object
async function generateBuffer(ig) {
  if (!ig.ready) {
    throw new Error("Load a position first");
  }

  const canvas = createCanvas(ig.size, ig.size)
  const context2D = canvas.getContext('2d')

  // ERROR: document not defined
  const ctx = new Context({
    width: ig.size,
    height: ig.size,
    ctx: context2D,
    document: undefined,
  })
  // create chess pattern and add chess pieces
  for (let i = 0; i < 8; i += 1) {
    for (let j = 0; j < 8; j += 1) {
      const coords = cols[col(j)] + row(i);

      if ((i + j) % 2 === 0) {
        ctx.beginPath();
        ctx.rect(
          ((ig.size / 8) * (7 - j + 1) - ig.size / 8),
          ((ig.size / 8) * i) + ig.padding[0],
          ig.size / 8,
          ig.size / 8
        );
        ctx.fillStyle = ig.dark;
        ctx.fill();
      }
      // ...
      // add chess pieces that are stored as svg files
      // ...
    }
  }

  return sharp(ctx.getSerializedSvg()).png().toBuffer()
}
cmdcolin commented 2 years ago

(i just watch this repo, not a dev, and currently using canvas2svg) i've done something like this under canvas2svg

import { JSDOM } from 'jsdom'
const { document } = new JSDOM(`...`).window
global.document = document

I copied it from https://github.com/jsdom/jsdom mindlessly and even left the ... in the production code lol...still works. you may also be able to pass the document object directly to the constructor. in my code, I also have a convert to png but i use a command line tool to do it (rsvg-convert), i didn't evaluate other options to do it though, so there might be others

jassler commented 2 years ago

Thank you for your suggestion, this was immensely helpful! Also apologies for taking so much time to respond - I shouldn't have opened an issue right before going on vacation.

Unfortunately now I get a DOMMatrix is not defined error message at the same place. I've had a look at the DOMMatrix npm package, but it doesn't quite look like it's solving the problem I have. Or did I get something wrong?

For my next.js application, I've added the following code to pages/api/board.js and called localhost:3000/api/board.

import { createCanvas } from 'canvas'
import { Context } from 'svgcanvas'
import { JSDOM } from 'jsdom'
const sharp = require('sharp')
const { document } = new JSDOM(`...`).window
global.document = document

export default function handler(req, res) {
  const canvas = createCanvas(80, 80)
  const context2D = canvas.getContext('2d')

  // code fails here. constructor needs DOMMatrix
  const ctx = new Context({
    width: 80,
    height: 80,
    ctx: context2D
  })
  for (let i = 0; i < 80; i += 10) {
    for (let j = 0; j < 80; j += 10) {
      ctx.beginPath()
      ctx.rect(i, j, 10, 10)
      ctx.fillStyle = ((i + j) % 10 === 0) ? '#FFFFFF' : '#000000'
      ctx.fill()
    }
  }

  return sharp(ctx.getSerializedSvg()).png().toBuffer()
}

Would you be able to give a complete example?