Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minuteUse PHP to control a real browser, then pass an explicit filesystem path to the browser’s screenshot method. PHP cannot reliably rasterize an arbitrary, JavaScript-heavy webpage by itself. Playwright or Puppeteer renders the page, waits for it to reach the state you need, and writes PNG, JPEG, or WebP bytes to a folder you control.
The shortest Playwright-style operation is:
$page->goto('https://example.com');
$page->screenshot(__DIR__ . '/screenshots/page.png');
This guide shows a complete PHP workflow, a Puppeteer worker option, full-page and element captures, safe filenames, permissions, concurrency, troubleshooting, and a browser-free alternative.
What PHP needs to save a webpage screenshot
A screenshot has two distinct parts: rendering and storage. A browser engine performs the rendering; PHP supplies the URL, timing and destination path. Your application therefore needs:
- A browser automation library such as Playwright or Puppeteer, plus its browser runtime.
- A destination directory that exists and is writable by the PHP-FPM, web-server or queue-worker user.
- An explicit filename with an image extension such as
.png,.jpgor.webp. - Timeout and cleanup rules appropriate for your workload.
Keep captures containing private or customer data outside a public web root unless public access is intentional. Build paths from __DIR__ or a configured storage root rather than the process’s current working directory; workers and cron jobs may start elsewhere.
#1 Best Overall
Option 1: Playwright PHP
Playwright PHP exposes a screenshot method whose first argument is the destination path. A minimal capture, after your application has created a page object, is:
$page->goto('https://example.com');
$page->screenshot(__DIR__ . '/artifacts/home.png');
The method returns image data as a string as well, but supplying a path writes the file directly. Use an absolute path and create the directory before navigation.
A production-oriented capture function
<?php
function saveScreenshot($page, string $url, string $storageRoot, string $name): string
{
$directory = rtrim($storageRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'screenshots';
if (!is_dir($directory) && !mkdir($directory, 0750, true) && !is_dir($directory)) {
throw new RuntimeException("Cannot create screenshot directory: {$directory}");
}
if (!is_writable($directory)) {
throw new RuntimeException("Screenshot directory is not writable: {$directory}");
}
// Permit a simple application-generated name; do not accept arbitrary path input.
$safeName = preg_replace('/[^A-Za-z0-9._-]/', '_', $name);
if ($safeName === '' || pathinfo($safeName, PATHINFO_EXTENSION) === '') {
$safeName .= '.png';
}
$path = $directory . DIRECTORY_SEPARATOR . $safeName;
$page->goto($url, ['waitUntil' => 'networkidle', 'timeout' => 60000]);
$page->screenshot($path, [
'fullPage' => true,
'type' => 'png'
]);
if (!is_file($path) || filesize($path) === 0) {
throw new RuntimeException('The browser returned without creating a non-empty image.');
}
return $path;
}
// $page is a page created by your Playwright bootstrap.
$path = saveScreenshot($page, 'https://example.com', __DIR__ . '/var', 'example-' . date('Ymd-His') . '.png');
echo "Saved to {$path}n";
The exact browser-launch code depends on the Playwright PHP package and version you have installed. Keep browser startup and shutdown in your application bootstrap, and always close the browser or release the context in a finally block.
Choose the capture scope
| Need | Setting | Result |
|---|---|---|
| What a visitor currently sees | Default screenshot | The current viewport only |
| The complete scrollable document | fullPage => true |
A tall image containing the page below the fold |
| One card, chart or component | Locate an element and call its screenshot method | Only that element’s bounding box |
For an element capture, use your library’s locator API rather than guessing coordinates. Conceptually:
$page->locator('.invoice-summary')->screenshot(__DIR__ . '/artifacts/summary.png');
Selectors must match the rendered DOM. If a component is created after navigation, wait for it explicitly before taking the image.
Option 2: PHP orchestrating a Puppeteer worker
Puppeteer’s page.screenshot() accepts a path option. The extension determines the image type, and relative paths are resolved against the Node process’s current working directory, so an absolute path is safer.
Node worker
// capture.js
const puppeteer = require('puppeteer');
(async () => {
const [url, output] = process.argv.slice(2);
if (!url || !output) throw new Error('Usage: node capture.js URL ABSOLUTE_OUTPUT_PATH');
const browser = await puppeteer.launch({headless: true});
try {
const page = await browser.newPage();
await page.setViewport({width: 1440, height: 900, deviceScaleFactor: 1});
await page.goto(url, {waitUntil: 'networkidle2', timeout: 60000});
await page.screenshot({path: output, fullPage: true});
} finally {
await browser.close();
}
})();
PHP wrapper
<?php
$url = 'https://example.com';
$directory = __DIR__ . '/screenshots';
if (!is_dir($directory) && !mkdir($directory, 0750, true) && !is_dir($directory)) {
throw new RuntimeException('Could not create screenshot directory');
}
if (!is_writable($directory)) {
throw new RuntimeException('Screenshot directory is not writable');
}
$output = $directory . '/example-' . date('Ymd-His') . '.png';
$command = 'node ' . escapeshellarg(__DIR__ . '/capture.js') . ' '
. escapeshellarg($url) . ' ' . escapeshellarg($output) . ' 2>&1';
exec($command, $lines, $status);
if ($status !== 0 || !is_file($output) || filesize($output) === 0) {
throw new RuntimeException("Capture failed (status {$status}): " . implode("n", $lines));
}
echo "Saved to {$output}n";
Install Puppeteer in the directory containing capture.js, install its browser runtime, and run the worker under the same user that owns the destination directory. In a web request, a queue worker is usually more reliable than waiting for a browser launch to finish synchronously.
Wait for the page you actually want
networkidle is useful for mostly static pages, but analytics, advertisements and live connections can prevent true idleness. Choose a condition that represents the content you need:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
- Navigation completion: use the library’s load or DOM-content-loaded state for simple documents.
- A selector: wait until
.report-tableor another required component is visible. - A fixed delay: reserve this for animations or third-party widgets with no dependable selector.
- Application readiness: expose a page flag or wait for a known API result when your own site controls the code.
Disable animations or add a small, deliberate delay when visual comparisons matter. Fonts, viewport size, device scale, browser version, timezone and geolocation all affect pixels. A screenshot is visual evidence, not a substitute for normal DOM or accessibility assertions.
Folders, names and concurrent jobs
Permissions and web visibility
The directory must be writable by the effective PHP or worker user, not merely by your login account. A permission failure commonly occurs after deployment when ownership changes. Store sensitive images in a private directory and stream them through an authorization check instead of placing them under public/.
Rank #3
Collision-resistant names
Use a database ID, UUID or timestamp plus a random suffix. Never concatenate an untrusted URL directly into a path, and reject path separators even after sanitizing. If two jobs can capture the same record, write to a temporary filename and rename after a successful, non-empty capture.
Retention
Screenshot directories grow without bound unless you remove old files. A scheduled cleanup can delete files older than a chosen age or keep only a maximum count per project. Do not delete a file while another process is still uploading it; atomic rename and a job status record make this easier to coordinate.
Free tools Windows power users keep installed
One-click scans. No signup required.
Common failures and precise fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| “No such file or directory” | The folder was never created, or a relative path resolved elsewhere. | Create it recursively and use __DIR__ or an absolute storage root. |
| Permission denied | PHP-FPM or the worker cannot write the directory. | Correct ownership and mode for that service user; verify with is_writable(). |
| Empty or missing image | The browser failed before writing, or the process was terminated. | Capture stderr, check the exit status, increase the navigation timeout and verify the browser runtime. |
| Cookie banner, popup or spinner in the image | The page was captured before interaction or before the UI settled. | Wait for the required selector, click or hide the element, and disable animations where appropriate. |
| Only the top portion appears | A viewport screenshot was requested. | Enable full-page capture, or capture the specific element you need. |
| Page is blank or partially rendered | JavaScript error, blocked resource, bot challenge or an overly short wait. | Inspect console/network logs, test the URL in the same browser environment and wait for a meaningful readiness selector. |
| Works in a shell but not through PHP | Different PATH, user, environment variables or working directory. | Use absolute executable paths, log the effective user and pass absolute output paths. |
| Images differ between runs | Fonts, animations, responsive viewport, time or data changed. | Pin viewport and browser version, wait for fonts/data, freeze time where possible and compare under the same environment. |
Performance, reliability and cost decisions
Launching a browser for every request is expensive. Keep a browser process alive in a worker, create an isolated context per job, and close pages promptly. Limit concurrency according to available CPU and memory; a queue prevents web requests from timing out while large full-page captures run.
Set separate navigation and overall-job timeouts. Record the URL, viewport, browser version, elapsed time, output path and failure reason. Retry transient navigation failures with a small backoff, but do not retry deterministic HTTP errors indefinitely. If a destination is user-supplied, restrict schemes to HTTPS and HTTP, block internal network ranges as appropriate for your threat model, and cap image dimensions and job duration.
Use PNG for lossless text and UI evidence, JPEG for photographic pages when size matters, and WebP when your downstream system supports it. Full-page images consume more memory than viewport captures; element screenshots are often the cheapest option for dashboards or product cards.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP or PDF, so PHP can save the response directly without installing a local browser:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo documentation for parameters and response headers. A PHP equivalent using cURL is:
<?php
$url = 'https://stripe.com';
$output = __DIR__ . '/screenshots/stripe.webp';
if (!is_dir(dirname($output))) {
mkdir(dirname($output), 0750, true);
}
$ch = curl_init('https://api.screenshotneo.com/v1/shot?' . http_build_query([
'access_key' => 'YOUR_API_KEY',
'url' => $url,
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 90,
]);
$bytes = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($bytes === false || $status < 200 || $status >= 300) {
throw new RuntimeException("Screenshot request failed ({$status}): {$error}");
}
file_put_contents($output, $bytes, LOCK_EX);
ScreenshotNeo removes cookie and consent banners, newsletter popups and chat widgets before capture; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients. Features include full-page and CSS-selector captures, device presets, retina scale, PDF controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, a usage API and an OpenAPI specification.
The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account.
FAQ
Can PHP take a screenshot without JavaScript?
It can save an image returned by a service, but a local capture of a modern webpage still needs a rendering engine. PHP’s role is orchestration and file handling.
Recommended Free Tools
Which extension should I use?
Use PNG for crisp interfaces and text, JPEG for smaller photographic images, and WebP when your consumer supports it. Match the extension to the format requested from the browser or API.
Why is an absolute path safer?
Relative paths depend on the process working directory, which can differ between a web request, CLI command and queue worker. An absolute path based on a known storage root removes that ambiguity.
Frequently Asked Questions
Can PHP take a screenshot without JavaScript?
It can save an image returned by a service, but a local capture of a modern webpage still needs a rendering engine. PHP’s role is orchestration and file handling.
Which extension should I use?
Use PNG for crisp interfaces and text, JPEG for smaller photographic images, and WebP when your consumer supports it. Match the extension to the format requested from the browser or API.
Why is an absolute path safer?
Relative paths depend on the process working directory, which can differ between a web request, CLI command and queue worker. An absolute path based on a known storage root removes that ambiguity.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

