# Chain Desk > A FILTER CHAIN IS A COMPOSITION, AND COMPOSITION DOES NOT COMMUTE. > `crop=1080:1080:420:0,scale=1080:1920` on a 1920x1080 frame gives 1080x1920 made of > 1080x1080 at (420,0) of the source. The same two filters the other way round > DOES NOT RUN: a crop at (420,0) of 1080x1080 runs off a 1080x1920 frame. > THE SMALLEST RECTANGLE ANYWHERE IN THE CHAIN IS THE CEILING ON EVERYTHING AFTER > IT: a chain through 960x540 that delivers 1920x1080 invents 75% of what it > ships, and a CROP is a bottleneck too. AND COMPOSING THE CHAIN BACKWARDS says which > rectangle of the ORIGINAL frame the output is made of, and where it sat. Five lanes > over one chain, plus a free browser-side engine that treats every geometry filter > as an affine map and inverts the whole chain exactly. URL: https://chain-desk.skillsafe.ai/ API: https://chain-desk.skillsafe.ai/api.html Token panel: https://chain-desk.skillsafe.ai/tokens.html (noindex) Platform: SkillSafe (https://skillsafe.ai/) Derived from the `ffmpeg-video-editor` skill in https://github.com/sundial-org/awesome-openclaw-skills, which generates ffmpeg commands from natural-language editing requests. Those commands are exactly the work object here. Not affiliated with or endorsed by the authors of that repository. ## The one thing to know **A FILTER CHAIN IS A COMPOSITION, AND COMPOSITION DOES NOT COMMUTE.** ``` crop=1080:1080:420:0,scale=1080:1920 1080x1920 from 1080x1080 at (420,0) scale=1080:1920,crop=1080:1080:420:0 DOES NOT RUN - a crop at (420,0) of 1080x1080 runs off a 1080x1920 frame ``` The same two filters. One order is the vertical cut-down everybody writes; the other **does not run at all**, because after the scale the frame is smaller than the crop. And when both orders DO run they are usually different pictures - a crop's offsets are measured on the frame that step is handed, so moving the crop past a scale changes which pixels it takes. **THE SMALLEST RECTANGLE ANYWHERE IN THE CHAIN IS THE CEILING ON EVERYTHING AFTER IT.** ``` scale=1280:720,scale=1920:1080 through 1280x720 -> 55.6% of the output invented scale=960:540,scale=1920:1080 through 960x540 -> 75% of the output invented ``` A chain that goes down and comes back up ships pixels that were interpolated rather than photographed, and the share is `1 - bottleneck / output` exactly. **A crop is a bottleneck too**: cropping a quarter of the frame and delivering full size invents the same share as scaling to half and back, because it is the same number of pixels. No later step can put them back. **AND COMPOSING THE CHAIN BACKWARDS ANSWERS THE QUESTION PEOPLE ACTUALLY HAVE.** Each geometry step is an affine map on coordinates plus a rectangle, so the whole chain is one map and its inverse is exact: **which rectangle of the ORIGINAL frame this output is made of, and where it sat**. A filter string never says. This page does, in pixels, for every step. **FINALLY THE GRIDS ARE NOT SUGGESTIONS.** In 4:2:0 chroma is subsampled by 2 in each direction, so an odd dimension is half a chroma sample at the edge and an odd crop OFFSET shifts luma against chroma by half a chroma sample - colour fringes on hard edges that no decoder can undo. `-2` in a scale keeps the aspect and rounds to even; `-1` keeps the aspect and takes whatever comes out. They differ by one pixel and one of them fails an encode. ## What each filter does | Filter | Changes the size | Changes the source region | Note | | --- | --- | --- | --- | | `scale=w:h` | yes | no - the same region, resampled | one resample. `-1` keeps the aspect, `-2` keeps it and rounds to even | | `crop=w:h:x:y` | yes | **yes - it moves and shrinks it** | offsets are measured on the frame THIS step is handed, not on the source | | `pad=w:h:x:y` | yes | no - it adds pixels with no source | so the output's pixel count and the source region stop dividing | | `transpose=n` | swaps them | swaps the axes | and swaps what every later crop and scale is talking about | | `hflip`, `vflip` | no | no - the same region, read mirrored | but a flip before and after a crop are different pictures | | `format=pix` | no | no | its POSITION decides whether an odd crop offset is a chroma problem | | `fps`, `setsar` | no | no | no geometry at all, and both are somebody else's subject | | `rotate`, `overlay`, `scale2ref` | yes | yes | **this engine does not model them** and says so rather than guessing | Each geometry step is an affine map on coordinates plus a rectangle, so the whole chain is one map — which is what makes the inverse question answerable: **which rectangle of the ORIGINAL frame this output is made of**, and where it sat. A filter string never answers that about itself. ## The same two filters, both ways round | Chain | Output | From the source | Smallest picture | | --- | --- | --- | --- | | `crop=1080:1080:420:0,scale=1080:1920` | 1080x1920 | 1080x1080 at (420,0) | 1080x1080 | | `scale=1080:1920,crop=1080:1080:420:0` | **does not run** | a crop at (420,0) of 1080x1080 runs off a 1080x1920 frame | - | | `scale=960:540,crop=480:270:240:135` | 480x270 | 960x540 at (480,270) | 480x270 | | `crop=960:540:480:270,scale=1920:1080` | 1920x1080 | 960x540 at (480,270) | 960x540 | | `crop=iw/2:ih:0:0,scale=1920:1080` | 1920x1080 | 960x1080 at (0,0) | 960x1080 | | `scale=1920:1080,crop=iw/2:ih:0:0` | 960x1080 | 960x1080 at (0,0) | 960x1080 | On a 1920x1080 source. **Every row is the same two filters as the row above or below it, in the other order** — and no two rows agree. The first pair is the ordinary vertical cut-down: one order crops then scales and works, the other scales then crops and **does not run at all**, because after the scale the frame is smaller than the crop. The `iw/2` pair at the bottom is the sharpest one: the same two filters, both orders run, and the OUTPUTS ARE DIFFERENT SIZES - because `iw` means the width of the frame THAT STEP IS HANDED, so the same text means 1920 in one order and 960 in the other. ## What a bottleneck costs | Chain | Smallest picture | Output | Upscale from it | Invented | | --- | --- | --- | --- | --- | | `scale=1920:1080` | 1920x1080 | 1920x1080 | 1x | 0% | | `scale=1280:720,scale=1920:1080` | 1280x720 | 1920x1080 | 2.25x | **55.6%** | | `scale=960:540,scale=1920:1080` | 960x540 | 1920x1080 | 4x | **75%** | | `scale=640:360,scale=1920:1080` | 640x360 | 1920x1080 | 9x | **88.9%** | | `scale=320:180,scale=1920:1080` | 320x180 | 1920x1080 | 36x | **97.2%** | | `crop=960:540:480:270,scale=1920:1080` | 960x540 | 1920x1080 | 4x | **75%** | From a 1920x1080 source in every row. **The smallest rectangle anywhere in a chain is the ceiling on everything after it**, so the invented share is `1 - bottleneck / output` and it is arithmetic rather than opinion. Note the last row: a crop is a bottleneck too, so cropping a quarter of the frame and delivering full size invents 75% of what goes out — the same as scaling down to half and back up, because it is the same number of pixels. ## The grids a dimension has to land on | Grid | Every | What happens past it | | --- | --- | --- | | chroma, 4:2:0 | 2 px | an odd dimension is half a chroma sample at the edge | | chroma offset, 4:2:0 | 2 px | an odd crop offset shifts luma against chroma | | macroblock, h.264 | 16 px | the encoder pads and signals a crop in the bitstream | | even, yuv420p | 2 px | the encoder refuses the frame outright | **`-1` and `-2` in a scale differ by at most one pixel, and one of them is always even.** `-1` keeps the aspect and takes whatever number comes out; `-2` does the same and rounds to even. In yuv420p that difference is the whole game: chroma is subsampled by 2 in each direction, so an odd dimension is half a chroma sample at the edge and an odd crop OFFSET shifts luma against chroma by half a chroma sample. The second one is worse, because it is a colour artefact rather than an error message. ## Constants and thresholds | Constant | Value | What it decides | | --- | --- | --- | | chroma subsampling | 2 x 2 | in 4:2:0 every dimension and every crop offset is on this grid | | even | 2 | yuv420p refuses an odd dimension outright | | macroblock | 16 | h.264 pads the frame to this and signals a crop in the bitstream | | resamples worth naming | more than 2 | above this the chain is running a kernel over the picture repeatedly | | invented share worth warning about | 25% | above this the output is substantially a scaler's invention | | source discarded worth naming | 50% | below this share kept, the chain is a reframe rather than a transcode | | stretch worth naming | 1% | any difference between the two axis factors above this | | assumed source | 1920x1080 | used when SOURCE is missing, and reported every time | | assumed format | yuv420p | decides whether the chroma-grid findings apply at all | **The first three are the formats', not this page's policy** — 4:2:0's chroma grid, yuv420p's refusal of odd dimensions and h.264's macroblock are what those formats are, and they are in this table so a reader can check them rather than take them on trust. Everything below is a threshold this page chose, and the harness proves each one is load-bearing by moving it and requiring the report to move with it. ## Chain grammar Paste the `-vf` string, or the whole ffmpeg command - the filter string is taken out of it and everything else is ignored rather than half-read. A few optional `KEY: value` lines above it say what the frame is. ``` JOB: episode 4 vertical cut-down a name for the record SOURCE: 1920x1080 the frame the chain is applied to TARGET: 1080x1920 what the delivery spec says, if any FORMAT: yuv420p decides whether the chroma grid applies CHAIN: crop=1080:1080:420:0,scale=1080:1920 or just: ffmpeg -i in.mov -vf "scale=-2:720,format=yuv420p" -crf 20 out.mp4 ``` **`SOURCE` IS THE LINE THAT MATTERS MOST.** Every rectangle on this page is measured from it, and a chain with `iw/2` in it means a different number of pixels at a different source size. Without it the page assumes 1920x1080 and says so on every answer. **THE FILTERS THAT ARE FOLLOWED EXACTLY** are `scale`, `crop`, `pad`, `transpose`, `hflip`, `vflip`, `format`, `fps` and `setsar`. Named arguments work (`crop=w=1080:h=1080:x=420:y=0`), a crop or pad with no offsets is centred the way ffmpeg centres it, and `iw`, `ih`, `-1` and `-2` are resolved against the frame **that step is handed** - which is the entire reason the order matters. **A FILTER_COMPLEX IS READ AS A CHAIN.** Pad labels like `[0:v]` and `[v]` are stripped, and the filters are followed in the order they appear. A graph that genuinely branches - two inputs, an `overlay`, a `split` - is not a chain, and the filters that join or move frames are reported as unfollowed rather than misinterpreted. **ANYTHING ELSE IN THE CHAIN IS KEPT AND COUNTED.** An unknown filter is treated as changing no geometry, which is true of most of them; the ones known to move the frame are named in the findings so the answer is partial rather than confident. ## Lanes | Lane | What it answers | Sections | | --- | --- | --- | | `plan` | Write the chain from what you want | Summary, The Chain, The Numbers, Reasoning, Next Step | | `check` (primary) | What this chain actually does to the picture | Summary, Verdict, Findings, Corrected Chain, Next Step | | `order` | The order question: which swaps change the picture | Summary, Step By Step, Every Swap, What The Order Buys, Next Step | | `pixels` | The pixel question: what survives, and what was invented | Summary, The Source Region, The Bottleneck, What Survives, Next Step | | `deliver` | Decide what changes: the order, the numbers, or the source | Summary, A Reorder Fixes, Only Different Numbers Fix, Nothing Fixes, Next Step | | If the question is | Ask | Because | | --- | --- | --- | | nothing is written yet | `plan` | it derives the chain from the source and the delivery, in the order that runs, instead of writing filters and discovering the picture. | | it runs and the picture looks wrong | `check` | the full read: every step, the source region, the bottleneck, the stretch and the order. | | does the order matter | `order` | it runs the chain both ways for every adjacent pair, so the answer is a fact about your numbers rather than a rule about filter names. | | why is it soft | `pixels` | the smallest rectangle in the chain is the ceiling, and the invented share is exactly `1 - bottleneck / output`. | | we know it is wrong, what do we change | `deliver` | sorts the findings into the order, the numbers, and nothing. | ## Findings All 38 are computed in the browser and cost nothing: 2 errors, 16 warnings, 20 notes. | Code | Severity | Scope | What it says | | --- | --- | --- | --- | | `NO-CHAIN` | error | chain | no filters found | | `THE-CHAIN-DOES-NOT-RUN` | error | chain | a step cannot run on the frame it is handed | | `A-CROP-WRITTEN-FOR-THE-SOURCE` | warn | order | the crop fits the source and not the frame it got | | `THE-CHAIN-IS-A-COMPOSITION` | note | chain | what goes in, what comes out, and how many resamples | | `WHICH-PIXELS-OF-THE-SOURCE-SURVIVE` | note | source | the rectangle of the original this output is made of | | `THE-SCALE-FACTOR-END-TO-END` | note | scale | the factor from that rectangle to the output | | `MOST-OF-THE-OUTPUT-WAS-INVENTED` | warn | bottleneck | the chain upscales past its own smallest rectangle | | `THE-BOTTLENECK-IS-BEFORE-THE-END` | note | bottleneck | the same thing, below the threshold worth warning about | | `THE-BOTTLENECK-IS-THE-END` | note | bottleneck | the output is the smallest picture in the chain | | `MOST-OF-THE-SOURCE-IS-DISCARDED` | warn | source | the chain keeps less than half the source | | `ORDER-CHANGES-THE-PICTURE` | warn | order | adjacent filters that do not commute | | `EVERY-ORDER-GIVES-THE-SAME-PICTURE` | note | order | every adjacent pair commutes on these numbers | | `A-SWAP-WOULD-NOT-RUN-AT-ALL` | warn | order | reordering the chain would break it | | `MORE-THAN-TWO-RESAMPLES` | warn | scale | more scale steps than the picture needs | | `TWO-RESAMPLES` | note | scale | two scale steps | | `ONE-RESAMPLE` | note | scale | one scale step, which is the floor | | `TWO-ADJACENT-SCALES-COLLAPSE-INTO-ONE` | warn | scale | two scales with nothing between them | | `THE-CHAIN-STRETCHES-THE-PICTURE` | warn | scale | the two axes are scaled by different factors | | `THE-OUTPUT-IS-BIGGER-THAN-THE-SOURCE` | warn | scale | the chain delivers more pixels than it was given | | `AN-ODD-DIMENSION-IN-A-SUBSAMPLED-FORMAT` | warn | grid | a step produces an odd width or height | | `EVERY-DIMENSION-IS-EVEN` | note | grid | every step lands on the chroma grid | | `AN-ODD-CROP-OFFSET-MOVES-THE-CHROMA` | warn | grid | a crop starts off the chroma grid | | `THE-CROP-OFFSETS-ARE-EVEN` | note | grid | every crop starts on the chroma grid | | `THE-OUTPUT-IS-NOT-A-WHOLE-NUMBER-OF-MACROBLOCKS` | note | grid | the output is not a multiple of 16 | | `A-SCALE-USED--1-RATHER-THAN--2` | note | grid | a scale takes whatever number keeps the aspect | | `A-CROP-AFTER-A-PAD` | warn | chain | the chain pads and then crops | | `A-SCALE-AFTER-A-PAD` | warn | chain | the border gets scaled with the picture | | `PADDED-PIXELS-HAVE-NO-SOURCE` | note | source | the pad adds pixels that came from nowhere | | `TRANSPOSE-SWAPS-THE-AXES` | note | chain | and swaps what every later filter means | | `A-FLIP-DOES-NOT-MOVE-THE-RECTANGLE` | note | chain | a flip changes which pixel lands where, not which rectangle is used | | `A-CROP-AND-A-SCALE-THAT-COMMUTE` | note | order | a fragile coincidence worth naming | | `NOTHING-CHANGES-THE-GEOMETRY` | note | chain | no step moves the frame | | `THE-CHAIN-REACHES-THE-TARGET` | note | chain | the output is the stated target size | | `THE-CHAIN-DOES-NOT-REACH-THE-TARGET` | warn | chain | the output is not the stated target size | | `A-FILTER-THIS-ENGINE-CANNOT-FOLLOW` | warn | chain | a filter whose geometry is not computed here | | `LINES-COULD-NOT-BE-READ` | warn | sheet | lines the parser could not use | | `THE-CHAIN-CAME-OUT-OF-A-COMMAND-LINE` | note | sheet | the filter string was extracted from a whole command | | `ASSUMED-VALUES` | note | sheet | what was not stated, and what was assumed | **Severity means one thing here.** An `error` is something the engine cannot compute at all — there are 2 of those, and one of them is the chain not running, where ffmpeg would stop too. A `warn` is a consequence somebody has to decide about. A `note` is the arithmetic itself. Nothing that fires on every usable chain is a warning. ## What this page cannot do This page computes from the chain you paste and the source size above it. It has not decoded a frame, run ffmpeg, or looked at a pixel. - **It follows the geometry filters and no others.** `scale`, `crop`, `pad`, `transpose`, `hflip`, `vflip`, `format`, `fps` and `setsar` are computed exactly. `rotate`, `overlay`, `scale2ref`, the stack filters and anything else that moves the frame are kept in the chain, reported as unfollowed, and treated as changing nothing - so an answer that involves one is partial and says so. - **It reads the expressions people actually write, not all of them.** Integers, `-1`, `-2`, `iw`, `ih` and one multiply or divide are resolved against the frame the step is handed. A full ffmpeg expression - `min(iw,ih)`, `if(gt(...))`, `t*30` - is not, and is reported as unfollowed rather than guessed at. - **The rounding is ffmpeg's convention, not a measurement.** `-1` rounds to the nearest integer and `-2` to the nearest even one; a real build can differ by a pixel on an odd input, and a crop with no offsets is centred with the same floor-division this page uses. Where a single pixel decides something, check against your own build. - **Quality is not modelled, only pixel counts.** The invented share says how many output pixels had no input pixel behind them. It says nothing about the scaler - lanczos, bicubic and neighbour all produce the same count and very different pictures - and nothing about whether the result looks acceptable. - **This is geometry, not colour.** Scaling in a gamma-encoded space rather than linear light changes edges, `format` conversions can be lossy, and neither is computed here. The chroma findings are about the SAMPLE GRID and not about colour accuracy. - **The aspect-ratio metadata is somebody else's subject.** This page works in pixels. A non-square SAR means the shape on screen is not the shape in the pixel grid, and `setsar` is followed only in that it changes no pixels. - Nothing here reaches the network, reads a file, or runs a command. ## API `POST https://api.skillsafe.ai/v1/app-api/run` with an app session token. The body is the input object itself - there is no `input` wrapper and no `X-App-Slug` header. Every lane takes `task` plus its own fields: | Lane | Fields | | --- | --- | | `plan` | `brief` (required), `known` | | `check` | `sheet` (required), `worry` | | `order` | `sheet` (required) | | `pixels` | `sheet` (required) | | `deliver` | `sheet` (required), `fixed` | `sheet` is the chain itself: a `-vf` string, a `filter_complex`, or a whole ffmpeg command line, with optional KEY: value lines giving SOURCE, TARGET and FORMAT. `GET /v1/app-api/estimate` prices a lane for nothing and is the way to check an input shape without spending credits. Full worked examples in eight languages are at https://chain-desk.skillsafe.ai/api.html. ## Provenance The affine model, the inverse map, the bottleneck arithmetic, the commutation test, the thresholds and every finding are this app's own. The chroma grid, the even- dimension rule and the macroblock are the formats', and they are in the constants table above so a reader can check them rather than take them on trust. _Generated from `chain.js` by `build-skill.js`, like SKILL.md, so the two documents and the app cannot disagree._