752
votes

Is it possible to capture or print what's displayed in an html canvas as an image or pdf?

I'd like to generate an image via canvas, and be able to generate a png from that image.

14
Example here freakyjolly.com/…Code Spy
You might find canvas2image library useful for this: github.com/hongru/canvas2imageeaorak

14 Answers

779
votes

Oops. Original answer was specific to a similar question. This has been revised:

var canvas = document.getElementById("mycanvas");
var img    = canvas.toDataURL("image/png");

with the value in IMG you can write it out as a new Image like so:

document.write('<img src="'+img+'"/>');
117
votes

HTML5 provides Canvas.toDataURL(mimetype) which is implemented in Opera, Firefox, and Safari 4 beta. There are a number of security restrictions, however (mostly to do with drawing content from another origin onto the canvas).

So you don't need an additional library.

e.g.

 <canvas id=canvas width=200 height=200></canvas>
 <script>
      window.onload = function() {
          var canvas = document.getElementById("canvas");
          var context = canvas.getContext("2d");
          context.fillStyle = "green";
          context.fillRect(50, 50, 100, 100);
          // no argument defaults to image/png; image/jpeg, etc also work on some
          // implementations -- image/png is the only one that must be supported per spec.
          window.location = canvas.toDataURL("image/png");
      }
 </script>

Theoretically this should create and then navigate to an image with a green square in the middle of it, but I haven't tested.

47
votes

I thought I'd extend the scope of this question a bit, with some useful tidbits on the matter.

In order to get the canvas as an image, you should do the following:

var canvas = document.getElementById("mycanvas");
var image = canvas.toDataURL("image/png");

You can use this to write the image to the page:

document.write('<img src="'+image+'"/>');

Where "image/png" is a mime type (png is the only one that must be supported). If you would like an array of the supported types you can do something along the lines of this:

var imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/tiff']; //Extend as necessary 
var acceptedMimes = new Array();
for(i = 0; i < imageMimes.length; i++) {
    if(canvas.toDataURL(imageMimes[i]).search(imageMimes[i])>=0) {
        acceptedMimes[acceptedMimes.length] = imageMimes[i];
    }
}

You only need to run this once per page - it should never change through a page's lifecycle.

If you wish to make the user download the file as it is saved you can do the following:

var canvas = document.getElementById("mycanvas");
var image = canvas.toDataURL("image/png").replace("image/png", "image/octet-stream"); //Convert image to 'octet-stream' (Just a download, really)
window.location.href = image;

If you're using that with different mime types, be sure to change both instances of image/png, but not the image/octet-stream. It is also worth mentioning that if you use any cross-domain resources in rendering your canvas, you will encounter a security error when you try to use the toDataUrl method.

40
votes
function exportCanvasAsPNG(id, fileName) {

    var canvasElement = document.getElementById(id);

    var MIME_TYPE = "image/png";

    var imgURL = canvasElement.toDataURL(MIME_TYPE);

    var dlLink = document.createElement('a');
    dlLink.download = fileName;
    dlLink.href = imgURL;
    dlLink.dataset.downloadurl = [MIME_TYPE, dlLink.download, dlLink.href].join(':');

    document.body.appendChild(dlLink);
    dlLink.click();
    document.body.removeChild(dlLink);
}
21
votes

I would use "wkhtmltopdf". It just work great. It uses webkit engine (used in Chrome, Safari, etc.), and it is very easy to use:

wkhtmltopdf stackoverflow.com/questions/923885/ this_question.pdf

That's it!

Try it

15
votes

Here is some help if you do the download through a server (this way you can name/convert/post-process/etc your file):

-Post data using toDataURL

-Set the headers

$filename = "test.jpg"; //or png
header('Content-Description: File Transfer');
if($msie = !strstr($_SERVER["HTTP_USER_AGENT"],"MSIE")==false)      
  header("Content-type: application/force-download");else       
  header("Content-type: application/octet-stream"); 
header("Content-Disposition: attachment; filename=\"$filename\"");   
header("Content-Transfer-Encoding: binary"); 
header("Expires: 0"); header("Cache-Control: must-revalidate"); 
header("Pragma: public");

-create image

$data = $_POST['data'];
$img = imagecreatefromstring(base64_decode(substr($data,strpos($data,',')+1)));

-export image as JPEG

$width = imagesx($img);
$height = imagesy($img);
$output = imagecreatetruecolor($width, $height);
$white = imagecolorallocate($output,  255, 255, 255);
imagefilledrectangle($output, 0, 0, $width, $height, $white);
imagecopy($output, $img, 0, 0, 0, 0, $width, $height);
imagejpeg($output);
exit();

-or as transparent PNG

imagesavealpha($img, true);
imagepng($img);
die($img);
9
votes

Another interesting solution is PhantomJS. It's a headless WebKit scriptable with JavaScript or CoffeeScript.

One of the use case is screen capture : you can programmatically capture web contents, including SVG and Canvas and/or Create web site screenshots with thumbnail preview.

The best entry point is the screen capture wiki page.

Here is a good example for polar clock (from RaphaelJS):

>phantomjs rasterize.js http://raphaeljs.com/polar-clock.html clock.png

Do you want to render a page to a PDF ?

> phantomjs rasterize.js 'http://en.wikipedia.org/w/index.php?title=Jakarta&printable=yes' jakarta.pdf
5
votes

This is the other way, without strings although I don't really know if it's faster or not. Instead of toDataURL (as all questions here propose). In my case want to prevent dataUrl/base64 since I need a Array buffer or view. So the other method in HTMLCanvasElement is toBlob. (TypeScript function):

    export function canvasToArrayBuffer(canvas: HTMLCanvasElement, mime: string): Promise<ArrayBuffer> {
  return new Promise((resolve, reject) => canvas.toBlob(async (d) => {
    if (d) {
      const r = new FileReader();
      r.addEventListener('loadend', e => {
        const ab = r.result;
        if (ab) {
          resolve(ab as ArrayBuffer);
        }
        else {
           reject(new Error('Expected FileReader result'));
        }
      }); r.addEventListener('error', e => {
        reject(e)
      });
      r.readAsArrayBuffer(d);
    }
    else {
      reject(new Error('Expected toBlob() to be defined'));
    }
  }, mime));
}

Another advantage of blobs is you can create ObjectUrls to represent data as files, similar to HTMLInputFile's 'files' member. More info:

https://developer.mozilla.org/en/docs/Web/API/HTMLCanvasElement/toBlob

3
votes

If you are using jQuery, which quite a lot of people do, then you would implement the accepted answer like so:

var canvas = $("#mycanvas")[0];
var img = canvas.toDataURL("image/png");

$("#elememt-to-write-to").html('<img src="'+img+'"/>');
1
votes

You can use jspdf to capture a canvas into an image or pdf like this:

var imgData = canvas.toDataURL('image/png');              
var doc = new jsPDF('p', 'mm');
doc.addImage(imgData, 'PNG', 10, 10);
doc.save('sample-file.pdf');

More info: https://github.com/MrRio/jsPDF

0
votes

On some versions of Chrome, you can:

  1. Use the draw image function ctx.drawImage(image1, 0, 0, w, h);
  2. Right-click on the canvas
0
votes

The simple answer is just to take the blob of it and set the img src to a new object URL of that blob, then add that image to a PDF using some library, like

var ok = document.createElement("canvas")
ok.width = 400
ok.height = 140
var ctx = ok.getContext("2d");
for(let k = 0; k < ok.height; k++) 
  (
    k 
    % 
    Math.floor(
      (
        Math.random()
      ) *
      10
    )
    == 
    0
  ) && (y => {
    for(var i = 0; i < ok.width; i++) {
      if(i % 25 == 0) {
        ctx.globalAlpha = Math.random()
        ctx.fillStyle = (
          "rgb(" + 
          Math.random() * 255 + "," +
          Math.random() * 255 + "," +
          Math.random() * 255 + ")"
        );

        (wdth =>
          ctx.fillRect(
            Math.sin(
              i * Math.PI / 180
            ) * 
              Math.random() *
              ok.width,
            Math.cos(
              i * Math.PI / 180,
            ) * wdth + y,
            wdth,
            wdth
          )
        )(15)
      }
    }
  })(k)

ok.toBlob(blob => {
  k.src = URL.createObjectURL(blob)
})
<img id=k>

Alternatively, if you wanted to work with low-level byte data, you can get the raw bytes of the canvas, then, depending on the file spec, write the raw image data into the necessary bytes of the data. you just need to call ctx.getImageData(0, 0, ctx.canvas.widht, ctx.canvas.height) to get the raw image data, then based on the file specification, write it to that

0
votes

if you want to emebed the canvas you can use this snippet

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <canvas id=canvas width=200 height=200></canvas>
    <iframe id='img' width=200 height=200></iframe>
    <script>
        window.onload = function() {
            var canvas = document.getElementById("canvas");
            var context = canvas.getContext("2d");
            context.fillStyle = "green";
            context.fillRect(50, 50, 100, 100);
            document.getElementById('img').src = canvas.toDataURL("image/jpeg");
            console.log(canvas.toDataURL("image/jpeg"));
        }
    </script>
</body>
</html>
0
votes

The key point is

canvas.toDataURL(type, quality)


And I want to provide an example for someone like me who wants to save SVG to PNG(also can add some text if you wish), which may be from an Online source or font-awesome icon, etc.

Example

100% javascript and no other 3-rd library.

<script>
  (() => {
    window.onload = () => {
      // Test 1: SVG from Online
      const canvas = new Canvas(650, 500)
      // canvas.DrawGrid() // If you want to show grid, you can use it.
      const svg2img = new SVG2IMG(canvas.canvas, "https://upload.wikimedia.org/wikipedia/commons/b/bd/Test.svg")
      svg2img.AddText("Hello", 100, 250, {mode: "fill", color: "yellow", alpha: 0.8})
      svg2img.AddText("world", 200, 250, {mode: "stroke", color: "red"})
      svg2img.AddText("!", 280, 250, {color: "#f700ff", size: "72px"})
      svg2img.Build("Test.png")

      // Test 2: URI.data
      const canvas2 = new Canvas(180, 180)
      const uriData = "data:image/svg+xml;base64,PHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtc21pbGUtd2luayBmYS13LTE2IiBhcmlhLWhpZGRlbj0idHJ1ZSIgZm9jdXNhYmxlPSJmYWxzZSIgZGF0YS1wcmVmaXg9ImZhciIgZGF0YS1pY29uPSJzbWlsZS13aW5rIiByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDQ5NiA1MTIiIGRhdGEtZmEtaTJzdmc9IiI+PHBhdGggZmlsbD0iY3VycmVudENvbG9yIiBkPSJNMjQ4IDhDMTExIDggMCAxMTkgMCAyNTZzMTExIDI0OCAyNDggMjQ4IDI0OC0xMTEgMjQ4LTI0OFMzODUgOCAyNDggOHptMCA0NDhjLTExMC4zIDAtMjAwLTg5LjctMjAwLTIwMFMxMzcuNyA1NiAyNDggNTZzMjAwIDg5LjcgMjAwIDIwMC04OS43IDIwMC0yMDAgMjAwem0xMTcuOC0xNDYuNGMtMTAuMi04LjUtMjUuMy03LjEtMzMuOCAzLjEtMjAuOCAyNS01MS41IDM5LjQtODQgMzkuNHMtNjMuMi0xNC4zLTg0LTM5LjRjLTguNS0xMC4yLTIzLjctMTEuNS0zMy44LTMuMS0xMC4yIDguNS0xMS41IDIzLjYtMy4xIDMzLjggMzAgMzYgNzQuMSA1Ni42IDEyMC45IDU2LjZzOTAuOS0yMC42IDEyMC45LTU2LjZjOC41LTEwLjIgNy4xLTI1LjMtMy4xLTMzLjh6TTE2OCAyNDBjMTcuNyAwIDMyLTE0LjMgMzItMzJzLTE0LjMtMzItMzItMzItMzIgMTQuMy0zMiAzMiAxNC4zIDMyIDMyIDMyem0xNjAtNjBjLTI1LjcgMC01NS45IDE2LjktNTkuOSA0Mi4xLTEuNyAxMS4yIDExLjUgMTguMiAxOS44IDEwLjhsOS41LTguNWMxNC44LTEzLjIgNDYuMi0xMy4yIDYxIDBsOS41IDguNWM4LjUgNy40IDIxLjYuMyAxOS44LTEwLjgtMy44LTI1LjItMzQtNDIuMS01OS43LTQyLjF6Ij48L3BhdGg+PC9zdmc+"
      const svg2img2 = new SVG2IMG(canvas2.canvas, uriData)
      svg2img2.Build("SmileWink.png")

      // Test 3: Exists SVG
      ImportFontAwesome()
      const range = document.createRange()
      const fragSmile = range.createContextualFragment(`<i class="far fa-smile" style="background-color:black;color:yellow"></i>`)
      document.querySelector(`body`).append(fragSmile)

      // use MutationObserver wait the fontawesome convert ``<i class="far fa-smile"></i>`` to SVG. If you write the element in the HTML, then you can skip this hassle way.
      const observer = new MutationObserver((mutationRecordList, observer) => {
        for (const mutation of mutationRecordList) {
          switch (mutation.type) {
            case "childList":
              const targetSVG = mutation.target.querySelector(`svg`)
              if (targetSVG !== null) {
                const canvas3 = new Canvas(64, 64) // 👈 Focus here. The part of the observer is not important.
                const svg2img3 = new SVG2IMG(canvas3.canvas, SVG2IMG.Convert2URIData(targetSVG))
                svg2img3.Build("Smile.png")
                targetSVG.remove() // This SVG is created by font-awesome, and it's an extra element. I don't want to see it.
                observer.disconnect()
                return
              }
          }
        }
      })
      observer.observe(document.querySelector(`body`), {childList: true})
    }
  })()

  class SVG2IMG {
    /**
     * @param {HTMLCanvasElement} canvas
     * @param {string} src "http://.../xxx.svg"  or "data:image/svg+xml;base64,${base64}"
     * */
    constructor(canvas, src) {
      this.canvas = canvas;
      this.context = this.canvas.getContext("2d")
      this.src = src
      this.addTextList = []
    }

    /**
     * @param {HTMLElement} node
     * @param {string} mediaType: https://en.wikipedia.org/wiki/Media_type#Common_examples_%5B10%5D
     * @see https://en.wikipedia.org/wiki/List_of_URI_schemes
     * */
    static Convert2URIData(node, mediaType = 'data:image/svg+xml') {
      const base64 = btoa(node.outerHTML)
      return `${mediaType};base64,${base64}`
    }

    /**
     * @param {string} text
     * @param {int} x
     * @param {int} y
     * @param {"stroke"|"fill"} mode
     * @param {string} size, "30px"
     * @param {string} font, example: "Arial"
     * @param {string} color, example: "#3ae016" or "yellow"
     * @param {int} alpha, 0.0 (fully transparent) to 1.0 (fully opaque) // https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#transparency
     * */
    AddText(text, x, y, {mode = "fill", size = "32px", font = "Arial", color = "black", alpha = 1.0}) {
      const drawFunc = (text, x, y, mode, font) => {
        return () => {
          // https://www.w3schools.com/graphics/canvas_text.asp
          // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillText
          const context = this.context
          const originAlpha = context.globalAlpha
          context.globalAlpha = alpha
          context.font = `${size} ${font}`

          switch (mode) {
            case "fill":
              context.fillStyle = color
              context.fillText(text, x, y)
              break
            case "stroke":
              context.strokeStyle = color
              context.strokeText(text, x, y)
              break
            default:
              throw Error(`Unknown mode:${mode}`)
          }
          context.globalAlpha = originAlpha
        }
      }
      this.addTextList.push(drawFunc(text, x, y, mode, font))
    }

    /**
     * @description When the build is finished, you can click the filename to download the PNG or mouse enters to copy PNG to the clipboard.
     * */
    Build(filename = "download.png") {
      const img = new Image()
      img.src = this.src
      img.crossOrigin = "anonymous" // Fixes: Tainted canvases may not be exported

      img.onload = (event) => {
        this.context.drawImage(event.target, 0, 0)
        for (const drawTextFunc of this.addTextList) {
          drawTextFunc()
        }

        // create a "a" node for download
        const a = document.createElement('a')
        document.querySelector('body').append(a)
        a.innerText = filename
        a.download = filename

        const quality = 1.0
        // a.target = "_blank"
        a.href = this.canvas.toDataURL("image/png", quality)
        a.append(this.canvas)
      }

      this.canvas.onmouseenter = (event) => {
        // set background to white. Otherwise, background-color is black.
        this.context.globalCompositeOperation = "destination-over" // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation // https://www.w3schools.com/tags/canvas_globalcompositeoperation.asp
        this.context.fillStyle = "rgb(255,255,255)"
        this.context.fillRect(0, 0, this.canvas.width, this.canvas.height)
        this.canvas.toBlob(blob => navigator.clipboard.write([new ClipboardItem({'image/png': blob})])) // copy to clipboard
      }
    }
  }

  class Canvas {

    /**
     * @description for do something like that: ``<canvas width="" height=""></>canvas>``
     **/
    constructor(w, h) {
      const canvas = document.createElement("canvas")
      document.querySelector(`body`).append(canvas)
      this.canvas = canvas;
      [this.canvas.width, this.canvas.height] = [w, h]
    }

    /**
     * @description If your SVG is large, you may want to know which part is what you wanted.
     * */
    DrawGrid(step = 100) {
      const ctx = this.canvas.getContext('2d')
      const w = this.canvas.width
      const h = this.canvas.height

      // Draw the vertical line.
      ctx.beginPath();
      for (let x = 0; x <= w; x += step) {
        ctx.moveTo(x, 0);
        ctx.lineTo(x, h);
      }
      // set the color of the line
      ctx.strokeStyle = 'rgba(255,0,0, 0.5)'
      ctx.lineWidth = 1
      ctx.stroke();

      // Draw the horizontal line.
      ctx.beginPath();
      for (let y = 0; y <= h; y += step) {
        ctx.moveTo(0, y)
        ctx.lineTo(w, y)
      }
      ctx.strokeStyle = 'rgba(128, 128, 128, 0.5)'
      ctx.lineWidth = 5
      ctx.stroke()
    }
  }

  function ImportFontAwesome() {
    const range = document.createRange()
    const frag = range.createContextualFragment(`
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" integrity="sha512-HK5fgLBL+xu6dm/Ii3z4xhlSUyZgTT9tuc/hSrtw6uzJOvgRr2a9jyxxT1ely+B+xFAmJKVSTbpM/CuL7qxO8w==" crossorigin="anonymous" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/js/all.min.js" integrity="sha512-UwcC/iaz5ziHX7V6LjSKaXgCuRRqbTp1QHpbOJ4l1nw2/boCfZ2KlFIqBUA/uRVF0onbREnY9do8rM/uT/ilqw==" crossorigin="anonymous"/>
`)
    document.querySelector("head").append(frag)
  }
</script>

if you want to run on stackoverflow and move your mouse on the picture may get error

DOMException: The Clipboard API has been blocked because of a permissions policy applied to the current document

You can copy the code on your local machine and run it again, will be fine.