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 Watermarker Laravel Package

filippo-toso/pdf-watermarker

Add text or image watermarks to existing PDF files in PHP. Configure the watermark content, position, opacity, and rotation, then apply it to documents on the fly via a straightforward API. Useful for branding, draft labels, or protecting sensitive PDFs in Laravel and plain PHP apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and clear legal first.

composer require filippo-toso/pdf-watermarker

Before you write a line of code, open vendor/filippo-toso/pdf-watermarker/LICENSE and read it. The repo reports NOASSERTION, meaning GitHub could not detect a standard license — that is a gating item for anything commercial, not a formality. In parallel, run:

composer show filippo-toso/pdf-watermarker --all      # confirms the illuminate/support constraint
composer why-not filippo-toso/pdf-watermarker         # confirms it resolves against your locked Laravel
composer show -t filippo-toso/pdf-watermarker         # reveals whether FPDI/FPDF is pulled in transitively

The transitive FPDI/FPDF dependency carries its own terms (historically a commercial license for certain uses of page import), so the package license and the dependency license are two separate questions.

Where to look first. Read these three things in this order, and skip the README's marketing bullets until you have:

  1. src/ — the whole package is small; read the facade and the service class end to end. You will own this code, so the 30 minutes is not optional.
  2. config/ (if present) — publish it with php artisan vendor:publish --tag=pdf-watermarker-config to see the defaults for font path, output path, opacity, rotation, and any driver settings. These are the knobs that produce surprise output.
  3. composer.json — pin down the PHP and Laravel version floors, and note whether a font directory or asset path is expected.

First use case. The lowest-risk, highest-value first task is a text watermark on a PDF you generated yourself — a "DRAFT" or "CONFIDENTIAL" stamp on an invoice or report export. It exercises the full path (load, overlay, save) without the FPDI parser having to cope with third-party PDFs. The shape of the call is a fluent or single-shot builder; verify the exact method names against the facade before copying anything from a blog post:

use FilippoToso\PdfWatermarker\Facades\PdfWatermarker;

PdfWatermarker::input(storage_path('app/invoices/1234.pdf'))
    ->text('CONFIDENTIAL')
    ->position('diagonal')   // or coordinates — check the source for accepted values
    ->opacity(0.25)
    ->save(storage_path('app/invoices/1234-watermarked.pdf'));

Open the output, zoom in, and check three things before trusting it: the watermark is visible but not obscuring content, the file size has not ballooned (a sign of rasterization), and page count is unchanged. That last one catches silent page drops, which is the failure mode that matters most.

Implementation Patterns

Wrap it in a contract from day one. Do not let the facade leak into controllers. One interface, one adapter, one afternoon:

interface WatermarkService
{
    /** @return resource|string Path or stream to the watermarked PDF. */
    public function apply(string $pdfPath, WatermarkSpec $spec): string;
}

WatermarkSpec is a small value object (text|image, opacity, rotation, position, color, fontPath). Because the package is unmaintained (16 stars, 0 dependents, one maintainer), this seam is the primary mitigation — a swap to direct FPDI, TCPDF, or a commercial SDK becomes a days-not-weeks project when the interface already exists. Adapter throws a domain exception (WatermarkFailed) rather than leaking the package's own exception types.

Never watermark in the request cycle for anything non-trivial. FPDI loads the whole document into memory. Anything over a handful of pages belongs in a queued job:

class ApplyWatermarkJob implements ShouldQueue
{
    public int $timeout = 120;
    public int $tries   = 3;
    // memory: raise for this worker only — see Gotchas

    public function handle(WatermarkService $watermarks): void
    {
        $out = $watermarks->apply($this->sourcePath, $this->spec);
        $this->store($out); // write to S3 / public disk
    }
}

Decide explicitly what should happen on failure — reject and notify, or fall back to shipping the unwatermarked file with an alert. Do not let a swallowed exception produce a silently unwatermarked "CONFIDENTIAL" document.

Storage and path hygiene. Watermarking produces a new file; do not overwrite the source. Write to a temp path inside the job and move into place atomically (Storage::disk('s3')->put(...) on a local temp stream). Clean up in a finally block — leaked temp PDFs at volume will quietly fill a disk.

Trial gating with per-recipient identifiers. The strongest day-to-day use is degrading free-tier output rather than blocking it:

$spec = WatermarkSpec::text("TRIAL — {$user->email}")
    ->opacity(0.15)
    ->diagonal()
    ->color('#888888');

Keep the identifier out of the visible body where it would hurt the document's usefulness — a footer or diagonal background stamp reads as professional; a giant red block reads as punitive. Centralise the spec-building so a policy change (opacity, wording) is one edit, not a grep across controllers.

Streaming downloads. For ad-hoc "download watermarked copy" endpoints, apply to a temp file and return response()->file($tmp)->deleteFileAfterSend(true). Do not buffer a multi-MB PDF in a JSON response, and do not use response()->download() with the original path.

Golden-file tests. Because the package has no ecosystem finding its bugs for you, add a regression test that watermarks two or three checked-in sample PDFs and asserts page count is preserved and the output is non-trivial in size. This is cheap and catches upstream drift after a composer update on the transitive dependency.

Gotchas and Tips

License is the dealbreaker until cleared. NOASSERTION plus a transitive FPDI/FPDF dependency means you have two license questions, not one. Get written confirmation of the package license and the terms of whatever PDF-page-import library it pulls in before this touches a paid product. Engineering can spike in parallel; shipping should wait.

Memory is the most likely production incident. FPDI parses the whole file into memory; a 100-page / 40 MB PDF can plausibly blow the default 128M memory_limit. Either run a dedicated queue worker with php -d memory_limit=512M artisan queue:work or set $this->memoryLimit-style config per job group. Size workers for the worst document, not the median. A single memory-exhausting job can take down a shared worker pool.

Compatibility drift is silent until composer update. A small, low-star package rarely tracks Laravel majors. Pin the version in composer.json (exact or ~), and after any Laravel/PHP upgrade run composer why-not and the golden-file test before touching anything else. Plan to fork into an internal namespace (App\Support\PdfWatermarker or a private repositories entry) so a future Laravel bump is not blocked by an abandoned constraint.

UTF-8 and non-Latin text need a real TTF. The default FPDF core fonts are Latin-1 only; "CONFIDENTIAL" renders fine, but accented names, Cyrillic, CJK, or an em-dash in a watermark string will come out as ? or nothing at all — a silent, fail-open bug. Pass an explicit TTF fontPath and add a test with a non-ASCII string.

Rotation and crop boxes misplace the stamp. Landscape or rotated pages frequently put a diagonal watermark off-canvas or half-off the edge. Test on a landscape page and a page with a non-zero /Rotate before assuming your placement math is right. If the package exposes raw coordinates, prefer computing from MediaBox dimensions per page.

Transparency and z-order. Verify the watermark draws above page content (otherwise a page with an opaque background hides it entirely — the worst kind of bug because it "succeeds") and that opacity is actually applied rather than baked into a solid fill. If the overlay is drawn as an image, opacity handling differs from a vector text overlay.

Encrypted and permission-restricted PDFs. Decide the policy explicitly: reject with a clear error, skip, or fail the job. Do not let the package attempt and produce a truncated file. Add an early check (/Encrypt present in the trailer) and route to the reject path.

Rasterized vs vector output. Open a watermarked file and check the size delta. Vector text overlays add kilobytes; rasterized ones can multiply the file several times over. If output size matters (egress, storage, email attachments), confirm you are getting the vector path.

Extension points are limited — plan around that. With no maintainer, treat the package as frozen. Put your logic in the adapter and the spec object, keep the package call surface to a single method, and document a known-issues list (fonts, rotation, encrypted inputs, memory) in the repo so on-call debugging does not restart from scratch. At least two engineers should be able to reproduce a bad watermark.

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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast