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

Filament Autograph Laravel Package

saade/filament-autograph

Filament Autograph adds a signature pad field to Filament forms, letting users draw handwritten signatures with mouse or touch. Signatures are saved as images or base64 for contracts, waivers, and approvals, with simple configuration and validation.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a single Filament form field — nothing else. Treat setup as four steps:

  1. Install and pin.

    composer require saade/filament-autograph
    # then immediately tighten the constraint in composer.json, e.g. "saade/filament-autograph": "^4.3"
    

    Pin the minor, not just the major. Fluent method names on this field have drifted between releases, and an unpinned composer update is the classic source of "my ->disk() call silently does nothing".

  2. Publish config (and views only if you must restyle).

    php artisan vendor:publish --tag=filament-autograph-config
    php artisan vendor:publish --tag=filament-autograph-views   # optional
    

    The config holds the canvas defaults — height, pen colour, background, dot size. Set sensible defaults there once rather than repeating fluent calls on every field.

  3. Add the column. Nullable, because the field must be clearable and because you'll want the form working before signatures exist.

    $table->longText('signature')->nullable();   // base64 path
    // or
    $table->string('signature_path')->nullable(); // disk path (preferred)
    
  4. Drop the field into a form you already have.

    use Saade\FilamentAutograph\Forms\Components\SignaturePad;
    
    SignaturePad::make('signature')
        ->label('Customer signature')
        ->canvasHeight(180)
        ->penColor('#111827')
        ->backgroundColor('#ffffff')
        ->required()
        ->columnSpanFull();
    

First use case: a proof-of-delivery or acknowledgement checkbox on an existing resource edit page. One nullable column, one field on one form, no changes to the surrounding schema. Run it on a real phone in the field, not on your desktop — that's the only test that matters at this stage.

Where to look first: the package's src/Forms/Components/SignaturePad.php (it's a small class — read the whole thing in five minutes and you'll know the real API surface), the published config, and the Blade view to see whether the canvas is initialised with Alpine on x-init or on Livewire hooks.

Implementation Patterns

Wrap it, always. Never reference Saade\FilamentAutograph\... from a resource. Define one class:

namespace App\Filament\Forms\Components;

use Saade\FilamentAutograph\Forms\Components\SignaturePad as BaseSignaturePad;

class SignatureField extends BaseSignaturePad
{
    protected function setUp(): void
    {
        parent::setUp();

        $this->canvasHeight(180)
            ->penColor('#111827')
            ->backgroundColor('#ffffff')
            ->required();
    }
}

One file to change when the upstream package goes dormant, breaks on a Filament major, or needs to be swapped for a FileUpload. Keep the diff against upstream clean.

Prefer disk-plus-path over base64-in-column. Base64 rides through Livewire component state, inflates the request body, and turns your table into a bloated blob store that's expensive to back up. If the version you pinned exposes ->disk(), ->directory() and ->visibility(), use them. If it doesn't, keep the field raw and normalise on save:

protected static function booted(): void
{
    static::saving(function (Model $model) {
        $raw = $model->signature; // data:image/png;base64,....
        if (! is_string($raw) || ! str_starts_with($raw, 'data:image')) {
            return;
        }

        $binary = base64_decode(Str::after($raw, ','), true);
        abort_if($binary === false, 422, 'Invalid signature payload');

        $path = 'signatures/'.Str::uuid().'.png';
        Storage::disk('private')->put($path, $binary);

        $model->signature_path = $path;
        $model->signature_hash = hash('sha256', $binary);
        $model->signature = null; // don't keep the blob
    });
}

Treat the image as evidence, not as the record. Store a sibling row (or columns) with: signer id, signed_at, IP, user agent, the document/template version, and the SHA-256 of the image. A PNG on its own is not a defensible signature record.

Reuse the schema between create and edit, but lock it down on edit. Signatures are append-only; a signature captured once should not be silently redrawn. On edit forms, render a read-only view:

ImageEntry::make('signature_path')
    ->disk('private')
    ->visible(fn ($record) => $record !== null);

Multi-signature forms (e.g. customer + technician) are just two fields on two columns, or a morphMany signatures relation if you expect more than two and want per-signer timestamps.

Testing is straightforward because state is a string. Generate a 1×1 PNG fixture, fillForm(['signature' => $dataUrl]), submit, and assert the row and the file exist — plus a negative test that a blank canvas is rejected.

PDF output is a separate concern; render Storage::disk('private')->path($model->signature_path) into the PDF with dompdf/snappy. Coordinate placement onto a pre-existing PDF is not what this package does.

Gotchas and Tips

  • A blank canvas is not a validation error. ->required() may be satisfied by an empty data URL or an empty string, and the form will happily save a null signature with no exception. Validate server-side — in a saving hook or model observer — that the decoded binary is non-empty and decodable. Never trust client-side required state.
  • Strip the data-URL prefix before base64_decode. Naively decoding data:image/png;base64,... yields a corrupt file that opens in nothing.
  • Payload size is a hard ceiling. Base64 in Livewire state means post_max_size and upload_max_filesize cap your signature size, and a rejected oversized payload fails as a 413 or a silent field loss. Log and alert on it.
  • Keep the binding deferred. Filament's default deferred binding sends the image once on submit; ->live() on a signature field round-trips a large string on every mouse-up.
  • Transparent background is the default and it bites. A black pen on a transparent PNG is invisible on dark UIs and prints badly. Set an explicit white background on the stored image, and note that the field follows the page's dark class (fixed in 4.3.2) — so the canvas looks right in dark mode even when the stored pixels are wrong.
  • Zero-width canvases. Put the field in a collapsed Section, a non-active Tabs tab, or a Wizard step and the canvas can initialise at width 0: signatures come out squashed, offset, or capture nothing. Test the field in its real container, and prefer an explicit pixel canvasHeight over percentage sizing.
  • Clear/reset semantics. The clear action resets state to null — the column must be nullable, and any saving hook must handle null without throwing.
  • Filament major upgrades fail quietly. A renamed asset-registration hook or a missing JS asset renders a blank canvas while the form still submits. Add a server-side assertion that the stored artifact is non-empty and decodable, and re-run your spike on every Filament major bump.
  • CSP and asset delivery. Prefer locally published/bundled assets over a CDN. A blocked script produces the same silent-blank failure as a broken hook.
  • Orphaned files on update/delete. If a user re-signs, the old file is not cleaned up automatically. Handle it in the observer and add a scheduled sweep for records past your retention window.
  • Accessibility. Canvas capture is pointer/stylus-driven; there is no meaningful keyboard fallback. If the workflow must serve assistive tech, provide an alternative path (upload, or a staff-witnessed override) rather than shipping the canvas alone.
  • Verify before you commit more than a day. Check the declared Filament constraints and what CI actually exercises, whether capture routes through Livewire temporary uploads or rides base64, and the real tag history — the published release date is future-dated and should be sanity-checked against actual tags. 82 stars, zero dependents, one maintainer: budget for a fork, keep the wrapper thin, and don't put a legally binding agreement behind it.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource