Images

Resizing, cropping, presets, srcset and base64 placeholders for image fields.

On this page

Declaring an image field

Declare a field of type image, then list it by name in the view:

// site/fields/hero/_index.json
{ "label": "Hero", "type": "@phlat/image", "options": {} }

// site/views/post/_index.json
{ "fields": { "hero": {} } }

image decodes to a PageImage, a PageFile with transform and encoding helpers on top. SVGs are excluded even in an image field because the resize pipeline can't rasterize them; they decode to null. Use a file field for SVG.

Basic access

{$page->hero}                       {* original file's public URL *}
{$page->hero('w=400,h=300,crop')}   {* transformed variant URL, shorthand for src() *}

Bare {$page->hero} calls url(), which exposes the original on first use and returns a cache-busted URL.

Transform options

src() takes a comma-separated option string, the same DSL used across PhlatPage. A bare name such as crop or grayscale is a flag, the same as crop=true, and a bare name that is a configured preset expands to that preset's options:

Option Effect
w=400 Max width in pixels
h=300 Max height in pixels
crop Crop to exactly fill w x h (fit=crop is the same). Default scales to fit, no crop
grayscale Desaturate
invert Invert colors
blur=5 Gaussian blur radius
brightness=10 Brightness adjustment
contrast=20 Contrast adjustment
gamma=1.5 Gamma correction
pixelate=8 Pixelate block size
sharpen=5 Sharpen amount
posterize=16 Reduce to at most this many colors
quality=60 JPEG quality (1-100), overriding the config default
rotate=90 Rotate clockwise by this many degrees
flip=v Mirror the image, h (default) or v
orient Auto-rotate upright from EXIF data, before any resize
trim=10 Remove border areas within this color-similarity tolerance
colorize=20:0:0 Adjust red:green:blue channel intensity
format=webp Encode as png, webp, etc. instead of JPEG
{$page->hero->src('w=800,h=400,fit=crop')}
{$page->hero->src('w=600,grayscale,blur=3')}
{$page->hero->src('orient,w=600')}
{$page->hero->src('rotate=90,flip=v')}

Each unique option string is its own cached variant.

Presets

Presets declared in config.json give a transform a name:

{
    "images": {
        "presets": {
            "thumbnail": "w=150,h=150,crop",
            "card":      "w=400,h=300,crop",
            "hero":      "w=1600,h=600,crop",
            "avatar":    "w=80,h=80,crop,grayscale"
        }
    }
}

A bare name with no = is a preset. Trailing key=value pairs override it, and a preset may reference other presets:

{$page->hero->src('card')}
{$page->hero->src('card,grayscale,quality=60')}

srcset()

Builds a srcset value from preset names (the width descriptor comes from the preset's w=) or from explicit option-to-descriptor pairs:

<img
    src="{$page->hero->src('card')}"
    srcset="{$page->hero->srcset(['thumbnail', 'card', 'hero'])}"
>

<img srcset="{$page->hero->srcset(['w=400' => '400w', 'w=800' => '800w'])}">

base64()

Encodes a variant as a data: URI, for blur-up placeholders inlined in the first response. Unlike src(), it generates the variant immediately.

<img
    src="{$page->hero->base64('w=20,h=20,blur=5')}"
    data-src="{$page->hero->src('w=800')}"
    loading="lazy"
>

placeholder()

A global template function, not a field method. It builds a static SVG data URI with no source file, for a src fallback or a fixed-ratio box before any upload:

<img src="{placeholder(400, 300, '#e5e5e5')}" alt="">

Dimensions

width(), height(), ratio(), isLandscape(), isPortrait() and isSquare() read the original file. They are on the decoded PageImage, so call decode() first:

{var $hero = $page->hero->decode()}
{if $hero}
    <div class="{if $hero->isPortrait()}max-w-sm{else}max-w-2xl{/if}">
        <img src="{$hero->src('w=800')}" width="{$hero->width()}" height="{$hero->height()}">
    </div>
{/if}

draw()

draw() and drawLayers() return a variant URL with shapes and text drawn over it, after the usual options are applied. A layer is a string of the same key=value form as image options. Its first token names the shape: rect, ellipse, line, or text=....

Key Applies to Meaning
x, y, w, h rect, ellipse, text Position and size in px or %. A negative x or y counts from the right or bottom edge. w and h default to 100%
bg, border, border-width rect, ellipse Fill, outline color and outline width
x1, y1, x2, y2, color, width line End points, color and thickness
font, size, color text A font file of the page (or {uuid}:{name} for any page), size in px, and color
align, valign, wrap, line-height, angle text Alignment, wrap width, line height and rotation
<img src="{$page->cover->draw('rect,y=-80,h=80,bg=#000a', 'w=1200,h=630,crop')}">

{* several layers, in order; an array merges into the layer before it *}
<img src="{$page->cover->drawLayers(['rect,y=-80,h=80,bg=#000a', 'text=Hello,x=24,y=-56,color=#fff,size=32', ['text' => $page->title]], 'w=1200,h=630,crop')}">

A layer that is just a name refers to a drawing in images.drawings. A drawing is one layer string or a list of them, and may name other drawings. Pass the values that must not be parsed as a string, such as a title with commas, as an array merged into the layer before it:

{
    "images": {
        "drawings": {
            "scrim": "rect,y=-80,h=80,bg=#000a",
            "caption": ["scrim", "text=,x=24,y=-56,color=#fff,size=32"]
        }
    }
}
<img src="{$page->cover->drawLayers(['caption', ['text' => $page->title]], 'w=1200,h=630,crop')}">

How variants are generated

src() and transform() only register a variant and return its URL. The variant is rendered when that URL is first requested, and after that the web server serves the file statically when the page is open to anyone. A gated page's variants are not mirrored into site/public/. They are kept in site/data/cache/files/ and served through PHP, behind the page's access check.

If w/h don't shrink the source and no effects (including quality=) are requested, the original is mirrored as-is. Otherwise the variant is re-encoded as JPEG, unless format= asks for another, at quality= or the config default.

Config

{
    "images": {
        "driver": "gd",
        "quality": 80
    }
}

driver is the Intervention Image backend, gd or imagick (default imagick). quality is the JPEG quality (1-100) for re-encoded variants.

Example

{* Responsive card image with a blurred inline placeholder while the real variant loads *}
<figure class="relative aspect-[4/3] overflow-hidden rounded-box">
    <img
        src="{$page->hero->base64('w=24,h=18,blur=4')}"
        data-src="{$page->hero->src('card')}"
        srcset="{$page->hero->srcset(['card', 'hero'])}"
        sizes="(min-width: 768px) 50vw, 100vw"
        class="w-full h-full object-cover"
        loading="lazy"
        alt="{$page->title}"
    >
</figure>