Screenshot Webpages as JPEG in PHP
Capture JavaScript-rendered webpages as JPEG in PHP with Browsershot, full-page options, waits, quality controls, troubleshooting, and an API alternative.

To screenshot a webpage as a JPEG in PHP, run a headless Chromium browser and set the screenshot format to JPEG. Spatie Browsershot is the shortest path for most PHP applications: it accepts a URL or HTML, delegates rendering to Puppeteer and Chrome, and saves an image or returns image bytes.
<?php
require __DIR__ . '/vendor/autoload.php';
use Spatie\Browsershot\Browsershot;
Browsershot::url('https://example.com')
->setScreenshotType('jpeg', 80)
->windowSize(1440, 900)
->save(__DIR__ . '/page.jpg');
The setScreenshotType('jpeg', 80) call selects JPEG output and a quality value of 80. windowSize fixes the viewport so repeated captures have predictable dimensions. Browsershot starts Chromium through Puppeteer, waits for the page to render, and writes page.jpg.
1. Install the PHP and browser dependencies
Browsershot is a Composer package, but it also needs Node.js, Puppeteer, and a working Chromium or Chrome executable. Install the PHP package in your application:
composer require spatie/browsershot
Install Puppeteer in the project or in the location configured for your deployment:
npm install puppeteer
Check the package requirements for your Browsershot version before deploying. Pin compatible PHP, Node.js, Chromium, and Puppeteer versions together; mismatched browser binaries are a common source of failures. In containers, ensure the image includes the shared libraries Chromium requires and that the process has permission to start a sandbox or is configured according to your container security policy.
2. Save a webpage as a JPEG
This is a complete command-line PHP script. It creates the output directory, captures the URL, and writes a JPEG file.

<?php
require __DIR__ . '/vendor/autoload.php';
use Spatie\Browsershot\Browsershot;
$url = 'https://example.com';
$output = __DIR__ . '/screenshots/example.jpg';
if (!is_dir(dirname($output))) {
mkdir(dirname($output), 0755, true);
}
Browsershot::url($url)
->setScreenshotType('jpeg', 82)
->windowSize(1440, 900)
->save($output);
echo "Saved {$output}\n";
JPEG quality is a trade-off between detail and file size. Start around 80–85 for web previews and raise it when small text or diagrams show visible compression. JPEG has no transparency and is not ideal for screenshots that contain sharp interface text, flat colors, or line art; use PNG when lossless edges matter. The question here is JPEG, so set the type explicitly instead of relying on a default.
3. Control viewport, density, and output area
Screenshot dimensions are determined by the viewport and the capture mode. Browsershot documents viewport sizing, clipping, element selection, full-page capture, and device scale controls in its image guide.
Viewport screenshots
Browsershot::url('https://example.com')
->setScreenshotType('jpeg', 80)
->windowSize(1366, 768)
->save(__DIR__ . '/viewport.jpg');
A viewport capture is useful for a browser preview or a responsive breakpoint. Pick widths that match your product requirements and keep them fixed in automated jobs.
Full-page screenshots
Browsershot::url('https://example.com')
->setScreenshotType('jpeg', 80)
->windowSize(1440, 900)
->fullPage()
->save(__DIR__ . '/full-page.jpg');
fullPage() expands the capture to the document’s full height. Long pages can create very tall JPEGs, consume more memory, and take longer when images or scripts are lazy-loaded. If the page relies on intersection observers, scroll it or use a readiness strategy before capture so below-the-fold content is present.
Capture one element or a rectangular region
// Element selected by CSS selector
Browsershot::url('https://example.com/pricing')
->setScreenshotType('jpeg', 85)
->select('.pricing-table')
->save(__DIR__ . '/pricing-table.jpg');
// Rectangle in viewport coordinates
Browsershot::url('https://example.com')
->setScreenshotType('jpeg', 85)
->clip(80, 120, 1000, 650)
->save(__DIR__ . '/region.jpg');
Use select when the page has a stable semantic component. Use clip when the coordinates are known and the viewport is fixed. A selector that matches nothing, changes after rendering, or is hidden will cause an element capture to fail or produce an unexpected image.
Increase pixel density
Browsershot::url('https://example.com')
->setScreenshotType('jpeg', 82)
->windowSize(1440, 900)
->deviceScaleFactor(2)
->save(__DIR__ . '/retina.jpg');
A device scale factor of 2 or 3 creates more pixels for the same CSS viewport. It improves detail on high-density displays but increases memory use, processing time, and file size. Keep the factor at 1 unless the consuming system needs a denser image.
4. Wait for JavaScript and lazy content
A screenshot records the rendered browser, not the original HTML response. Client-side components, fonts, API data, and lazy images must be ready before the capture. Prefer a page-specific readiness selector over an arbitrary delay.
Browsershot::url('https://example.com/dashboard')
->setScreenshotType('jpeg', 80)
->windowSize(1440, 900)
->waitForSelector('.dashboard-loaded')
->save(__DIR__ . '/dashboard.jpg');
Choose a selector that is added only after the important data is visible. For pages without a readiness marker, use a short delay or a network-idle strategy supported by your Browsershot version. A delay is less deterministic: a slow API or a blocked third-party script can require more time than expected, while a fast page wastes the remainder.
For full-page captures, make sure lazy images are loaded before the screenshot. A page-specific script that scrolls through the document can trigger lazy loading. Keep that script narrowly scoped and verify that it does not alter the page layout.
5. Return JPEG bytes from a PHP response
You do not have to create a temporary file. Browsershot can return screenshot bytes with screenshot() or a base64 representation with base64Screenshot(). Streaming bytes avoids a write-read-delete cycle for HTTP endpoints.
<?php
require __DIR__ . '/vendor/autoload.php';
use Spatie\Browsershot\Browsershot;
$jpeg = Browsershot::url('https://example.com')
->setScreenshotType('jpeg', 80)
->windowSize(1440, 900)
->screenshot();
header('Content-Type: image/jpeg');
header('Content-Length: ' . strlen($jpeg));
echo $jpeg;
For a Laravel controller, return the bytes with an image/jpeg content type. Set cache headers only when the URL and capture options are safe to cache. If callers can provide URLs, validate and restrict them before passing them to a browser process; unrestricted rendering can expose internal network services or consume excessive resources.
6. Add authentication, cookies, and page-specific setup
Private pages often need cookies or custom headers before they render. Browsershot exposes browser and page configuration methods; use the method names documented for the version installed in your project. A typical flow is:
- Open the target URL.
- Set the required cookies, headers, or authentication state.
- Wait for a selector that proves the authenticated view loaded.
- Capture the viewport, full page, element, or clip.
Keep secrets out of URLs and screenshot filenames. Redact sensitive areas with CSS or capture only a safe element. If a page renders differently by locale, timezone, user agent, or viewport, set those values explicitly and record them with the job so another worker can reproduce the image.
7. Alternative PHP libraries
| Approach | Best fit | Trade-offs |
|---|---|---|
| Spatie Browsershot | Most PHP applications | High-level API; requires Node.js, Puppeteer, and Chromium |
| chrome-php/chrome | Lower-level Chrome control | More control over screenshot options; more browser lifecycle code |
| Raw Puppeteer | Advanced browser-side JavaScript | Requires a Node integration boundary instead of a PHP-only API |
The lower-level chrome-php/chrome library documents JPEG format, quality, clipping, and full-page capture through captureBeyondViewport and a full-page clip. Raw Puppeteer’s Page.screenshot() returns image bytes or base64 according to its options. Choose based on rendering fidelity, deployment complexity, control requirements, and JPEG size targets.
8. Or skip the browser setup
If you want PHP to make one HTTP request instead of installing and operating Chromium, use ScreenshotNeo, a website screenshot API. The API returns PNG, JPEG, WebP, or PDF and supports full-page and element capture, waits, custom CSS and JavaScript, cookies and headers, device presets, dark mode, blocking rules, resizing, caching, signed links, asynchronous jobs, bulk capture, and a usage API. See the ScreenshotNeo API documentation for the complete option list.

For a JPEG response, pass the URL and your access key. The endpoint can be called directly from PHP with cURL:
<?php
$ch = curl_init('https://api.screenshotneo.com/v1/shot?' . http_build_query([
'access_key' => 'YOUR_API_KEY',
'url' => 'https://stripe.com',
'format' => 'jpeg',
]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
]);
$image = curl_exec($ch);
if ($image === false) {
throw new RuntimeException(curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status < 200 || $status >= 300) {
throw new RuntimeException("ScreenshotNeo returned HTTP {$status}");
}
file_put_contents(__DIR__ . '/stripe.jpg', $image);
The equivalent requests are:
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(`HTTP ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', image);
Set the output format to JPEG using the documented format parameter when you need a JPEG file. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and whether the request was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account to get 1,000 screenshots a month without a card.
9. Troubleshooting checklist
Chromium cannot start
Cause: Chromium is missing, its shared libraries are absent, or the process cannot access the configured executable. Fix: install the browser dependencies, verify the executable path, and run the same PHP user and environment used by your worker.
“Node” or “Puppeteer” not found
Cause: PHP is running with a different PATH than your shell, or Puppeteer was installed in another directory. Fix: install dependencies in the deployed application and configure Browsershot with the correct Node and npm paths for that environment.
The JPEG is blank
Cause: the page is still loading, a bot check is displayed, JavaScript failed, or the selected element has no dimensions. Fix: wait for a readiness selector, inspect browser console and network errors, confirm the selector exists, and capture a known visible element as a diagnostic.
Dynamic content is missing
Cause: the screenshot ran before an API request or lazy image completed. Fix: wait for a page-specific selector, use a bounded delay when no selector exists, and ensure full-page workflows trigger lazy loading.
Full-page output is unexpectedly short
Cause: the page height was measured before content expanded, or an application uses an internal scroll container. Fix: wait for the content marker, scroll the relevant container, or capture the container element instead of the document.
Text or images look soft
Cause: a low device scale factor or aggressive JPEG compression. Fix: try deviceScaleFactor(2) and raise quality, then check resulting memory use and file size.
Requests hang in production
Cause: a page waits indefinitely on a third-party resource, a browser process is exhausted, or the URL is unreachable from the server. Fix: set application and browser timeouts, limit concurrency, block unnecessary resource types, and log the URL, viewport, wait condition, and browser error for each job.
10. Performance, reliability, and cost
- Reuse workers carefully: launching Chromium for every request adds startup cost. Queue captures and control concurrency so browser processes do not exhaust CPU or memory.
- Keep captures bounded: full-page, high-density, and very large viewports create more pixels. Capture an element when the requirement is a component image.
- Wait on facts: a readiness selector is usually more predictable than a long fixed sleep. Keep a maximum timeout so broken pages do not occupy workers forever.
- Cache deliberately: cache only when the URL, authentication state, and page data permit it. Include capture options in your cache key.
- Validate inputs: restrict user-supplied URLs and HTML, limit output dimensions, and isolate browser workers from internal services.
- Measure the whole job: record queue delay, browser startup, page load, wait time, screenshot encoding, output bytes, and failure reason. These measurements reveal whether to tune the page or the worker.
With a self-hosted browser, your costs are infrastructure, browser maintenance, and engineering time. With ScreenshotNeo, clean shots are the only billed shots, and the response reports billing status. The free plan includes 1,000 shots each month without a card; paid plans are $5 for 3,000, $15 for 15,000, $39 for 60,000, $99 for 250,000, and $249 for 1,000,000, with two months free on yearly billing.
11. PHP JPEG screenshot FAQ
Can PHP take a screenshot without a browser?
Not for a JavaScript-rendered webpage. PHP can request HTML, but a browser engine is needed to execute JavaScript, apply layout, load fonts, and render the page. Use Browsershot, chrome-php/chrome, or a screenshot API.
Should I use JPEG or PNG?
Use JPEG for smaller photographic or preview images and choose a quality value explicitly. Use PNG when lossless text edges, transparency, or flat interface colors are more important than file size.
How do I screenshot a page after login?
Provide the browser with the required cookies or headers, open the page, wait for an authenticated selector, and capture only after that selector is visible. Never put credentials in a URL or output filename.
Why does full-page capture miss images?
Lazy-loaded images may not request their files until they approach the viewport. Trigger lazy loading by scrolling or use a page-specific readiness script before calling fullPage().
Can I return the JPEG directly from Laravel?
Yes. Call Browsershot’s byte-returning screenshot method and return the result with an image/jpeg content type, or save the file and return a streamed response.


