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

Pdfjs Viewer Bundle Laravel Package

jjalvarezl/pdfjs-viewer-bundle

A Symfony bundle that integrates Mozilla PDF.js to embed and display PDF documents in your application. It provides a ready-to-use viewer, simple configuration, and a clean way to serve PDFs without browser plugins.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check. This is a Symfony 4 bundle, not a Laravel package. There is no Jjalvarezl\PDFjsViewerBundle\PDFjsViewerServiceProvider, no auto-discovery entry in composer.json, no facade, and no publishable config tag. composer require jjalvarezl/pdfjs-viewer-bundle in a Laravel app will install the code and do absolutely nothing with it — Laravel will not read DependencyInjection/Extension.php, will not register the routes, and will not load the Twig templates.

So the day-one decision is which of two things you actually want:

  1. The PDF.js assets — a prebuilt copy of Mozilla's viewer bundled into Resources/public/. This is the part that has value to a Laravel developer.
  2. The Symfony integration — a controller, a route, and a Twig template. Throw this away; it's ~50 lines and reimplementing it in Laravel is faster than porting it.

Minimal path in a Laravel app:

composer require --dev jjalvarezl/pdfjs-viewer-bundle
# copy the viewer build out of the vendor dir
cp -R vendor/jjalvarezl/pdfjs-viewer-bundle/Resources/public/* public/vendor/pdfjs/
composer remove --dev jjalvarezl/pdfjs-viewer-bundle

Then a stream route and an iframe:

// routes/web.php
Route::get('/documents/{document}/raw', [DocumentController::class, 'raw'])
    ->middleware('auth')->name('documents.raw');
public function raw(Document $document)
{
    abort_unless($document->exists(), 404);

    return response()->file(
        Storage::disk('local')->path($document->path),
        ['Content-Type' => 'application/pdf', 'Content-Disposition' => 'inline']
    );
}
{{-- resources/views/documents/show.blade.php --}}
<iframe
    src="{{ asset('vendor/pdfjs/web/viewer.html') }}?file={{ urlencode(route('documents.raw', $document)) }}"
    style="width:100%; height:80vh; border:0"
    title="{{ $document->title }}"
></iframe>

Where to look first if you do want to read the bundle rather than skim it: Resources/config/ (the service and route definitions), DependencyInjection/Configuration.php (the full config surface, which maps cleanly onto a Laravel config/pdfjs.php), Resources/views/ (the Twig template you'll translate to Blade), and Resources/public/ (the asset snapshot, and the thing to check the version of immediately).

Best first use case: authenticated PDF preview inside an admin panel or client portal — invoices, contracts, generated reports. That's where an embedded viewer beats target="_blank" links, and it's a contained enough surface that a 2018-era PDF.js build won't hurt you.

Implementation Patterns

Iframe embed over JS API. The bundle's viewer is the full PDF.js viewer application, not the low-level pdfjsLib API. Use it as an iframe with a file= query parameter. Reaching into the library directly (pdfjsLib.getDocument(...) + a custom canvas) means you're writing your own UI and getting none of the bundle's value — at which point just install pdfjs-dist from npm and skip all of this.

Signed URLs are the correct auth pattern, not Authorization headers. The viewer fetches the PDF via XHR/fetch, and an <iframe> cannot attach custom headers. Session cookies work same-origin, but for anything cross-origin or shared, use URL::temporarySignedRoute:

$url = URL::temporarySignedRoute('documents.raw', now()->addMinutes(15), ['document' => $document]);
<iframe src="{{ asset('vendor/pdfjs/web/viewer.html') }}?file={{ urlencode($url) }}"></iframe>

This gives you expiry, no session coupling, and works from a CDN-hosted viewer build.

Wrap it in a Blade component so the viewer path, height, and URL-encoding live in one place:

// app/View/Components/PdfViewer.php
public function render()
{
    $this->src ??= URL::temporarySignedRoute('documents.raw', now()->addMinutes(15), ['document' => $this->document]);
    return view('components.pdf-viewer');
}
{{-- resources/views/components/pdf-viewer.blade.php --}}
<iframe
    src="{{ config('pdfjs.viewer_path', 'vendor/pdfjs/web/viewer.html') }}?file={{ urlencode($src) }}#{{ $fragment }}"
    style="width:100%; height:{{ $height }}"
></iframe>

Mirror the bundle's config into config/pdfjs.php. The Symfony bundle exposes options like viewer path, default file, locale, and toolbar/theme settings. Recreate that surface as a flat config file and read it from the component; it keeps the port honest and gives you one place to pin a newer PDF.js build later.

Range requests matter. Laravel's response()->file() returns a BinaryFileResponse, which handles Accept-Ranges and 206 Partial Content correctly. Don't reach for Storage::download() (forces Content-Disposition: attachment) or manually readfile() a 200 — a 40 MB scanned PDF will load the whole thing before the first page paints.

Fragment for initial navigation. PDF.js reads #page=N&zoom=page-width from the URL fragment, not the query string. Append it after your encoded file= param; the server never sees it, so you can't validate it, but it's the clean way to deep-link to a signature page.

Porting the Symfony bits (only if you run Symfony too). In a Symfony app: register in config/bundles.php, import @JjalvarezlPDFjsViewerBundle/Resources/config/routing.xml, and the bundle's route serves the PDF through its own controller. The bundle-era Configuration class may emit deprecation notices on Symfony 5+; the flex recipe era has passed it by.

Asset placement. Serve the viewer from public/ so it goes through your web server, not Laravel — a few hundred static files don't need to hit PHP. If you front with CloudFront/Cloudflare, put vendor/pdfjs/* on a long Cache-Control, and version the directory (vendor/pdfjs/2.16.105/) so upgrades don't poison caches.

Gotchas and Tips

Do not ship the 2018 build to production. The bundle's last release is March 2018, which pins roughly PDF.js 1.10 / early 2.0. That's ~7 years and several CVEs of PDF.js behind current — old versions have had XSS and memory-safety issues in font/JPEG2000/image decoding, which matters enormously when the PDFs are user-uploaded. Vendor the pattern from this bundle, then download a current pdfjs-dist legacy build for the actual assets. Track it: viewer.html → viewer.mjs/viewer.mjs-wrapped builds changed around 4.x, so don't blindly swap files.

CSP will bite you. PDF.js spawns a web worker from a blob: URL and, depending on build, may use eval-ish constructs. You'll need worker-src 'self' blob: and possibly script-src ... 'unsafe-eval' — scope these to the viewer path via a separate CSP header rather than loosening the app-wide policy.

URL encoding of nested query strings is the #1 silent failure. ?file= takes a full URL with its own ? and & (signature, expiry). One missed urlencode() and the signature verification fails with a 403 that looks like an auth bug. Symptom: works in the network tab when opened directly, blank viewer in the iframe.

SameSite on the session cookie. If you go the cookie-auth route instead of signed URLs, SameSite=Lax blocks cookies in cross-site iframes; you need SameSite=None; Secure. This is exactly the class of bug that shows up only in an embedded CRM/portal context.

Don't cache the PDF response blindly. After a re-upload or regeneration, a Cache-Control: max-age=3600 on the raw stream serves stale bytes. Emit a strong ETag (file hash or updated_at) and let BinaryFileResponse handle conditional requests.

Debugging checklist, in order: open viewer.html?file=<url> directly in a tab (isolates iframe/CSP from auth); check the network panel for a 200 vs 206 on the PDF (no 206 means range handling is broken); check the console for "Invalid PDF structure" (byte corruption from a storage adapter) vs "Missing PDF" (404/403 — auth); check that Content-Type is literally application/pdf and not application/octet-stream.

iOS/Safari. PDF.js renders to canvas, so it works where inline native PDF does not — but touch scrolling and text selection inside the viewer are noticeably worse than native. Consider native inline PDF for desktop Safari and PDF.js only where you need consistency (annotations, page control, search UI).

Extension points are thin by design: swap the viewer build, override the wrapper template (Twig → Blade), and pass PDF.js's own hash parameters through. There's no meaningful PHP-side API to hook — no events, no service you'd inject. Treat the bundle as a snapshot, not a dependency.

License and bus factor. Apache-2.0, so vendoring the assets is fine — keep the NOTICE/license file alongside the build. But 10 stars, 0 dependents, one author, no release since 2018 means: no upstream fixes, ever. Pin the asset directory in your repo (it's static files, so this is cheap) and treat upgrades as a manual task on your own security calendar rather than a composer update.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib