PHP has three practical paths to PDF-to-JPG: Imagick (the cleanest), Ghostscript via shell_exec (the most widely installed), or the ChangeThisFile API (zero local dependencies). Imagick is the right default for any server that has ImageMagick — which most shared hosting and VPS images include. GD won't read PDFs without GS under the hood anyway. The API is the no-install escape hatch.
Method 1: Imagick (ImageMagick PHP extension)
Imagick wraps ImageMagick and can render PDF pages directly. It delegates to Ghostscript internally, but you don't have to call GS yourself.
# Verify Imagick is installed
php -r "echo extension_loaded('imagick') ? 'ok' : 'not found';"pingImage($pdfPath);
$pageCount = $imagick->getNumberImages();
$imagick->clear();
$paths = [];
for ($i = 0; $i < $pageCount; $i++) {
$page = new Imagick();
$page->setResolution($dpi, $dpi);
$page->readImage("{$pdfPath}[{$i}]");
$page->setImageFormat('jpeg');
$page->setImageCompressionQuality(90);
$page->setColorspace(Imagick::COLORSPACE_SRGB);
$outPath = rtrim($outDir, '/') . '/page-' . str_pad($i + 1, 3, '0', STR_PAD_LEFT) . '.jpg';
$page->writeImage($outPath);
$page->clear();
$paths[] = $outPath;
}
return $paths;
}
$pages = pdfToJpg('document.pdf', './pages', 150);
echo sprintf("Wrote %d pages\n", count($pages));
Key settings:
- setResolution() must come before readImage() — Imagick reads at native resolution if you set DPI after loading.
- COLORSPACE_SRGB — prevents weird color shifts on PDFs with CMYK content.
- quality(90) — sweet spot for web use; 85 is also fine for thumbnails.
- [N] suffix — loads a specific zero-indexed page. readImage('file.pdf') without a suffix loads ALL pages at once, which can OOM on large PDFs.
Method 2: GD + Ghostscript (shell_exec)
PHP's GD extension can't read PDFs. But if Ghostscript is installed, you can use shell_exec to render pages to PNG, then load with GD for further processing.
# Check Ghostscript
gs --version&1";
$output = shell_exec($cmd);
// Convert PNGs to JPG using GD
$pngs = glob($outDir . '/page-*.png');
sort($pngs);
$paths = [];
foreach ($pngs as $pngPath) {
$img = imagecreatefrompng($pngPath);
if (!$img) continue;
$jpgPath = str_replace('.png', '.jpg', $pngPath);
// Fill white background (PNGs from GS may have transparency)
$bg = imagecreatetruecolor(imagesx($img), imagesy($img));
imagefill($bg, 0, 0, imagecolorallocate($bg, 255, 255, 255));
imagecopy($bg, $img, 0, 0, 0, 0, imagesx($img), imagesy($img));
imagejpeg($bg, $jpgPath, 90);
imagedestroy($img);
imagedestroy($bg);
unlink($pngPath);
$paths[] = $jpgPath;
}
return $paths;
}
$pages = pdfToJpgViaGs('document.pdf', './pages', 150);
echo sprintf("Wrote %d pages\n", count($pages));
The -dSAFER flag restricts Ghostscript's filesystem access — always include it when processing user-supplied PDFs. This approach is slower than Imagick (two passes: GS render + GD encode) but works on any server with Ghostscript.
Method 3: ChangeThisFile API (Guzzle, no installs)
No Imagick, no Ghostscript? The ChangeThisFile API runs Poppler server-side. Free tier: 1,000 conversions/month, no card required. The endpoint is the SDK — one POST with your file and target format.
composer require guzzlehttp/guzzle 120]);
$response = $client->post('https://changethisfile.com/v1/convert', [
'headers' => ['Authorization' => 'Bearer ' . $apiKey],
'multipart' => [
['name' => 'file', 'contents' => fopen($pdfPath, 'r'), 'filename' => basename($pdfPath)],
['name' => 'target', 'contents' => 'jpg'],
],
]);
$contentType = $response->getHeader('Content-Type')[0] ?? '';
$body = $response->getBody()->getContents();
// Multi-page PDFs return a zip
if (str_contains($contentType, 'application/zip')) {
$zipPath = sys_get_temp_dir() . '/ctf_pages.zip';
file_put_contents($zipPath, $body);
$zip = new ZipArchive();
$zip->open($zipPath);
$zip->extractTo($outDir);
$zip->close();
unlink($zipPath);
$paths = glob($outDir . '/*.jpg');
sort($paths);
return $paths;
}
// Single page
$outPath = rtrim($outDir, '/') . '/page-001.jpg';
file_put_contents($outPath, $body);
return [$outPath];
}
$pages = pdfToJpgApi('document.pdf', './pages', 'ctf_sk_your_key_here');
echo sprintf("Wrote %d pages\n", count($pages));
No Guzzle? Use PHP's native curl instead:
true,
CURLOPT_TIMEOUT => 120,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'file' => new CURLFile($pdfPath, 'application/pdf', basename($pdfPath)),
'target' => 'jpg',
],
]);
$result = curl_exec($ch);
curl_close($ch);
return $result; // binary JPG or ZIP
}
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| Imagick | Servers with ImageMagick installed, multi-page PDFs | Needs php-imagick extension + ImageMagick + Ghostscript |
| GD + Ghostscript | Servers where Imagick is unavailable but GS is installed | Two-pass (slower), shell_exec must be enabled |
| ChangeThisFile API | Zero local deps, shared hosting, serverless PHP | Network latency, 25MB file limit on free tier |
Production tips
- Always set resolution before readImage() in Imagick. Setting DPI after loading the page has no effect — the page was already rasterized at 72 DPI.
- Process pages one at a time for large PDFs. Loading all pages at once via readImage('file.pdf') can exhaust PHP's memory_limit. Loop through page indices instead.
- Increase PHP memory_limit for high-DPI rendering. A 300 DPI A4 page takes ~25MB in memory. Set memory_limit = 256M in php.ini or ini_set() at the top of your script.
- Use set_time_limit(0) for multi-page PDFs. A 50-page PDF at 200 DPI can take 30+ seconds. PHP's default 30s limit will kill it.
- Sanitize filenames from user uploads. Never pass user-supplied filenames directly to shell_exec or Imagick's readImage. Use basename() and strip non-alphanumeric characters before constructing paths.
- Check Imagick's policy.xml. Some distros ship ImageMagick with PDF reading disabled (rights="none") for security. Edit /etc/ImageMagick-7/policy.xml and change the PDF pattern's rights to "read|write".
For most PHP stacks, Imagick is the right call — it's likely already installed and handles multi-page PDFs cleanly. For minimal server environments, the API needs nothing beyond curl. Get a free API key covering 1,000 conversions/month.