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

Gif Frame Extractor Laravel Package

sybio/gif-frame-extractor

PHP library to extract and split frames from animated GIFs. Read frame durations, colors, dimensions and disposal methods without Imagick, then export frames as separate GIF, JPEG or PNG images for thumbnails, previews or processing.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the extension

composer require sybio/gif-frame-extractor
php -m | grep -i gd   # needs GD with GIF support

There is no service provider, facade, or config file — it is a single plain PHP class (Sybio\GifFrameExtractor\GifFrameExtractor) plus an exception class. That means zero Laravel integration work and also zero framework help: you own the wiring.

Where to look first

  1. The README's "Extract frames" snippet — it is essentially the entire API surface.
  2. src/GifFrameExtractor/GifFrameExtractor.php — skim extract(), getFrames(), getFrameImage(), getFrameDuration(), isAnimatedGif(). It is a few hundred lines; you can read all of it.
  3. GifFrameExtractorException — the only error type you need to catch.

First use case: a poster image for uploaded GIFs

The highest-value day-one use is generating a static poster/thumbnail so every GIF in your app has a real image for OG tags, feed previews, and loading="lazy" placeholders.

use Sybio\GifFrameExtractor\GifFrameExtractor;

$path = Storage::disk('local')->path($gifPath); // MUST be a local filesystem path

if (GifFrameExtractor::isAnimatedGif($path)) {
    $extractor = new GifFrameExtractor();
    $extractor->extract($path);          // must be called first

    $frame = $extractor->getFrameImage(0); // GD image (GdImage in PHP 8+)

    ob_start();
    imagepng($frame);
    $png = ob_get_clean();

    Storage::disk('public')->put("posters/{$id}.png", $png);
    imagedestroy($frame);
}

That is the whole loop: isAnimatedGif() → extract() → getFrameImage()/getFrameDuration() → your own persistence.

Implementation Patterns

Wrap it, don't sprinkle it. Put the library behind one small service so the rest of the codebase never touches GD handles or the extractor's stateful API:

final class GifFrameService
{
    public function frames(string $localPath, int $limit = 24): array
    {
        $ex = new GifFrameExtractor();
        $ex->extract($localPath);

        return collect($ex->getFrames())
            ->take($limit)
            ->map(fn ($f, $i) => [
                'index'       => $i,
                'duration_ms' => $ex->getFrameDuration($i) * 10, // centiseconds
                'png'         => $this->encodePng($ex->getFrameImage($i)),
            ])->all();
    }
}

Bind it in a service provider and inject it into jobs/controllers. Because the extractor holds frame state on the instance, always construct a fresh one per file — do not make it a singleton.

Queue the work, always. Extraction is CPU- and memory-bound and takes tens to hundreds of milliseconds. Trigger it from an UploadedGif event into a queued job, write derived artifacts to Storage, and record their paths on the model. On re-render, read the cached artifact — never re-extract on a page request.

Upload flow that stays clean

  • Validate with mimes:gif plus an explicit isAnimatedGif() check in a FormRequest rule (static GIFs are a common surprise and should skip the pipeline entirely).
  • Store the original on a disk, but pass Storage::path() (local disks only) into the extractor. For S3 or any remote disk, Storage::get() to a tmpfile()/tempnam() first — the library does not read streams.
  • After extraction, persist frame count, total duration (getTotalDuration()), and the poster frame as columns. These are cheap to query and save you re-opening files forever.

Frame selection heuristics. Frame 0 is often a blank or logo frame in real-world GIFs. Two better options: pick the frame with the longest getFrameDuration() (usually the "held" visual), or sample e.g. 3 evenly spaced frames. getFrameImageSrc($i) returns a data: URI, which is convenient for an admin preview grid without touching the filesystem.

Contact sheet / sprite generation. Loop the frames and imagecopy() each into one canvas at fixed tiles. This is the natural fit for feed previews and is much smaller than shipping N PNGs. Keep the tile count bounded.

Downscale before you store. Extracted frames are full GIF dimensions. Run each through imagescale() (or Intervention Image if it's already in the project) before encoding; storing 800×800 PNGs from a 60-frame GIF will fill a disk fast.

Gotchas and Tips

Durations are centiseconds. GIF stores delay in 1/100s. Multiply by 10 for milliseconds. Many GIFs ship with 0 or 2 for frames, which browsers silently clamp — don't trust totals for anything user-facing without clamping.

Frame compositing is the weak spot. GIF frames are often stored as differences from the previous frame, with disposal methods deciding what's restored. Output for frame 2+ on optimized or transparent-heavy GIFs can look torn, doubled, or partial. Always eyeball a few real uploads from your user base before trusting the pipeline, and treat a wrong result as a library limitation rather than a bug you introduced. If fidelity matters, keep an Imagick or ffmpeg path behind a feature flag and compare.

Memory. Each frame is a full decoded GD image held on the extractor. A 500×500, 200-frame GIF is ~200 MB of raw bitmap. Set a hard frame cap (take($limit) after extraction is too late for memory — check frame count early or bound input dimensions), raise memory_limit in the job, and imagedestroy() every handle you create.

PHP 8 resource/GdImage split. In PHP 8+, imagecreatefromgif returns GdImage, not resource. Any wrapper code doing is_resource($img) type checks will silently break on upgrade. Check for GdImage|resource or just truthiness.

Order matters. Calling getFrameImage() before extract() throws GifFrameExtractorException. Also note isAnimatedGif() itself throws if the file does not exist — wrap it in try/catch rather than if (file_exists()) alone, since paths from storage can race.

Transparency. GIFs use a transparent palette index, not alpha. Convert to PNG and preserve transparency via imagesavealpha()/imagealphablending(false) on the destination, otherwise transparent regions come out black — the single most common visual complaint.

Don't trust save(). The built-in save helper writes files with its own naming and directory assumptions. Encoding the GD handle yourself with imagepng()/imagejpeg() into Storage gives you predictable paths, CDN-friendly keys, and no surprise writes outside your storage roots.

No extension points. It is a closed, single-class library with no hooks, events, or strategy interfaces. Treat it as an internal implementation detail behind your own interface — that way swapping to Imagick, ffmpeg, or a maintained fork later is a one-file change. It also has zero dependents on Packagist and receives little maintenance, so budget for the possibility of vendoring it.

Testing. Commit 3–4 small GIF fixtures: a simple animation, a transparent one, a static GIF (should return false), and one with long frame delays. Assert frame count and durations — those are stable. Do not snapshot-compare pixel output; it varies across GD builds.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin