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

Fast Image Size Laravel Package

marc1706/fast-image-size

Fast, dependency-free PHP library that reads image dimensions from file headers without loading the whole file, supporting JPEG, PNG, GIF, BMP, WebP, and more. A quick drop-in alternative to getimagesize for speed and low memory use.

View on GitHub
Deep Wiki
Context7

Getting Started

marc1706/fast-image-size answers one question cheaply: "what are the pixel dimensions and format of this image?" It parses image headers only, so it works on remote URLs without downloading the full payload and needs neither GD nor Imagick. In a Laravel app, that makes it the right tool for three jobs and the wrong tool for everything else:

  • resolving dimensions of remote images (imports, link previews, CMS embeds, feed ingestion)
  • backfilling width/height/mime columns over an existing library
  • probing files in environments where GD/Imagick aren't guaranteed
composer require marc1706/fast-image-size

First use case — enrich a media record from a URL or a storage path:

use FastImageSize\FastImageSize;

$size = (new FastImageSize())->getImageSize($urlOrPath);

// Typical shape (verify keys against your pinned version):
// ['type' => 'jpeg', 'width' => 1200, 'height' => 630, 'mime' => 'image/jpeg']

Where to look first, in order:

  1. src/FastImageSize.php — the whole public surface: getImageSize(), getImageType(), addType(), and the HTTP-client/user-agent setters. Read this file end to end; it's short.
  2. src/Type/ — one parser per format (JPEG, PNG, GIF, WebP, BMP, TIFF, ICO, PSD). Skim JPEG and WebP; that's where real-world edge cases live.
  3. src/Client/ — the HTTP abstraction used for remote URLs. This is exactly what v1.3.1 changed, so understand which client you're getting by default.
  4. tests/ — the fixtures double as the authoritative list of supported variants.

Do not call new FastImageSize() at call sites. Bind it once and wrap it — you'll want a single place to configure timeouts, register custom types, and add caching.

// app/Providers/MediaServiceProvider.php
public function register(): void
{
    $this->app->singleton(FastImageSize::class, function () {
        $reader = new FastImageSize();
        // Many CDNs 403 on a default/empty UA.
        $reader->setUserAgent(config('media.probe.user_agent'));
        return $reader;
    });

    $this->app->singleton(ImageProbe::class);
}

Implementation Patterns

Thin wrapper service, single entry point. Keep the library replaceable and give the rest of the app one method to reason about:

namespace App\Support\Images;

use FastImageSize\FastImageSize;

final class ImageProbe
{
    public function __construct(private FastImageSize $reader) {}

    public function probe(string $pathOrUrl): ?ImageMetadata
    {
        if (str_starts_with($pathOrUrl, 'http')) {
            $this->guard->assertFetchable($pathOrUrl); // SSRF check, see below
        }

        $size = $this->reader->getImageSize($pathOrUrl);

        if (! is_array($size) || empty($size['width']) || empty($size['height'])) {
            return null; // never throw; callers decide
        }

        return new ImageMetadata(
            width: (int) $size['width'],
            height: (int) $size['height'],
            type: $size['type'] ?? null,
            mime: $size['mime'] ?? null,
        );
    }
}

Nullable return, explicit ImageMetadata value object. Everything downstream depends on that contract, not on the library's array shape.

Queued enrichment for remote URLs. Never probe a user-supplied or third-party URL inline in a request. One job per URL, with a tight timeout and a fallback path:

final class HydrateRemoteImage implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public int $timeout = 15;

    public function __construct(public int $mediaId, public string $url) {}

    public function handle(ImageProbe $probe): void
    {
        $meta = $probe->probe($this->url);

        if ($meta === null) {
            return; // dimensions stay null; UI uses its placeholder ratio
        }

        Media::whereKey($this->mediaId)->update([
            'width' => $meta->width,
            'height' => $meta->height,
            'mime' => $meta->mime,
        ]);
    }
}

Cache decorator, keyed on URL. Remote lookups are network calls, not CPU: cache them or you'll pay egress and latency repeatedly for the same asset.

final class CachedImageProbe
{
    public function __construct(private ImageProbe $inner) {}

    public function probe(string $pathOrUrl): ?ImageMetadata
    {
        if (! str_starts_with($pathOrUrl, 'http')) {
            return $this->inner->probe($pathOrUrl); // local reads are free
        }

        $value = Cache::remember(
            'img-meta:'.sha1($pathOrUrl),
            now()->addDays(30),
            fn () => $this->inner->probe($pathOrUrl),
        );

        return $value instanceof ImageMetadata ? $value : null;
    }
}

Bind CachedImageProbe as the ImageProbe implementation so call sites get caching for free.

Backfill command for existing libraries. This is the highest-value use of the package: populate width/height columns that were never captured, at a cost low enough to run over the whole library.

Media::whereNull('width')->select('id', 'path')
    ->chunkById(200, function ($rows) use ($probe) {
        foreach ($rows as $row) {
            $meta = $probe->probe(Storage::disk('public')->path($row->path));
            if ($meta) {
                $row->update(['width' => $meta->width, 'height' => $meta->height]);
            }
        }
    });

For S3/GCS-backed disks, path() won't exist — probe a temporary URL instead, or fall back to getimagesize() on a streamed download if the object is small.

Custom formats via addType(). If your asset mix includes something the bundled parsers miss (or a wrapper format you control), the extension point is addType() with your own TypeBase subclass. Prefer this over forking.

Testing. Because the HTTP client is injectable, bind a stub FastImageSize (or a fake PSR-18/Client implementation) in tests rather than hitting the network. For the wrapper itself, fixture files under tests/Fixtures cover the local path and are fast enough to run in the default suite.

Gotchas and Tips

Laravel already has a dimensions rule for uploads. If the file is an UploadedFile you're about to move, dimensions:max_width=… uses getimagesize() under the hood and adds zero dependencies. The package earns its keep for remote URLs and for already-stored files, not for upload validation.

Return value is not an exception. Failure is false/null, not a throw. Every call site must handle "unknown dimensions" deliberately — otherwise nulls propagate into <img width height> attributes and defeat the CLS goal you adopted this for.

v1.3.1 changed error semantics. The library no longer enforces content only on 200. That fixes 206 Partial Content from CDNs and signed object-store URLs — which is precisely the range-request happy path — but it also means a 403/404/503 that happens to carry bytes may now parse. If any workflow depends on "probe succeeded ⇒ URL is live" (dead-link sweeps, retry logic, alerting), validate the status at your HTTP layer instead. Add a regression test for a 206 and for a 404-with-HTML-body.

SSRF is your problem, not the library's. Any user-supplied URL reaching this package is a server-side request forgery vector. Enforce a scheme allowlist (https only), block private/link-local IPs at resolution time (not just on the initial hostname), cap response size, and set a hard timeout. The library is not a security boundary.

No default timeout worth relying on. Configure your HTTP client with connect + total timeouts, otherwise a hostile or slow origin parks PHP-FPM workers. Keep remote probes off the request path entirely; queue them.

EXIF orientation is ignored. Width and height are raw pixel dimensions. A portrait phone JPEG may report 4032×3024 with orientation 6, meaning it displays as 3024×4032. If you're using these numbers for aspect-ratio boxes, either normalize against EXIF (separate library) or accept a rotated placeholder.

Cache invalidation when an image is replaced at a stable URL. URL-keyed caching returns stale dimensions forever after a swap. Invalidate explicitly on re-upload, or key on a content hash / ETag / version query param. Include a manual flush hook in your media model's saved/deleted events.

Unsupported formats return nothing, silently. SVG, AVIF, and HEIC are not in the parser list. If your CMS accepts SVGs, you need a distinct path (parse viewBox, or store null and let the UI measure). Check your actual asset mix against src/Type/ before promising "we support all images".

Animated GIF/WebP report the canvas, not frames. Fine for layout, useless for anything frame-related.

Don't pass user-controlled local paths. A path-traversal-shaped string lets an attacker probe arbitrary files on disk for "is this a PNG and how big". Always resolve through Storage::disk(...)->path() with a validated, database-sourced relative path.

For local files, getimagesize() is often faster. It's a C function; this is pure PHP. The package's advantage is remote URLs and no-extension environments. Benchmark before assuming a win on the local path — and consider keeping getimagesize() as the local fallback inside your wrapper.

Pin ^1.3 and treat upgrades deliberately. Single-maintainer project with an infrequent release cadence. Keep the wrapper thin so replacing it with getimagesize() on a ranged download, or a Guzzle header parse, is an afternoon's work rather than a migration.

Add the status-code guard at the call site and a config/media.php for user agent, timeouts, host allowlist, and cache TTL. There's no published config file — the knobs are yours to define, and you'll want them in one place when security reviews the URL-fetching path.

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.
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc