How to Pass html2canvas Screenshots from JavaScript to Python
Send html2canvas output to Python with base64 JSON or multipart Blob uploads, with Flask code, CORS fixes, troubleshooting, and a hosted API option.
Direct answer: html2canvas(element) returns a browser <canvas>. Export that canvas with toDataURL() and send JSON to Python for small images, or export with toBlob() and send a FormData multipart upload for larger images. Python then validates and stores the bytes.
How the browser-to-Python flow works
- Select the DOM element to capture.
- Render it with
html2canvas(). - Export the canvas as a PNG, JPEG, or WebP data URL or Blob.
- POST the result to a Python endpoint.
- Validate the payload, enforce a size limit, and store or process the image.
html2canvas reconstructs the DOM and CSS it understands. It is not a pixel-perfect browser screenshot engine, so browser-only rendering effects and unsupported CSS can differ from what a user sees.
Option A: send a base64 data URL as JSON
This approach is simple and convenient for small screenshots. A data URL contains an image MIME prefix followed by base64-encoded bytes.
JavaScript client
import html2canvas from "https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/+esm";
async function sendScreenshot() {
const element = document.querySelector("#capture");
if (!element) throw new Error("#capture was not found");
const canvas = await html2canvas(element, {
backgroundColor: "#fff"
});
const dataUrl = canvas.toDataURL("image/png");
const response = await fetch("/api/screenshot", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ image: dataUrl })
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
return response.json();
}
sendScreenshot().then(console.log).catch(console.error);
Flask receiver
from base64 import b64decode
from binascii import Error as Base64Error
from flask import Flask, request, jsonify
app = Flask(__name__)
MAX_BYTES = 10 * 1024 * 1024
@app.post("/api/screenshot")
def receive_screenshot():
payload = request.get_json(silent=False)
data_url = payload.get("image", "")
prefix = "data:image/png;base64,"
if not isinstance(data_url, str) or not data_url.startswith(prefix):
return jsonify(error="expected a PNG data URL"), 400
try:
image_bytes = b64decode(data_url[len(prefix):], validate=True)
except (Base64Error, ValueError):
return jsonify(error="invalid base64"), 400
if len(image_bytes) > MAX_BYTES:
return jsonify(error="image too large"), 413
with open("upload.png", "wb") as output:
output.write(image_bytes)
return jsonify(ok=True, bytes=len(image_bytes))
if __name__ == "__main__":
app.run(debug=True)
The prefix check prevents accepting an unexpected format. Strict base64 decoding rejects malformed input, and the size limit prevents an unbounded request from consuming memory or disk. In production, authenticate the endpoint, generate a unique filename, and store uploads outside a publicly executable directory.
Option B: upload a Blob with multipart FormData
Blob upload keeps the image binary instead of expanding it into base64. It is generally the better choice for larger screenshots because it uses less bandwidth and less encoding and decoding work.
JavaScript client
import html2canvas from "https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/+esm";
async function uploadScreenshot() {
const canvas = await html2canvas(document.querySelector("#capture"), {
backgroundColor: "#fff"
});
const blob = await new Promise(resolve => {
canvas.toBlob(resolve, "image/png");
});
if (!blob) throw new Error("canvas export failed");
const form = new FormData();
form.append("screenshot", blob, "screenshot.png");
const response = await fetch("/api/screenshot-upload", {
method: "POST",
body: form
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
return response.json();
}
uploadScreenshot().then(console.log).catch(console.error);
Flask receiver
from flask import request, jsonify
@app.post("/api/screenshot-upload")
def receive_upload():
uploaded = request.files.get("screenshot")
if uploaded is None or uploaded.mimetype != "image/png":
return jsonify(error="PNG upload required"), 400
image_bytes = uploaded.read()
if len(image_bytes) > 10 * 1024 * 1024:
return jsonify(error="image too large"), 413
with open("upload.png", "wb") as output:
output.write(image_bytes)
return jsonify(ok=True, bytes=len(image_bytes))
Do not set the Content-Type header yourself for FormData. The browser adds the multipart boundary automatically.
Choosing PNG, JPEG, or WebP
| Format | Use it when | Trade-off |
|---|---|---|
| PNG | Text, diagrams, transparency, or lossless output | Usually larger files |
| JPEG | Photographic content and smaller payloads | Lossy compression and no transparency |
| WebP | You control both clients and servers | Confirm decoder and workflow support |
For a JPEG or WebP export, change both the MIME argument to toDataURL() or toBlob() and the server-side MIME validation.
Important html2canvas options
backgroundColor: set a solid background such as"#fff"; usenullwhen transparency is required.useCORS: true: asks the browser to request cross-origin images with CORS. The image server must return an appropriate CORS header.allowTaint: do not rely on this to export cross-origin pixels. A tainted canvas cannot be read withtoDataURL()ortoBlob().scale: usewindow.devicePixelRatiofor high-DPI output, while remembering that larger canvases require more memory and upload bandwidth.windowWidthandwindowHeight: set these to the element’s scroll dimensions when content is clipped.width,height,x, andy: control the capture rectangle when the default element bounds are not suitable.onclone: modify the cloned document before rendering, for example by hiding animation or inserting a capture-only style.
const element = document.querySelector("#capture");
const canvas = await html2canvas(element, {
backgroundColor: "#fff",
useCORS: true,
scale: window.devicePixelRatio,
windowWidth: element.scrollWidth,
windowHeight: element.scrollHeight,
onclone: clonedDocument => {
clonedDocument.querySelectorAll(".animated").forEach(node => {
node.style.animation = "none";
node.style.transition = "none";
});
}
});
Cross-origin images and tainted canvases
The most common reason for a blank or failed export is a cross-origin image. The browser may display the image but still mark the canvas as tainted, preventing pixel reads.
- Make the image response include a suitable
Access-Control-Allow-Originvalue. - Set
useCORS: truebefore rendering. - Ensure the image URL is requested in a way that allows CORS.
- If you cannot change the remote server, proxy the image through your own origin and return it with the correct content type.
useCORS: true cannot override a server that omits the required response header. A proxy must actually fetch and return the image through the page’s origin.
Preventing clipped captures
html2canvas captures the element’s computed bounds by default. Long pages can therefore be clipped if the element has a constrained viewport or scroll container. Capture the full scroll area and remove overflow restrictions in the cloned document.
const element = document.querySelector("#capture");
const canvas = await html2canvas(element, {
width: element.scrollWidth,
height: element.scrollHeight,
windowWidth: element.scrollWidth,
windowHeight: element.scrollHeight,
onclone: doc => {
const clone = doc.querySelector("#capture");
clone.style.maxHeight = "none";
clone.style.overflow = "visible";
}
});
Security and validation checklist
- Require authentication or a CSRF-protected session for browser uploads.
- Enforce request and decoded-byte limits at the web server and application layers.
- Validate the declared MIME type and, for higher assurance, inspect the file signature.
- Use generated filenames rather than trusting the client-provided name.
- Store files outside executable and public directories unless public access is intended.
- Rate-limit the endpoint and reject unexpected JSON shapes or multipart fields.
- For untrusted users, scan or re-encode images before serving them back.
Performance, reliability, and cost considerations
Base64 adds encoding and decoding work, expands the payload, and is less cacheable for binary data. JSON is easy to inspect and works well for small screenshots. Multipart Blob uploads preserve binary data and are usually more efficient for larger files.
High scale, large scroll dimensions, and multiple simultaneous captures increase browser memory use. Capture only the required element, avoid unnecessary device-pixel scaling, and release references after upload. For reliability, check both the HTTP status and the JSON response, retry transient network failures with a limit, and make server writes atomic so interrupted uploads do not become valid-looking files.
Your own infrastructure determines the cost of this DIY method: browser CPU and memory, network transfer, storage, and any image processing. Set explicit limits before accepting screenshots from untrusted clients.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
SecurityError during export |
A cross-origin image tainted the canvas | Configure image CORS or proxy the asset through your origin; use useCORS: true. |
| Remote images are missing | The image server blocks CORS or the image was not loaded before capture | Fix response headers, wait for image load, and then render. |
| Screenshot is blank | The target is empty, hidden, not yet rendered, or outside the expected viewport | Verify the selector, wait for fonts and images, and inspect the cloned DOM with onclone. |
| Right or bottom edge is clipped | The element has a fixed viewport or scroll container | Use scrollWidth/scrollHeight and remove overflow limits in the clone. |
| Flask returns 400 | Wrong prefix, malformed JSON, missing field, or unexpected MIME type | Inspect the request body, validate the exact data URL prefix, and send the correct multipart field. |
| Flask returns 413 | Decoded image exceeds the configured limit | Reduce capture dimensions or compression, or deliberately adjust the server limit. |
toBlob() returns null |
The browser could not encode the requested format or the canvas is invalid | Use a supported MIME type such as PNG and handle the null result. |
| Fonts or animations differ | Resources were still loading or CSS is not fully supported | Wait for fonts, disable animation in onclone, and compare against html2canvas’s supported CSS behavior. |
| Upload hangs or times out | Image is too large or the network is unreliable | Reduce scale, use Blob upload, set client and server timeouts, and retry transient failures. |
Or skip the browser setup
ScreenshotNeo captures a URL on its servers and returns an image or PDF, so your Python service does not need to run a browser or transfer a canvas from JavaScript. See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Does html2canvas create a file on the server?
No. It creates a canvas in the browser. You must export the canvas and upload the resulting bytes.
Is base64 or FormData better?
Use base64 JSON for small, simple requests. Use Blob and FormData for larger screenshots or higher upload efficiency.
Can html2canvas capture a page exactly like the browser?
No. It reconstructs the DOM and supported CSS, so some browser rendering details differ from a native screenshot.
Why does useCORS not fix every remote image?
The remote server must opt into CORS. A client option cannot add a missing response header.
Can I send JPEG instead of PNG?
Yes. Export with image/jpeg, optionally pass a quality value, and update server MIME validation and storage handling.


