Skip to content

Scale

Video Single output Transform

Make a video come out at a specific size — shrink a 4K clip to 1080p, fit phone footage into a widescreen frame, or build a thumbnail. scale is the filter you’ll reach for most, and while it’s resizing it can quietly smooth over colour and format differences between clips. Skim the parameter table at the bottom if you just need a reminder, or read top-to-bottom to actually understand what each knob does.

FFmpeg filter: scale — the official reference.

Add it to a graph. The explicit form works for every filter — bind it to its input and read the result from outputs():

$node = $video->addFilter(new Scale());
$result = $node->outputs()[0];

Shorthand. Single-output filters can skip the node — apply() returns the result stream directly:

$result = $video->apply(new Scale());

Reach for scale whenever the output needs to be a particular size. A few everyday situations:

  • “I have a 4K video and need a 1080p version.” Shrinking to a delivery resolution is what scale does all day.
  • “My clips are different sizes and I want to stack or overlay them.” Compositing filters expect their inputs to line up; scale each clip to a common size first.
  • “I need a thumbnail.” Scale a frame down to a small fixed size.
  • “This footage is the wrong shape for where it’s going.” Scale can fit it — ideally without distorting it (there’s a recipe for that below).

One term worth pinning down up front: aspect ratio is just the width-to-height shape of the frame — 16:9 is widescreen, 4:3 is the older, more square TV shape. Most resizing mishaps come from changing the size while ignoring the aspect ratio, which leaves everyone looking stretched or squashed. Scale gives you a couple of ways to avoid that, covered in the next section.

When another filter is the better tool:

  • To cut away part of the frame (zoom into a corner, trim a letterbox someone already baked in) rather than squash the whole picture, you want to crop — see Crop.
  • To keep the whole picture but add black bars so it fits a different shape (the classic “letterbox”), scale it to fit and then Pad the bars on. That recipe is in the examples.
  • To only re-label the aspect ratio without resampling a single pixel, use Setsar/Setdar.
  • For colour-critical work — HDR, wide-gamut, careful tone-mapping — Zscale is the more precise (and slower) resizer. For everyday SD and HD video, scale is the right, fast choice.

scale’s behaviour comes down to a handful of parameters. Here’s what each one actually does.

Setting the size — width and height. Pass plain numbers (1280, 720), a small formula as a string ('iw/2' means “half the input width”), or one of two special values that keep the shape intact:

  • -1 says “work this dimension out for me so the aspect ratio stays the same.” Ask for a height of 720 and a width of -1, and scale picks the width that keeps the picture from looking stretched.
  • -2 does the same but rounds to an even number — and you should usually prefer it. Here’s why: the most common delivery format, H.264 with yuv420p (the standard way web video stores its colour), can’t handle odd pixel dimensions. A width of -1 might land on 641, and the encode then fails outright. -2 guarantees an even number, so you stay safe.

Leave both unset and the size is unchanged — useful when you only want scale for its colour or format conversion.

Formulas, and when they’re worked out — eval. Width/height formulas can refer to the input size: iw/ih (input width/height), plus a (the aspect ratio), and a few others. By default (eval: ScaleEval::Init) the formula is calculated once, at the start. Switch to eval: ScaleEval::Frame only if your size is meant to change over time and you want it recalculated every frame — it costs a little per frame, so don’t reach for it otherwise.

Choosing the quality — flags. When scale resizes, it has to invent the in-between pixels, and flags picks the maths it uses — a trade of sharpness against speed. 'bilinear' (the default) is fast and slightly soft; 'lanczos' is noticeably sharper and a great all-rounder for both shrinking and enlarging; 'neighbor' is the fastest but looks blocky; 'area' is excellent for shrinking.

Fitting into a box without distortion — force_original_aspect_ratio + force_divisible_by. Give scale a width×height box and let it keep the shape: ScaleForceOar::Decrease shrinks the picture until it fits inside the box (you then usually Pad the leftover space with bars); ScaleForceOar::Increase grows it until it covers the box. Add force_divisible_by: 2 to round the result to an encodable (even) size.

Colour — in_range/out_range and in_color_matrix/out_color_matrix. These convert between colour conventions, and they’re worth understanding before you touch them:

  • Range is how the black-to-white levels are stored. limited (a.k.a. “TV” range) keeps a little headroom at the top and bottom; full (“PC” range) uses the entire scale. Most video is limited range.
  • Matrix (bt709, bt2020, …) is the standard that defines how colour itself is encoded. HD video almost always uses BT.709; standard-definition used an older one; modern wide-gamut and HDR use BT.2020.

The golden rule: leave these alone unless you are deliberately converting. Forcing the wrong range or matrix is a classic cause of washed-out or muddy-looking footage. (The remaining *_chroma_loc / *_chr_pos options control exactly where colour samples sit relative to the brightness samples — genuine edge-case territory.)

param0 / param1 fine-tune particular scalers; interl enables interlaced-aware scaling for old interlaced footage. You’ll rarely touch either.

And one thing scale deliberately does not do: it never changes the frame rate or the duration — only the size and format.

Downscale to 720p, keep the shape, stay encodable. The -2 keeps the aspect ratio and rounds to an even width:

use FFmpeg\Media;
use FFmpeg\Filter\Scale;
$video = Media::open('input.mp4')->videoStream();
$scaled = $video->apply(new Scale(width: -2, height: 720));
Terminal window
ffmpeg -i input.mp4 -vf "scale=-2:720" output.mp4

Half size, using a formula:

$half = $video->apply(new Scale(width: 'iw/2', height: 'ih/2'));
Terminal window
ffmpeg -i input.mp4 -vf "scale=iw/2:ih/2" output.mp4

Letterbox into 16:9 without distortion (scale to fit, then pad the bars). This is the recipe for “make this fit a 1280×720 frame but don’t stretch it” — scale shrinks the picture to fit inside the box, then Pad fills the leftover space with black bars and centres the picture:

use FFmpeg\Filter\{Scale, Pad};
use FFmpeg\Filter\Enum\ScaleForceOar;
$letterboxed = $video
->apply(new Scale(width: 1280, height: 720, force_original_aspect_ratio: ScaleForceOar::Decrease))
->apply(new Pad(width: 1280, height: 720, x: '(ow-iw)/2', y: '(oh-ih)/2'));
Terminal window
ffmpeg -i input.mp4 -vf "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2" output.mp4

The “Ken Burns” effect — turn a still into a slow zoom-and-pan. The move you see over photos in documentaries. Seeing it as a chain is the whole point: scale up so the push-in stays sharp, Crop to the delivery shape first so the zoom can’t stretch a non-16:9 source into an oblong, Zoompan to ease in and drift across the frame, then Scale down to the final size:

use FFmpeg\Media;
use FFmpeg\Filter\{Scale, Zoompan, Crop};
$still = Media::open('blue-marble.png')->videoStream(); // a single still image
$kenBurns = $still
->apply(new Scale(width: 'iw*2', height: 'ih*2')) // headroom so the zoom stays sharp
->apply(new Crop(out_w: 'iw', out_h: 'iw*9/16')) // crop to 16:9 first so it can't be stretched
->apply(new Zoompan(
zoom: 'min(1.05+0.35*on/149,1.4)', // ease 1.05×→1.4× across the whole clip (ends with the pan)
x: '(iw-iw/zoom)*(on/149)', // pan left→right across the slack the zoom opens up
y: '(ih-ih/zoom)*0.5', // hold vertically centred
d: 150, // a still is ONE frame, so d = frames to emit (150 @ 30fps = 5s)
s: '1280x720', // 16:9 in → 16:9 out: no distortion
fps: '30',
))
->apply(new Scale(width: 960, height: 540));
Terminal window
ffmpeg -i blue-marble.png -vf "scale=iw*2:ih*2,crop=iw:iw*9/16,zoompan=z='min(1.05+0.35*on/149,1.4)':x='(iw-iw/zoom)*(on/149)':y='(ih-ih/zoom)*0.5':d=150:s=1280x720:fps=30,scale=960:540" output.mp4

For a moving video source instead of a still, use d: 1 (one output frame per input frame) and drop fps. The clip below is exactly the snippet above, rendered by ext-ffmpeg itself (no command line involved) from NASA’s public-domain “Blue Marble”:

Convert colour to BT.709 limited range (e.g. normalising an older BT.601 clip for HD delivery):

use FFmpeg\Filter\Enum\{ScaleColor, ScaleRange};
$converted = $video->apply(new Scale(
width: 1280,
height: 720,
out_color_matrix: ScaleColor::Bt709,
out_range: ScaleRange::Limited,
));
  • Odd dimensions fail at encode. H.264 with yuv420p needs an even width and height. width: -1 keeps the ratio exactly but can land on an odd number; prefer -2, or add force_divisible_by: 2.
  • Enlarging can’t add detail. Scaling up only stretches what’s already there — flags: 'lanczos' looks sharper than the default, but there’s no extra detail to recover that the camera didn’t capture.
  • Only touch the colour options when you mean to. The range and matrix options default to automatic for a reason; forcing a mismatched one shifts the levels (milky blacks, blown highlights). When in doubt, leave them unset.
  • eval: ScaleEval::Frame is for animation only. It recalculates the size every frame; don’t enable it for a size that never changes.
  • scale is size and format, nothing else. It won’t change the frame rate or duration — use the dedicated filters for those.
  • Crop — remove pixels (change the framing) instead of resampling the whole picture.
  • Pad — add letterbox/pillarbox bars; pairs with force_original_aspect_ratio: Decrease to fit then bar out the remainder (see the letterbox example).
  • Setsar / Setdar — change the stored aspect-ratio label without resampling.
  • Zoompan — the zoom/pan engine behind the Ken Burns example.
  • Zscale — the zimg scaler, for stricter colourspace and HDR conversions.
ParameterTypeDefaultRange / valuesDescription
widthint|float|stringexpressionOutput video width
heightint|float|stringexpressionOutput video height
flagsstringFlags to pass to libswscale
interlboolfalseset interlacing
sizestringset video size
in_color_matrixScaleColorautoauto, smpte170m, bt709, fcc, smpte240m, bt2020set input YCbCr type
out_color_matrixScaleColor2auto, smpte170m, bt709, fcc, smpte240m, bt2020set output YCbCr type
in_rangeScaleRangeunknownunknown, full, limitedset input color range
out_rangeScaleRangeunknownunknown, full, limitedset output color range
in_chroma_locScaleChromaLocunknownunknown, left, center, topleft, top, bottomleft, bottomset input chroma sample location
out_chroma_locScaleChromaLocunknownunknown, left, center, topleft, top, bottomleft, bottomset output chroma sample location
in_v_chr_posint-513-513512input vertical chroma position in luma grid/256
in_h_chr_posint-513-513512input horizontal chroma position in luma grid/256
out_v_chr_posint-513-513512output vertical chroma position in luma grid/256
out_h_chr_posint-513-513512output horizontal chroma position in luma grid/256
force_original_aspect_ratioScaleForceOardisabledisable, decrease, increasedecrease or increase w/h if necessary to keep the original AR
force_divisible_byint11256enforce that the output resolution is divisible by a defined integer when force_original_aspect_ratio is used
param0floatScaler param 0
param1floatScaler param 1
evalScaleEvalinitinit, framespecify when to evaluate expressions

FFmpeg also names these options (use the parameter shown above): wwidth, hheight, ssize.


Maps to FFmpeg’s scale filter. Verified against ffmpeg n7.1.1.

An Artisan Build project.

Built on FFmpeg — an independent binding, not affiliated with or endorsed by the FFmpeg project. Support FFmpeg.