Fixing the potato

In my last post about screenshotting a node with JavaScript I got as far as a fairly low quality image. Since then I’ve figured out how to fix it.

The first part was when I discovered the toDataURL() method can take a second paramater. I already had the type in, but the second one is called encoderOptions and is a number between 0 and 1 that represents the quality of the final image. Setting that to 1 helped a little, but mainly doubled weight of the resulting image.

On top of that, and there’s not much difference between the two but I found JPEG to be marginally better than the original PNG.

So now instead of newImage.src = canvas.toDataURL('image/png'); it’s newImage.src = canvas.toDataURL('image/jpeg', 1.0);.

The real key to the whole thing was in 3 parts:

  1. Make the canvas element twice the width and twice the height of the node in question.
  2. Make the SVG the same size as the node.
  3. Draw the SVG onto the canvas at the size of the canvas.

The new code is:

const card = document.getElementById('card');
const css = document.querySelector('style');
const screenshot = document.getElementById('screenshot');

// This function takes any DOMDocumentElement as a first parameter.
// And return a Data URL of its generated image.
async function renderElement(element) {
  // Get the dimensions of the element
  const dimensions = element.getBoundingClientRect();

  // The canvas does not need to exist in document body
  const canvas = document.createElement('canvas');

  // The canvas is adjusted here
  canvas.width = dimensions.width * 2;
  canvas.height = dimensions.height * 2;

  const ctx = canvas.getContext('2d');

  // SVG scaffolding with CSS
  const imageSVG = `
    <svg xmlns="http://www.w3.org/2000/svg" width="${canvas.width / 2}" height="${canvas.height / 2}">
      <style>* { font-family: sans-serif } span.weight { color: #fd0d96 }</style> ${css.outerHTML}
      <foreignObject width="100%" height="100%">
        <div xmlns="http://www.w3.org/1999/xhtml">
          ${element.outerHTML}
        </div>
      </foreignObject>
    </svg>`;

  // The image also does not need to exist in document body
  const imageElement = document.createElement('img');
  const newImage = document.createElement('img');

  // The svg image source is already converted to Data URL (Base64)
  imageElement.src = `data:image/svg+xml;base64,${btoa(imageSVG)}`;

  imageElement.addEventListener('load', () => {
    ctx.drawImage(imageElement, 0, 0, canvas.width, canvas.height);

    newImage.src = canvas.toDataURL('image/jpeg', 1.0);

    newImage.addEventListener('load', () => {
      newImage.width  = newImage.naturalWidth;
      newImage.height = newImage.naturalHeight;

      // Update the screenshot link
      screenshot.href = newImage.src;
    });
  });
}

renderElement(card);

The results

Comparing the images the difference is noticeable.

Screenshot of an HTML node showing a boxing event, highlighting it's low quality Screenshot of na HTML node showing a boxing event, highlighting it's better quality

One final note, the updated code has fixed an issue where there was some space left over at the bottom, and the screenshot is the exact size of the original node.

Tags