Getting Started

Requirements

  • PHP 8.4+
  • A provider backend (see below)

Installation

composer require timber/chainsaw

Depending on your provider, install the required dependencies:

Intervention (local)
Imagine (local)
Cloudinary
Cloudflare
composer require intervention/image league/flysystem
composer require imagine/imagine league/flysystem

No additional dependencies required.

No additional dependencies required.

Setup

1. Create a provider

InterventionProvider
ImagineProvider
CloudinaryProvider
CloudflareProvider
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
use Intervention\Image\ImageManager;
use League\Flysystem\Filesystem;
use League\Flysystem\Local\LocalFilesystemAdapter;
use Timber\Chainsaw\Provider\Intervention\InterventionProvider;
use Timber\Chainsaw\Source\FlysystemSourceAdapter;

$sourceFs = new Filesystem(new LocalFilesystemAdapter('/path/to/images'));
$cache    = new Filesystem(new LocalFilesystemAdapter('/path/to/cache'));

$provider = new InterventionProvider(
    sourceReader: new FlysystemSourceAdapter($sourceFs),
    cache: $cache,
    cachePublicUrl: '/images/cache',
    manager: new ImageManager(new GdDriver()),
);
use Imagine\Gd\Imagine; // or Imagine\Imagick\Imagine
use League\Flysystem\Filesystem;
use League\Flysystem\Local\LocalFilesystemAdapter;
use Timber\Chainsaw\Provider\Imagine\ImagineProvider;
use Timber\Chainsaw\Source\FlysystemSourceAdapter;

$sourceFs = new Filesystem(new LocalFilesystemAdapter('/path/to/images'));
$cache    = new Filesystem(new LocalFilesystemAdapter('/path/to/cache'));

$provider = new ImagineProvider(
    sourceReader: new FlysystemSourceAdapter($sourceFs),
    cache: $cache,
    cachePublicUrl: '/images/cache',
    imagine: new Imagine(),
);
use Timber\Chainsaw\Provider\Cloudinary\CloudinaryProvider;

$provider = new CloudinaryProvider(
    cloudName: 'my-cloud',
    deliveryType: 'upload',
);
use Timber\Chainsaw\Provider\Cloudflare\CloudflareProvider;

$provider = new CloudflareProvider(
    host: 'https://example.com',
);

2. Create a factory

use Timber\Chainsaw\ImageFactory;

$factory = new ImageFactory($provider);

3. Use it

$image = $factory->image('photo.jpg');

// Resize
echo $image->width(400);

// Chain manipulations
echo $image->crop(800, 600)->greyscale()->quality(80);

Factory configuration

ImageFactory is final readonly — every dependency is passed at construction:

use Timber\Chainsaw\Encoding;
use Timber\Chainsaw\Enum\Format;
use Timber\Chainsaw\ImageFactory;
use Timber\Chainsaw\Manipulator\Crop;
use Timber\Chainsaw\Manipulator\Watermark;
use Timber\Chainsaw\Preset;
use Timber\Chainsaw\Presets;
use Timber\Chainsaw\Watermark\WatermarkFromUrl;

$factory = new ImageFactory(
    provider: $provider,

    // Default manipulators applied to every image
    defaults: [new Watermark(new WatermarkFromUrl('logo.png'), Position::BottomRight)],

    // Default encoding
    defaultEncoding: new Encoding(quality: 85),

    // Named presets
    presets: (new Presets())
        ->add(new Preset('thumbnail', [new Crop(200, 200)]))
        ->add(new Preset('hero', [new Crop(1200, 600)])),
);

Image metadata

Providing source dimensions enables dimension math without I/O, which in turn lets the renderer emit intrinsic width/height attributes on rendered <img> and <source> for CLS-safe markup.

Explicit metadata (zero I/O)

If you already know the source dimensions (from a CMS, database, or manifest), pass them directly:

use Timber\Chainsaw\Metadata\ImageMetadata;

$meta = new ImageMetadata(1920, 1080);
$image = $factory->image('photo.jpg', $meta);

$dims = $image->width(400)->dimensions();
// → width: 400, height: 225 (calculated from ratio)

Without metadata, dimensions are unknown until the provider processes the image.

Auto-resolved metadata (via MetadataResolverInterface)

For workflows where dimensions aren’t known upfront, wire a MetadataResolverInterface on the factory. The resolver reads the source bytes once and caches the result in a PSR-6 pool, so subsequent calls for the same source are zero-I/O:

use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Timber\Chainsaw\ImageFactory;
use Timber\Chainsaw\Metadata\CachedMetadataResolver;
use Timber\Chainsaw\Source\FlysystemSourceAdapter;

$resolver = new CachedMetadataResolver(
    upstream: new FlysystemSourceAdapter($source),  // Flysystem holding the originals
    pool: new FilesystemAdapter(directory: '/var/cache/metadata'),
);

$factory = new ImageFactory(
    provider: $provider,
    metadataResolver: $resolver,
);

$image = $factory->image('photo.jpg');  // resolver reads + caches on first call

Shipped resolvers:

  • FlysystemSourceAdapter — reads source bytes, dimensions, and last-modified version via any Flysystem adapter (local, S3, FTP, memory). Same class used for URL-grammar provider source bytes.
  • NativeMetadataResolver — reads local paths with getimagesize() (optional basePath prefix).

Explicit metadata always wins: $factory->image('photo.jpg', $explicit) never calls the resolver.

See also: Automatic cache invalidation — when your resolver also implements SourceVersioner, the library propagates a version tag through metadata, variant, and CDN caches so overwriting a source bytes auto-invalidates everything.