Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Pdf To Image Laravel Package

spatie/pdf-to-image

Convert PDF pages to images with this simple PHP class built on Imagick and Ghostscript. Save as jpg, jpeg, png, or webp, render any page, and check page counts. Requires PHP 8.2+.

View on GitHub
Deep Wiki
Context7

Getting Started

System prerequisites come first — the package has no Laravel service provider, facade, or config file. It is a plain PHP library that shells out to Ghostscript through the Imagick extension, so the install is half Composer, half OS packages.

# Debian/Ubuntu host or Docker image
apt-get install -y ghostscript imagemagick libmagickwand-dev
pecl install imagick && docker-php-ext-enable imagick

composer require spatie/pdf-to-image

Confirm the two hard dependencies before writing any application code:

gs --version                      # Ghostscript must exist
php -m | grep imagick             # Imagick extension must be loaded
php -r "var_dump(class_exists('Imagick'));"

Where to look first

  1. Spatie\PdfToImage\Pdf — the entire public API lives on this one class. Read it top to bottom; it is small.
  2. Spatie\PdfToImage\Enums\OutputFormat — backed enum of allowed output formats (Jpg, Png, Webp, …). This is the source of truth, not the README prose.
  3. Spatie\PdfToImage\DTOs\PageSize — returned by getSize(), useful for guarding against absurd page dimensions before you render.
  4. The v3.0.0 changelog entry — it lists the breaking renames from v2 (saveImage → save, setPage → selectPage, getNumberOfPages → pageCount). Most blog posts and Stack Overflow answers you will find online use the v2 names and will silently fail to autoload.

First use case: a PDF preview thumbnail on upload

The canonical Laravel flow is "user uploads a PDF, we show a preview of page 1 in the admin table." That is ~5 lines:

use Spatie\PdfToImage\Pdf;

$pdf = new Pdf($request->file('document')->getRealPath());

$path = storage_path('app/previews/'.$document->id.'.jpg');

$pdf->selectPage(1)
    ->thumbnailSize(400)        // width 400px, height auto
    ->quality(85)
    ->save($path);

$document->update(['preview_path' => 'previews/'.$document->id.'.jpg']);

Two things to internalise immediately, because they shape everything downstream:

  • The target directory must already exist. save() will not create it, so Storage::makeDirectory() or File::ensureDirectoryExists() first.
  • save() returns a string for one page and an array<string> for multiple pages. Never assume one or the other without normalising.

A sensible first-day smoke test: convert the same fixture PDF at resolution(72) and resolution(300), open both, and confirm you understand that resolution() changes rendering DPI while size() / thumbnailSize() change output pixel dimensions. They compose; they are not interchangeable.

Implementation Patterns

Wrap it in a single action class. Because there is no config, no DI registration, and no facade, the package should appear in exactly one place in your codebase. A dedicated action keeps the mutation-heavy fluent object contained and gives you one seam to swap later (e.g. to a pdftoppm implementation if Ghostscript becomes a problem).

namespace App\Actions;

use Spatie\PdfToImage\Pdf;
use Spatie\PdfToImage\Enums\OutputFormat;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class RenderPdfPage
{
    /** @return string[] absolute paths of written files */
    public function handle(
        string $pdfPath,
        string $diskDir,
        array $pages = [1],
        int $width = 800,
        OutputFormat $format = OutputFormat::Jpg,
        int $quality = 85,
    ): array {
        Storage::disk('local')->makeDirectory($diskDir);

        $pdf = (new Pdf($pdfPath))
            ->resolution(150)
            ->thumbnailSize($width)
            ->quality($quality)
            ->format($format)
            ->backgroundColor('white');   // neutralises transparent PDF backgrounds

        $created = [];

        foreach ($pages as $page) {
            $target = storage_path("app/{$diskDir}/".Str::uuid().'.'.strtolower($format->name));
            $pdf->selectPage($page)->save($target);
            $created[] = $target;
        }

        return $created;
    }
}

Looping one page at a time rather than calling selectPages() gives you control over filenames — the package generates its own naming when handed a directory, which is rarely what you want for a documents/{id}/page-{n}.jpg layout.

Always run conversion in a queued job. A 50-page PDF at 300 DPI will occupy a worker for seconds to minutes and can consume hundreds of MB. In an HTTP request this is a timeout waiting to happen.

class GeneratePdfPreview implements ShouldQueue
{
    public int $timeout = 300;
    public int $tries = 3;
    public int $backoff = 30;

    public function __construct(public int $documentId) {}

    public function handle(RenderPdfPage $render): void
    {
        $document = Document::findOrFail($this->documentId);

        $files = $render->handle(
            path: Storage::disk('local')->path($document->source_path),
            diskDir: "previews/{$document->id}",
            pages: range(1, min($document->page_count, 5)),
        );

        $document->update(['preview_paths' => $files, 'rendered_at' => now()]);
    }

    public function failed(Throwable $e): void
    {
        // Imagick surfaces almost everything as ImagickException — log it verbatim.
        report($e);
    }
}

Give the job a unique constraint (WithoutOverlapping on the document id) so re-uploads and retries cannot race on the same output directory.

Decide the render strategy from getSize() and pageCount() first.

$pdf  = new Pdf($path);
$size = $pdf->getSize();          // DTOs\PageSize: ->width, ->height (in points)

// Ultra-wide or enormous pages blow past ImageMagick's default resource limits.
if ($size->width > 5000 || $pdf->pageCount() > 100) {
    $pdf->resolution(72)->thumbnailSize(600);
} else {
    $pdf->resolution(200)->thumbnailSize(1200);
}

pageCount() uses pingImage() rather than a full read, so it is cheap — but treat it as advisory. It has historically misreported on malformed files, so wrap the call in a try/catch and fall back to ImagickException handling rather than trusting it blindly.

High-quality downscaling: render big, then shrink. Ghostscript's output at low DPI is genuinely blurry soft text. The reliable pattern for crisp thumbnails is resolution(300)->size($targetWidth): Ghostscript rasters at 300 DPI and Imagick downsamples to your target, which reads far better than resolution(72)->size(...).

Caching: key on content hash, not path. Renders are deterministic for a given (file, page, resolution, size, quality) tuple, so a sha1_file($path) prefix on the output directory makes re-renders free.

$key = sha1_file($pdfPath)."-{$page}-{$width}-{$quality}";

Password-protected PDFs are supported, but only on ImageMagick 7:

if (Pdf::supportsPasswordProtectedPdfs()) {
    $pdf->password($decryptedPassword)->save($target);
} else {
    throw new UnsupportedPdfException('Encrypted PDFs require ImageMagick 7.');
}

Gate on supportsPasswordProtectedPdfs() before attempting, otherwise you get a PasswordNotSupported exception from password() itself on IM6.

Transparent and layered PDFs — the two knobs that fix most "renders black" or "renders blank" complaints:

$pdf->backgroundColor('white')                              // or '#fff' / 'rgb(255,255,255)'
    ->layerMethod(\Spatie\PdfToImage\Enums\LayerMethod::Merge);

Use LayerMethod::None only when you deliberately want to skip flattening; Merge is the safe default for annotated or form-bearing PDFs.

OCR / text extraction handoff. If you are feeding Tesseract, render at 300 DPI PNG and skip thumbnailSize() entirely — resizing destroys OCR accuracy. Treat the image pipeline and the OCR pipeline as separate passes with separate output directories.

Testing. There is no Pdf::fake(). Test against a small committed fixture and skip when the host lacks the binaries:

it('renders page one', function () {
    if (! Pdf::isValidOutputFormat('jpg') || trim(shell_exec('which gs') ?? '') === '') {
        $this->markTestSkipped('Ghostscript unavailable.');
    }

    $out = sys_get_temp_dir().'/'.uniqid().'.jpg';
    (new Pdf(__DIR__.'/fixtures/sample.pdf'))->selectPage(1)->size(200)->save($out);

    expect(file_exists($out))->toBeTrue();
    expect(getimagesize($out)[0])->toBe(200);
});

Pin this to a CI job with Ghostscript installed; running it in the main suite on developer machines causes false red builds.

Gotchas and Tips

save()'s polymorphic return value is the #1 footgun. Single page → string; multiple → array. Any code path that can produce both must normalise:

$result = $pdf->save($dir);
$files  = is_array($result) ? $result : [$result];

Output format is inferred from the filename extension. jpg, jpeg, png, webp are honoured; anything else silently falls back to jpg. That means save('/tmp/page.PNG') — uppercase — is a format question, not a certainty. Set ->format(OutputFormat::Png) explicitly and match the extension yourself. Also verify WebP is actually compiled into your ImageMagick build (convert -list format | grep WEBP); on minimal Docker images it often is not, and you get a JPG with a .webp name.

FailedToExecuteCommand 'gs' under FPM but not CLI is the classic deployment failure. PHP-FPM scrubs PATH, so Ghostscript is invisible to the web process even though artisan tinker works fine. Fix in php-fpm.conf:

env[PATH] = /usr/local/bin:/usr/bin:/bin

Restart FPM. This wastes hours for people who test only via the CLI, so verify the FPM path explicitly during staging.

attempt to perform an operation not allowed by the security policy 'PDF' — modern ImageMagick ships with PDF reading disabled by default for security reasons. Edit /etc/ImageMagick-7/policy.xml:

<policy domain="coder" rights="read | write" pattern="PDF" />

This is a host-level change, not something the package can work around. Bake it into your Dockerfile so it is reproducible.

Ultra-wide PDFs need resource policy bumps. Default width/height resource limits reject very large pages. Raise them in the same policy.xml:

<policy domain="resource" name="width" value="4GiB"/>
<policy domain="resource" name="height" value="4GiB"/>

Do not use 4GiB casually in a multi-tenant app — a malicious PDF plus a generous policy is a memory-exhaustion vector. Cap your own input dimensions with getSize() first.

The Pdf object is mutable and stateful across calls. selectPage(2) persists on the instance; a subsequent save() still writes page 2. If you reuse one instance in a loop (or inject it as a singleton — don't), you will silently render the wrong page. Construct fresh instances per file, or at minimum re-select the page before every save().

Pages are 1-based, and page 0 or negative throws. selectPage(1) is the first page. The package throws on negative numbers rather than clamping, so validate user-supplied page numbers against pageCount() before passing them through — especially if they arrive from a URL parameter.

Directory must exist, and errors are Imagick-flavoured. Missing directory produces a low-level Imagick write failure, not a friendly "directory not found". Wrap conversion in try/catch, catch ImagickException broadly, and translate to a domain exception:

try {
    $pdf->save($target);
} catch (\ImagickException $e) {
    throw new PdfRenderFailed("Rendering {$target} failed: {$e->getMessage()}", previous: $e);
}

Remote URLs are gone in v3. v1.x allowed new Pdf('https://...'); v3 dropped it deliberately for SSRF reasons. Download to a local temp path with Http::get(...)->body() and write it yourself. Any tutorial showing a URL constructor argument is pre-v3.

No config, no container binding, no extension points. There is no published config file, no interface to implement, and no events. Your extension surface is limited to (a) decorating the class behind your own interface, or (b) subclassing Pdf. If you need multiple output profiles, define them in your own config/pdf.php and map them to fluent calls in your action — the package will never read your config for you.

Watch the version boundary in tutorials. v3 renamed a large slice of the API. saveImage, setPage, setResolution, setCompressionQuality, getNumberOfPages, saveAllPagesAsImages all no longer exist. saveAllPages() and pageCount() replace them. Composer will not warn you when a copied snippet's method is missing — you get a generic BadMethodCallException at runtime.

Concurrency. There is no locking inside the package. Two workers rendering the same source PDF to the same target directory will interleave writes. Use WithoutOverlapping on the job, or write to a unique temp directory and move atomically on success.

Windows dev machines frequently can't find gs.exe on PATH at all. If your team is mixed-platform, containerise the render step or gate the feature behind a service interface so Windows developers can stub it — otherwise local test suites diverge from CI.

Maturity signal. The package is stable, MIT, actively maintained by Spatie, and the v3 rewrite raised test coverage substantially — but the Dependents: 0 metadata and absence of any Laravel integration layer mean you should treat it as a low-level utility you own, not a framework citizen. Budget half a day for the infrastructure work (Ghostscript in the image, policy.xml, FPM PATH, CI fixture job) before you write the first line of business logic; that infrastructure, not the PHP API, is where this package's real cost sits.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
encore2/laravel-admin2
cboxdk/laravel-console-kit
shapecode/doctrine-dbal-datetimeutc
gilads-otiannoh254/inertia-protocol
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite
faghy/laracashbook
roundly-consulting/google-places-for-laravel
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
adachsoft/cli-process-tester
adachsoft/changelog-linter
adachsoft/collection
adachsoft/php-code-style
adachsoft/api-version-bundle
cgv/localized-resource