amitdugar / img-opt
PHP image optimizer/delivery helper with AVIF/WebP support and CLI batch tool.
Installs: 19
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/amitdugar/img-opt
Requires
- php: >=8.2
- ext-imagick: *
- symfony/console: ^6.4 || ^7.0
- symfony/filesystem: ^6.4 || ^7.0
- symfony/finder: ^6.4 || ^7.0
README
Reusable PHP helpers for image delivery and batch conversion with AVIF/WebP support. Includes:
- In-app service to generate cached variants (resize + format negotiation)
<picture>helper for srcset generation- CLI tool for one-time/bulk conversion
- Cloudflare helper for /cdn-cgi/image URLs and dev fallback
Namespace: ImgOpt. Dependencies: PHP 8.2+, ext-imagick, Symfony Console/Filesystem/Finder.
Versioning: Semantic Versioning (SemVer) via git tags (e.g., 0.0.1).
Install
composer require amitdugar/img-opt
Quick start (PHP)
use ImgOpt\Config; use ImgOpt\ImgOpt; $config = Config::fromArray([ 'cache_root' => __DIR__ . '/public/_imgcache', 'public_root' => __DIR__ . '/public', 'cdn_base' => getenv('CDN_BASE') ?: '', 'max_width' => 0, // keep original unless a smaller width is requested 'quality' => ['avif' => 42, 'webp' => 80, 'jpeg' => 82], ]); $imgopt = ImgOpt::fromConfig( $config, publicPath: __DIR__ . '/public', useCloudflare: getenv('CF_ENABLED') === '1' ); // Generate variants and emit a <picture> tag with srcset echo $imgopt->picture( __DIR__ . '/public/img/photo.jpg', [480, 768, 1080, 1600], $_SERVER['HTTP_ACCEPT'] ?? '', '100vw', ['alt' => 'Sample photo'] );
- The helper emits a
<picture>with AVIF/WebP sources when Imagick supports them (and theAcceptheader allows them), then falls back to the original format. Variants are generated on demand undercache_root. ImageService::ensureVariant($source, $width, $acceptHeader, $forceFormat)is available if you just need the cached file path.- Set
cdn_baseto rewrite URLs to a CDN host (e.g.,https://cdn.example.com).
Cloudflare Image Resizing helper
If useCloudflare is enabled in ImgOpt::fromConfig, the same $imgopt->picture() call will emit Cloudflare URLs instead of local variants.
use ImgOpt\CloudflareImage; $cf = new CloudflareImage( publicPath: __DIR__ . '/public', // local public root (for size checks) cacheFile: __DIR__ . '/storage/image-sizes.json', // intrinsic-size cache (optional) defaultQuality: 85, enabled: true // set false in dev to bypass CF ); echo $cf->img( '/img/photo.jpg', [480, 768, 1080, 1600], // widths for srcset (will clamp to intrinsic) ['alt' => 'Sample photo', 'class' => 'img-fluid'], default: 800 );
- Builds
/cdn-cgi/image/...URLs when enabled. In dev/disabled mode, falls back to a local<picture>and will use adjacent.avif/.webpfiles if they exist. - Caches intrinsic sizes in a small JSON file to avoid repeated
getimagesizecalls. - Helper avoids upscaling and auto-adds
width/heightfor layout stability.
CLI (bulk conversion)
php bin/img-opt <folder> [--max-width N] [--q-avif N] [--q-webp N] [--formats avif,webp] [--force] [--dry-run] [--cache-dir DIR]
php vendor/bin/img-opt <folder> [--max-width N] [--q-avif N] [--q-webp N] [--formats avif,webp] [--force] [--dry-run] [--cache-dir DIR]
- Converts PNG/JPG recursively, writing AVIF/WebP variants into
cache-dir(defaults to<folder>/_imgcache). - Skips fresh outputs unless
--forceis set. Use--dry-runto preview.
Design notes
- AVIF/WebP capability is auto-detected from Imagick. If AVIF is missing, it falls back to WebP → JPEG/PNG, and if the
Acceptheader is empty it still prefers AVIF/WebP when available. - Cache keys include the source path + mtime + width + format + quality, so updates to the original regenerate variants.
- Width requests are clamped to avoid upscaling and can be capped via
max_width. - Minimal, maintained dependencies: Symfony Console/Filesystem/Finder; image work uses
ext-imagick.
Troubleshooting
- Ensure Imagick is built with WebP/AVIF:
php -r "var_dump((new Imagick())->queryFormats('WEBP'));" - If AVIF is unavailable, install
libheif+ rebuild Imagick (varies by distro). The library will automatically downgrade formats.***
SVG optimization (optional)
If you want smaller SVGs, consider running SVGO as a separate step (SVGs are served as-is and not converted by ImgOpt).
Example (one-off):
npx svgo -f public/assets/img -r