How to Save an HTML Image as JPEG
Learn how to save an HTML image as a real JPEG using canvas, handle CORS errors, control quality, and automate page screenshots.
Use the HTML canvas API when you need to export pixels as JPEG. Draw the image into a <canvas>, call canvas.toBlob(callback, "image/jpeg", quality), then download the returned Blob. The quality value is between 0 and 1. For large images, this Blob and object URL approach avoids the large in-memory string created by toDataURL() (MDN: toBlob).
An image displayed by an <img> element is not automatically a canvas image. If you only have an ordinary image file, download or open that file in an image converter. If you need to save a rendered web page, first capture the page pixels, then encode those pixels as JPEG.
Choose the right workflow
| Starting point | Recommended method | Important limitation |
|---|---|---|
| Pixels already in a canvas | toBlob("image/jpeg") |
Canvas must not be tainted by disallowed cross-origin pixels. |
| A small canvas export or data URL | toDataURL("image/jpeg") |
Creates the entire encoded image as an in-memory string. |
An <img> or downloaded file |
Use the original file or draw it into a canvas first. | Renaming .png to .jpg does not convert the encoding. |
| A complete web page screenshot | Use a browser capture tool or ScreenshotNeo. | Page loading, consent banners, lazy content, and cross-origin resources need handling. |
Save a canvas image as JPEG in the browser
This complete example loads an image, draws it into a canvas, converts it to JPEG, and starts a download. It checks that the browser actually returned JPEG data before naming the file.
<!doctype html>
<meta charset="utf-8">
<title>Canvas to JPEG</title>
<input id="file" type="file" accept="image/*">
<button id="save" disabled>Save as JPEG</button>
<canvas id="canvas" hidden></canvas>
<script>
const input = document.querySelector('#file');
const save = document.querySelector('#save');
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
let image;
input.addEventListener('change', () => {
const file = input.files[0];
if (!file) return;
const url = URL.createObjectURL(file);
image = new Image();
image.onload = () => {
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
ctx.drawImage(image, 0, 0);
URL.revokeObjectURL(url);
save.disabled = false;
};
image.onerror = () => {
URL.revokeObjectURL(url);
alert('The selected file could not be decoded as an image.');
};
image.src = url;
});
save.addEventListener('click', () => {
const quality = 0.92;
canvas.toBlob((blob) => {
if (!blob) {
alert('JPEG encoding failed.');
return;
}
if (blob.type !== 'image/jpeg') {
alert(`The browser returned ${blob.type || 'unknown data'}, not JPEG.`);
return;
}
const downloadUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = 'image.jpg';
document.body.appendChild(link);
link.click();
link.remove();
// Keep the URL alive for the download, then release it.
setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000);
}, 'image/jpeg', quality);
});
</script>
toBlob() is asynchronous, so the callback receives the encoded result later. The callback can receive null when encoding fails. Keep the object URL alive until the browser has used it, then revoke it to release memory.
Convert an existing image element
Draw the loaded image at its natural dimensions before encoding. Setting crossOrigin must happen before assigning src, and the image server must send a compatible CORS header.
const image = new Image();
image.crossOrigin = 'anonymous';
image.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
canvas.getContext('2d').drawImage(image, 0, 0);
canvas.toBlob((blob) => {
if (!blob) throw new Error('Could not encode the image');
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'converted.jpg';
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}, 'image/jpeg', 0.9);
};
image.src = 'https://cdn.example.com/photo.png';
The remote server, rather than JavaScript, controls whether its pixels can be read by your page. Without the required CORS response, exporting a canvas containing those pixels raises a security error (MDN: CORS-enabled images).
Use toDataURL() when a data URL is needed
toDataURL() returns a string that can be assigned to an image source or download link:
const dataUrl = canvas.toDataURL('image/jpeg', 0.9);
const link = document.createElement('a');
link.href = dataUrl;
link.download = 'image.jpg';
link.click();
This is convenient for small images, previews, and APIs that specifically require a data URL. For large canvases, prefer toBlob(); a data URL holds the complete encoded image in one JavaScript string (MDN: toDataURL).
JPEG options and image quality
- MIME type: Pass
image/jpeg. If the requested type is unsupported or omitted, browsers use PNG as the documented fallback. - Quality: Pass a number from
0to1. Higher values generally preserve more detail and produce larger files, but quality is not a file-size promise. - Transparency: JPEG has no alpha channel. Transparent canvas pixels are typically composited against a solid background. Paint the background explicitly when it matters:
ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, canvas.width, canvas.height);. - Dimensions: Set canvas dimensions before drawing. CSS resizing changes display size, not the number of encoded pixels.
- Metadata: Canvas export may not preserve the source file’s EXIF metadata. Store any metadata you need separately.
Saving a full HTML page as a JPEG
If the requirement is a screenshot of a URL, a page-authored canvas script cannot reliably read every page. Browser automation must load the page, wait for dynamic content, handle lazy images, and respect origin rules. A self-managed flow commonly uses a headless browser, waits for a selector or network idle, captures PNG, and then converts the captured pixels to JPEG. Keep capture and conversion separate so you can diagnose whether a failure came from page loading or encoding.
Or skip the browser setup
ScreenshotNeo returns a page screenshot or PDF from one request. Its API can output JPEG directly, and the ScreenshotNeo documentation lists the available options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -d format=jpeg -o shot.jpg
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com", "format": "jpeg"}, timeout=90)
r.raise_for_status()
open("shot.jpg", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com', format: 'jpeg' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.jpg', Buffer.from(await res.arrayBuffer()));
Before capture, ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. An MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The Free plan includes 1,000 screenshots per month without a card, and paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
SecurityError during export |
The canvas is tainted by cross-origin pixels without permitted CORS. | Serve the image with the required CORS header, set crossOrigin before src, or proxy the image through a server you control. |
| Downloaded file is PNG | The requested JPEG type was unsupported or ignored. | Check blob.type or the data URL prefix; do not trust the extension alone. |
Callback receives null |
Encoding failed, often because of invalid dimensions or resource pressure. | Check canvas width and height, reduce the image size, and retry. |
| Image is blank | Drawing happened before the image loaded, or the image failed to decode. | Draw inside onload and handle onerror. |
| JPEG has a black or unexpected background | JPEG cannot store transparency. | Paint an explicit background before drawImage. |
| File is unexpectedly large | Large dimensions or a high quality setting. | Resize before drawing and lower quality gradually; measure the resulting Blob. |
| Screenshot misses content | Lazy loading or asynchronous page scripts had not finished. | Wait for a selector, a delay, or network idle before capture. |
Performance, reliability, and cost
- Memory: A canvas stores raw pixels, and
toDataURL()adds another large string. PrefertoBlob()for large images and revoke object URLs after use. - Speed: Resize at the canvas stage when the final output does not need source dimensions. Avoid repeated encode cycles; decode once, draw once, encode once.
- Reliability: Check image load events, export results, MIME types, and HTTP status codes. For page captures, record whether the failure occurred during navigation, rendering, or JPEG encoding.
- Cost: Local canvas conversion has no API request cost, but browser automation requires infrastructure. ScreenshotNeo bills only clean shots; failed loads, bot checks, blank pages, timeouts, and cache hits are free.
FAQ
Can I save an HTML <img> directly as JPEG?
Not with a filename change. Use the original image file, or draw it into a canvas and export the canvas as JPEG.
Does JPEG quality 1 guarantee perfect quality?
No. The value is an encoder preference in the 0–1 range. JPEG remains a lossy format.
Why does my canvas export work locally but fail in production?
The production image host may not permit CORS, or the page may load an additional cross-origin resource. Inspect every image drawn into the canvas.
Should I use PNG instead?
Use PNG when you need lossless pixels or transparency. Use JPEG when broad compatibility and smaller photographic output matter more.
Can I convert a remote web page without running a browser?
A normal HTTP client downloads HTML but does not render its final pixels. Use a browser capture service such as ScreenshotNeo when you need the rendered page.


