How to Download a PNG Image From a URL
Save a PNG from any accessible URL using your browser, curl, Python, or Node.js—and verify that the response is really a PNG.
To download a PNG from a URL, open the direct image link and save it from your browser, or run curl -L "IMAGE_URL" -o image.png. The -L option follows redirects and -o chooses the local filename.
For repeatable downloads, use curl or a small Python or Node.js script. Always check the response type and the saved file: a URL ending in .png can still return an HTML error page, a login screen, or another image format.
Download a PNG in a browser
- Open the direct image URL in your browser. A direct URL usually displays the image by itself.
- Use the image context menu or the browser’s image controls.
- Choose Save image as or the equivalent download command.
- Choose a destination and a filename ending in
.pngonly when the content is actually PNG data.
The exact menu, automatic-download behavior, and filename depend on the browser, operating system, response headers, and browser settings. If the URL opens a web page instead of an image, you may have a page URL rather than the image’s direct URL.
Can an HTML link force a PNG download?
<a href="/images/chart.png" download="chart.png">Download PNG</a>
The HTML download attribute suggests a filename and download behavior, but it is not a universal download switch. Browsers generally restrict it to same-origin URLs and blob: or data: URLs. It cannot reliably force a download from an arbitrary third-party image host. The remote server’s headers and the browser’s rules still apply. See the MDN documentation for the download attribute.
Download with curl
Use an explicit output path so the saved name does not depend on the URL’s final path segment:
curl -L "https://example.com/path/image.png" -o image.png
Replace the example URL with the direct image URL. To save into a directory, include the path:
mkdir -p downloads
curl -L "https://example.com/path/image.png" -o downloads/image.png
Useful curl options
| Option | Purpose | Example |
|---|---|---|
-L |
Follow HTTP redirects | curl -L URL -o image.png |
-o FILE |
Write the response to a chosen filename | -o image.png |
-I |
Inspect response headers without downloading the body | curl -I -L URL |
-f |
Return an error for HTTP 4xx or 5xx responses | curl -fL URL -o image.png |
-sS |
Hide the progress bar but show errors | curl -sS -L URL -o image.png |
--retry 3 |
Retry transient failures | curl --retry 3 -L URL -o image.png |
-O |
Use the remote filename when appropriate | curl -LO URL |
A robust one-off command is:
curl -fL --retry 3 "https://example.com/path/image.png" -o image.png
Use -O only when you trust the server-provided name. An explicit -o path is safer for scripts and batch jobs. curl documents remote retrieval and output naming in its command-line manual.
Download with Python
The following script streams the response to disk, checks the HTTP status, and reports the server’s media type:
import sys
from pathlib import Path
import requests
url = sys.argv[1]
out = Path(sys.argv[2] if len(sys.argv) > 2 else "image.png")
with requests.get(url, stream=True, timeout=30, allow_redirects=True) as response:
response.raise_for_status()
content_type = response.headers.get("Content-Type", "")
print(f"HTTP {response.status_code}; Content-Type: {content_type}")
with out.open("wb") as file:
for chunk in response.iter_content(chunk_size=1024 * 1024):
if chunk:
file.write(chunk)
print(f"Saved {out}")
Run it after installing Requests:
python -m pip install requests
python download_png.py "https://example.com/path/image.png" downloaded.png
If you already know the URL and want a short version:
import requests
r = requests.get("https://example.com/path/image.png", timeout=30)
r.raise_for_status()
open("image.png", "wb").write(r.content)
Use streaming for large files so the entire response is not held in memory. A successful HTTP status alone does not prove that the body is a PNG; inspect Content-Type and validate the file when that distinction matters.
Download with Node.js
Modern Node.js includes fetch. This example follows redirects, checks the status, and writes the response as bytes:
import { createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { Readable } from "node:stream";
const url = process.argv[2];
const output = process.argv[3] || "image.png";
if (!url) {
console.error("Usage: node download-png.mjs IMAGE_URL [OUTPUT_FILE]");
process.exit(1);
}
const response = await fetch(url, { redirect: "follow" });
if (!response.ok || !response.body) {
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}
console.log(`Content-Type: ${response.headers.get("content-type") || "unknown"}`);
await pipeline(Readable.fromWeb(response.body), createWriteStream(output));
console.log(`Saved ${output}`);
node download-png.mjs "https://example.com/path/image.png" downloaded.png
For a small response, you can also use an ArrayBuffer:
const response = await fetch("https://example.com/path/image.png");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const bytes = new Uint8Array(await response.arrayBuffer());
await Bun.write("image.png", bytes);
The second example uses Bun’s file API. Use the streaming version in standard Node.js applications.
Verify that the downloaded file is really PNG
image.png is only a filename. The relevant HTTP media type is image/png, and PNG files have a standard signature at the beginning of the file. Servers can return an HTML error page, an access-denied document, or a different image format while the URL or output name still ends in .png.
Inspect headers
curl -I -L "https://example.com/path/image.png"
Look for a successful final status and a suitable Content-Type. Redirects mean you should inspect the final response, which is why -L is useful.
Inspect the saved file on macOS or Linux
file image.png
xxd -l 8 image.png
A valid PNG begins with the signature bytes 89 50 4e 47 0d 0a 1a 0a. The file command can identify common image formats. The IANA media-type registry lists image/png as the PNG media type.
Check in Python
from pathlib import Path
signature = Path("image.png").read_bytes()[:8]
if signature != b"\x89PNG\r\n\x1a\n":
raise ValueError("The file does not have a PNG signature")
print("PNG signature found")
When a URL needs authentication or special headers
A bare URL may not be enough when the image requires a login session, a cookie, an API token, a referrer, or a particular user agent. Use the site’s permitted download flow and send only credentials you are authorized to use.
curl -fL \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: image/png" \
"https://example.com/private/image" \
-o private-image.png
Do not put long-lived secrets in shell history, source control, client-side JavaScript, or a public URL. Prefer environment variables or a server-side request.
Why a browser script may fail even when the image is visible
Cross-origin browser rules can allow an image to be displayed while preventing JavaScript from reading its pixels through a canvas. The image server must opt in with appropriate CORS headers. Setting an image element’s crossOrigin property starts a CORS request; it cannot grant permission that the server does not provide. See MDN’s CORS documentation.
const image = new Image();
image.crossOrigin = "anonymous";
image.src = "https://images.example.com/photo.png";
image.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
canvas.getContext("2d").drawImage(image, 0, 0);
// canvas.toBlob(...) works only when the image passed CORS checks.
};
For a simple file download, a direct browser save or a server-side curl/Python/Node.js request is usually more appropriate than reading pixels in page JavaScript.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| The file is HTML, not an image | Redirect, login page, error page, or hotlink protection | Use curl -I -L, inspect the final URL and Content-Type, then authenticate or use the site’s download flow. |
| HTTP 301 or 302 | The image moved or the host redirects HTTP to HTTPS | Follow redirects with curl’s -L or Python/Node redirect handling. |
| HTTP 403 | Permission, anti-hotlink rules, missing cookie, or blocked user agent | Use an authorized session or required headers. Do not attempt to bypass access controls. |
| HTTP 404 | The URL is wrong or the asset was removed | Copy the direct image address again and confirm the resource exists. |
| Browser says the image is blocked by CORS | The server does not allow your origin to read image pixels | Download server-side, or ask the image host to configure CORS if you control it. |
| Downloaded file is empty or truncated | Interrupted connection, disk problem, or a script that did not consume the stream | Use retries, check free disk space, and stream the complete response before closing the file. |
| It opens but has the wrong extension | The server returned JPEG, WebP, or another format | Trust the response type and file signature; rename only after identifying the actual format. |
Performance, reliability, and cost notes
- For one image, browser saving is simplest. For repeated downloads, curl or a script gives consistent names, logging, retries, and automation.
- Use streaming for large responses and an explicit output path to avoid memory spikes and accidental overwrites.
- Follow redirects, but log the final URL when reproducibility matters.
- Retries help with temporary network failures; they do not fix a 401, 403, or 404.
- Check status, media type, and file signature before processing untrusted downloads.
- The download itself normally uses software already on your computer. The research does not identify a special paid product required to retrieve one PNG.
Or skip the browser setup
If what you actually need is a PNG screenshot of a webpage rather than the original image bytes, ScreenshotNeo provides a one-request screenshot API. It accepts a URL and returns a PNG, JPEG, WebP, or PDF. The API can also capture a selected element, load lazy images, apply custom headers and cookies, wait for selectors or network idle, and use device, viewport, dark-mode, and other capture options. Full details are in the ScreenshotNeo API documentation.
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)
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}`);
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. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account and get 1,000 screenshots each month without adding a card.
FAQ
How do I know whether a URL is a direct image URL?
Open it in a browser and inspect the response headers with curl -I -L. A direct image response normally has an image media type such as image/png; a page URL usually returns text/html.
Does changing .jpg to .png convert an image?
No. The extension changes the name only. Convert the image with an image-processing tool after downloading it.
Can I download an image that is visible but has no obvious image URL?
Use the browser’s developer tools to inspect the image request, or use the site’s own download command. If access requires a session or permission, a simple public URL request may not work.
Should I use -O or -o with curl?
Use -o when you need a predictable local filename. Use -O when the server’s remote filename is acceptable.


