zetacomponents/image-analysis
PHP component from Zeta Components for analyzing image files. It detects MIME type, image format, dimensions, and animation status using GD or ImageMagick backends, making it useful for validating uploads and describing images in PHP applications.
Install and verify the API surface first.
composer require zetacomponents/image-analysis
Before writing any code, open the package in vendor/ and read three files in this order:
composer.json — confirms the autoload namespace (older Zeta releases were ezc*; the maintained ports use Zeta\Components\ImageAnalysis) and the required PHP range.src/Analyzer.php — the only class you will normally touch. Look at analyze(), getMimeType(), getWidth(), getHeight(), getIsImage(), and getData().src/Handlers/ — the pure-PHP handler is the reason to use this package. It reads image headers without GD or Imagick, so it works in slim containers and serverless runtimes that ship no image extensions.Minimal first use case — validate an upload without trusting the client:
use Zeta\Components\ImageAnalysis\Analyzer;
$analyzer = new Analyzer();
$analyzer->analyze($request->file('photo')->getRealPath());
if (! $analyzer->getIsImage()) {
abort(422, 'Not an image');
}
$mime = $analyzer->getMimeType(); // content-sniffed, not extension-based
$width = $analyzer->getWidth();
$height = $analyzer->getHeight();
That is the whole value proposition: real MIME + dimensions from file bytes, cheaply, before anything decodes pixels.
Wrap it in a tiny service, not in controllers. The analyzer is stateful (each analyze() overwrites the last result), so instantiate per use and expose an immutable DTO:
final class ImageMeta
{
public function __construct(
public readonly string $mime,
public readonly int $width,
public readonly int $height,
) {}
}
final class ImageInspector
{
public function inspect(string $path, ?string $mimeHint = null): ImageMeta
{
$a = new Analyzer();
$a->analyze($path, $mimeHint);
return new ImageMeta($a->getMimeType(), $a->getWidth(), $a->getHeight());
}
}
Bind it as bind (not singleton) in a service provider — see Gotchas for Octane.
Use it as a validation backstop. Laravel's image rule and mimes: rely on the client-declared type plus an extension guess. Combine them with a real content check behind the scenes:
class ImageMetadataRule implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! $value instanceof UploadedFile || ! $value->isValid()) {
$fail('Invalid upload.'); return;
}
try {
$a = new Analyzer();
$a->analyze($value->getRealPath());
} catch (\Throwable) {
$fail('Unreadable image.'); return;
}
if (! $a->getIsImage()) {
$fail('The file is not a valid image.'); return;
}
if (! in_array($a->getMimeType(), ['image/jpeg', 'image/png'], true)) {
$fail('Unsupported image type.'); return;
}
if ($a->getWidth() > 8000 || $a->getHeight() > 8000) {
$fail('Image dimensions exceed the limit.');
}
}
}
Gate expensive work. Run the inspector before dispatching an Intervention\Image job or an S3 conversion. It costs microseconds and prevents a 200 MP decompression bomb from killing a queue worker.
Cache by content hash. Metadata for an immutable object never changes:
$key = 'imgmeta:'.hash_file('xxh128', $path);
$meta = Cache::remember($key, 86400, fn () => app(ImageInspector::class)->inspect($path));
Pass a MIME hint when you already know it. The second argument to analyze() skips sniffing and short-circuits to the right handler, which is useful if you have already filtered the upload by extension server-side.
NOASSERTION, but the Zeta Components source is Apache-2.0. Read LICENSE in vendor/ before shipping this in a commercial product; keep a copy of the notice.composer.json, add your own feature tests around the formats you accept, and be prepared to fork. Run the suite on your target PHP version first — PHP 8.1+ will surface deprecations in older Zeta code.Orientation=6 tag is reported with swapped width/height. If the number is user-facing or feeds a crop, read orientation separately via exif_read_data() or let Imagick auto-orient.analyze() in try/catch and treat any exception as a validation failure, not a 500.analyze() accepts a path that may go through stream wrappers; a user-supplied value is an SSRF vector. Always hand it $file->getRealPath() or Storage::path(...).Storage::fake() returns a different root. UploadedFile objects created by UploadedFile::fake()->image() do not necessarily sit where your storage disk does. For integration tests, copy real fixture images into place and assert against those, not fake-generated ones.getIsImage() === true before you rely on it; otherwise fall back to getimagesize() for those cases.analyze() mutates the instance, do not resolve the Analyzer as a singleton in a long-running worker or Octane process; a reused instance across requests is a subtle cross-contamination bug. bind or new is correct.finfo rather than patching the package in place.How can I help you explore Laravel packages today?