ScreenshotNeo

BlogHow-to

Why PHP Bash Scripts Return Black Screenshots and How to Fix Them

Find the cause of black screenshots in PHP and Bash by checking display access, execution context, ImageMagick policy, resources, and transparency.

By the ScreenshotNeo team1 October 20269 min read

A black screenshot usually means one layer of the capture pipeline failed: the process cannot access the expected display, PHP runs with a different environment than your terminal, ImageMagick rejects or limits the operation, or a valid transparent image is being displayed on black. First identify whether you are capturing a desktop surface or rendering a URL/document. Then run the exact command as the PHP worker user, preserve stderr and the exit status, set the output format and background explicitly, and validate the resulting file before serving it.

1. Identify what “black screenshot” means

There are three different failures that look similar:

  • Empty or invalid output: the file is zero bytes, truncated, or not an image. This points to command, permission, policy, path, or resource failure.
  • Valid image with black pixels: the renderer produced an image, but it captured an unavailable display, a blank browser surface, or an image whose transparency was composited against black.
  • Correct file that looks black in one viewer: the image may have an alpha channel or color-profile/display difference. ImageMagick notes that the same color image can look different on different workstations.

Check the file before changing commands:

file /absolute/path/shot.png
identify -verbose /absolute/path/shot.png | head -80
wc -c /absolute/path/shot.png

A nonzero size and valid dimensions do not prove that the pixels are useful, but they separate rendering problems from file-generation failures.

2. Desktop capture: why PHP sees a black or useless screen

PHP’s imagegrabscreen() captures the current screen and only the primary display. The PHP manual explicitly warns that it does not capture all monitors. A web request normally runs in a service account, without the interactive desktop session, display socket, authorization cookie, or GPU context available in your terminal. The function can therefore capture the wrong surface or no useful surface.

Compare the interactive and PHP environments

From the terminal, record the values that identify the display and executable paths:

printf 'user=%s\npwd=%s\nDISPLAY=%s\nWAYLAND_DISPLAY=%s\nXAUTHORITY=%s\nPATH=%s\n' \\
  "$(id -un)" "$PWD" "$DISPLAY" "$WAYLAND_DISPLAY" "$XAUTHORITY" "$PATH"
command -v php bash magick convert import gnome-screenshot scrot
php -v

Expose the same information temporarily from PHP (remove it after diagnosis):

<?php
header('Content-Type: text/plain');
echo 'user: ' . get_current_user() . PHP_EOL;
echo 'cwd: ' . getcwd() . PHP_EOL;
foreach (['DISPLAY', 'WAYLAND_DISPLAY', 'XAUTHORITY', 'PATH', 'HOME'] as $name) {
    echo $name . ': ' . ($_SERVER[$name] ?? getenv($name) ?: '(unset)') . PHP_EOL;
}
echo 'php: ' . PHP_BINARY . PHP_EOL;

Run the capture as the same account that owns the PHP worker. For a systemd service this is often a dedicated web user. Use absolute paths and an explicit working directory:

sudo -u www-data env DISPLAY=:0 XAUTHORITY=/home/capture/.Xauthority \\
  /usr/bin/bash -lc 'cd /srv/capture && /usr/bin/magick import -window root /tmp/test.png'

Adapt the account, display, and authorization file to your host. Do not copy values from your login shell unless that desktop session is the one the service is allowed to access.

PHP desktop capture example

<?php
$out = '/srv/capture/shot.png';
if (!function_exists('imagegrabscreen')) {
    throw new RuntimeException('GD screen capture is unavailable');
}
$im = imagegrabscreen();
if ($im === false) {
    throw new RuntimeException('No screen was captured');
}
if (!imagepng($im, $out)) {
    throw new RuntimeException('Could not write ' . $out);
}
imagedestroy($im);
if (!is_file($out) || filesize($out) === 0) {
    throw new RuntimeException('Output is empty');
}

This only works where PHP can access a supported desktop session. It is not a reliable way to render a remote URL from a headless web server.

3. Bash capture: make the command reproducible

Capture stdout, stderr, status, and the final path. A terminal command can appear to work because your shell supplies PATH, HOME, DISPLAY, and authentication that PHP does not.

#!/usr/bin/env bash
set -u
out=/srv/capture/shot.png
log=/srv/capture/shot.log
mkdir -p "$(dirname "$out")"
/usr/bin/magick import -window root "$out" >"$log" 2>&1
status=$?
printf 'exit=%s\nfile=%s\nbytes=%s\n' "$status" "$out" "$(stat -c %s "$out" 2>/dev/null || echo 0)" >>"$log"
exit "$status"

ImageMagick 7 uses magick as its primary command. Older distributions may provide legacy commands such as convert; verify which binary and version the PHP process actually invokes.

4. URL, PDF, SVG, and existing-image rendering

If you are rendering a URL or document rather than the desktop, remove display capture from the investigation. Check the browser or delegate input first, then ImageMagick output. A headless browser needs its own executable, writable temporary directory, and enough memory; it does not need an interactive desktop if run in a supported headless mode.

Render with an explicit format

/usr/bin/magick input.pdf[0] -background white -alpha remove -alpha off output.jpg
/usr/bin/magick input.svg -background white -alpha remove -alpha off output.png

Set the format before writing when using PHP Imagick:

<?php
$im = new Imagick('/srv/input/source.png');
$im->setImageFormat('png');
$im->writeImage('/srv/output/shot.png');
$im->clear();
$im->destroy();

Imagick is a PHP extension, while ImageMagick’s command-line binaries and configuration are separate installation layers. Installing one does not automatically install or configure the other.

5. Transparency: a valid image can look black

PNG, SVG, and PDF content can contain transparent pixels. Some viewers show transparency as black, and converting to JPEG without choosing a background can produce black areas. Flatten explicitly:

/usr/bin/magick input.png -background white -alpha background -alpha remove -alpha off output.jpg

For a transparent PNG, preserve alpha instead:

/usr/bin/magick input.png -alpha on output.png

Inspect alpha and colors:

identify -format 'format=%m size=%wx%h channels=%[channels] mean=%[mean]\n' shot.png

Choose the output format intentionally: JPEG has no transparency; PNG and WebP can preserve it depending on the encoder settings.

6. ImageMagick policy and resource limits

ImageMagick can deny coders, delegates, paths, or operations through policy.xml. Resource controls can stop processing because of pixel area, memory, disk, file count, thread count, or time. A black or missing output is sometimes the visible symptom of a policy error printed only to stderr.

magick -list policy
magick -list resource
magick -debug Policy,Resource input.pdf output.png 2>imagemagick-debug.log

Read the log for messages such as “not authorized,” “area limit,” “cache resources exhausted,” or delegate failures. Change policy only when you understand the security impact and the workload. Keep least privilege and avoid broad permissions for untrusted files.

7. A PHP diagnostic wrapper that preserves the evidence

<?php
$cmd = ['/usr/bin/magick', 'input.png', '-background', 'white', '-alpha', 'remove', '-alpha', 'off', 'output.jpg'];
$escaped = array_map('escapeshellarg', $cmd);
$descriptor = [
    1 => ['pipe', 'w'],
    2 => ['pipe', 'w'],
];
$process = proc_open(implode(' ', $escaped), $descriptor, $pipes, '/srv/capture');
if (!is_resource($process)) {
    throw new RuntimeException('Could not start ImageMagick');
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$status = proc_close($process);
error_log(json_encode([
    'status' => $status,
    'stdout' => $stdout,
    'stderr' => $stderr,
    'cwd' => getcwd(),
    'path' => getenv('PATH'),
]));
if ($status !== 0) {
    throw new RuntimeException('ImageMagick failed; inspect stderr');
}

Use argument arrays or carefully escaped arguments. Never concatenate untrusted URLs or file names into a shell command.

8. Validate before returning the screenshot

Do not send a file merely because the command exited. Confirm that it exists, is nonempty, has expected dimensions and MIME type, and contains more than a uniform black frame when your use case requires visible content.

$path = '/srv/output/shot.png';
if (!is_file($path) || filesize($path) === 0) {
    http_response_code(500);
    exit('capture failed');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($path);
$allowed = ['image/png', 'image/jpeg', 'image/webp'];
if (!in_array($mime, $allowed, true)) {
    http_response_code(500);
    exit('unexpected image type');
}
[$width, $height] = getimagesize($path);
if ($width < 1 || $height < 1) {
    http_response_code(500);
    exit('invalid dimensions');
}
header('Content-Type: ' . $mime);
readfile($path);

The Imagick project recommends checking that image processing produced a valid image before displaying it. Validate magic bytes and MIME type when accepting uploaded or remote content.

9. Common errors and fixes

Symptom Likely cause Fix
Works in terminal, black from PHP Different user, PATH, DISPLAY, XAUTHORITY, HOME, or working directory Run as the PHP account; log environment values; use absolute paths.
imagegrabscreen() returns false GD capture unavailable or no accessible desktop Check the extension and session; use a headless browser or API for URL capture.
Only one monitor appears PHP captures the primary display Select the needed display with a capture tool or capture each surface separately.
command not found: magick ImageMagick 7 is absent or not on the service PATH Install/configure the package and use its absolute path; verify the version.
“not authorized” or coder denied policy.xml restriction Inspect magick -list policy; allow only the required operation.
“cache resources exhausted” Area, memory, disk, file, thread, or time limit Inspect resource limits, reduce input size, or set an appropriate controlled limit.
JPEG has black background Transparent source flattened without a background Use -background white -alpha remove -alpha off.
Zero-byte or truncated file Write permission, process termination, full disk, or failed delegate Check exit status, stderr, directory ownership, disk space, and logs.
Image is valid but appears black in one viewer Alpha or display/color interpretation Inspect channels; flatten for JPEG or view with an alpha-aware viewer.

10. Reliability, performance, and security

  • Separate capture from conversion. Save the first renderer output, validate it, then convert. This shows which layer introduced the failure.
  • Use bounded work. Set process timeouts, limit input dimensions, and monitor temporary disk and memory usage.
  • Keep diagnostics. Record command version, exit status, stderr, dimensions, MIME type, and a request identifier.
  • Expect GPU cost. PHP documents that GPU-intensive screen capture can cause significant lag. Queue expensive jobs instead of blocking a web request.
  • Use least privilege. Run the worker with only the files, display socket, and binaries it needs. Validate magic bytes and never serve untrusted uploads directly.
  • Make output deterministic. Set viewport, background, format, color handling, and fonts explicitly when rendering documents.

11. Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. One request returns PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted and 60+ known consent platforms, newsletter popups, and chat widgets are removed before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. AI agents can capture through its MCP tools: take_screenshot, get_page_info, and capture_pdf.

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)
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}`);

There are 1,000 screenshots a month free with no card. Paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

12. FAQ

Can PHP capture a browser window on a headless server?

Not with imagegrabscreen() unless a supported desktop session is available to that process. Use a headless browser renderer or a screenshot API for URL capture.

Should I use convert or magick?

Check the installed ImageMagick version. ImageMagick 7’s primary command is magick; older packages may expose legacy names.

Why does PNG look fine but JPEG look black?

PNG can retain transparency. JPEG cannot, so flatten against an explicit background before writing it.

Is a nonzero exit code the only failure signal?

No. A command can exit successfully and still produce a blank or transparent image. Validate dimensions, MIME type, channels, and pixels.

Can changing policy.xml solve every black screenshot?

No. Policy addresses authorization and limits. It cannot provide a missing display session or repair an incorrect alpha/compositing choice.