# Where the SMPTE RP 219 pattern came from The bars in this app were **measured**, not transcribed from a description. This document records how, so the numbers can be re-derived and challenged. ## Why not just read the spec SMPTE RP 219 is a paywalled standard, and every free description of it on the web is a redrawing of a redrawing. The layouts disagree with each other in the bottom row, and almost none of them state the Y′CbCr code values at all. Building from one of those would have produced something that looks like colour bars and measures like nothing in particular. ffmpeg's `smptehdbars` source filter is a real, widely used, independently maintained implementation. Rendering it and reading the pixels back gives ground truth that can be checked by anyone with ffmpeg installed. ## The two traps Both of these produce output that looks completely plausible and is wrong. ### 1. `-pix_fmt rgb24` uses the wrong matrix ``` ffmpeg -f lavfi -i smptehdbars=size=1920x1080 -frames:v 1 -pix_fmt rgb24 out.png ``` This renders 75% yellow as **(189, 202, 7)**. The correct Rec.709 value is **(191, 191, 0)**. The conversion applied a BT.601 matrix to HD content — the classic HD/SD colour-matrix mix-up, reproduced here by the tool itself. Any implementation built by sampling that PNG inherits the error. So the RGB output is used **only for geometry**, and the colour comes from the Y′CbCr planes with the matrix applied explicitly in `src/lib/colour.ts`. ### 2. Default chroma upsampling smears the band edges `smptehdbars` renders to a chroma-subsampled format. Converting to `yuv444p` without saying how will interpolate the chroma, which: - shifts the apparent row boundaries by ~3px (the first measurement put the first row band at y=627 instead of 630, with six 1px transition bands after it), and - returns chroma values near every band edge that are close to right and not right. `-sws_flags neighbor` pins it to nearest-neighbour and the bands come back sharp. ## The extraction `scripts/extract-rp219-reference.py` does both correctly: ```bash python3 scripts/extract-rp219-reference.py > test/fixtures/rp219-ffmpeg.json ``` It renders at **1920x1080, 3840x2160, 1280x720 and 2048x1080**, run-length encodes each row and column, and writes the boundaries and code values as JSON. Four sizes matter: three are 16:9 and one (DCI 2K) is not, so a model that fits all four is proportional rather than a 1920-specific coincidence. ## The result Rows, as fractions of raster height — identical at all four sizes: | Row | Height | Content | |-----|--------|---------| | A | 7/12 | 40% grey, then 75% white/yellow/cyan/green/magenta/red/blue, then 40% grey | | B | 1/12 | 100% cyan, +I, 75% white (6 bars wide), 100% blue | | C | 1/12 | 100% yellow, +Q, luma ramp (6 bars wide), 100% red | | D | 3/12 | 15% grey, black, 100% white, black, pluge, black, 15% grey | Columns, as fractions of raster width: ``` a = 1/8 the two grey blocks at each end b = 3/28 one colour bar 2a + 7b = 1/4 + 3/4 = 1 (closes exactly) ``` Row D in units of `b`: `a | 3b/2 | 2b | 5b/6 | b/3 ×5 | b | a`, where the five `b/3` patches are the pluge: **−2%, 0%, +2%, 0%, +4%**. ## The colours Every value except +I and +Q is **computed** from its R′G′B′ definition through the Rec.709 matrix, not tabulated. Each computed value was checked against the measurement and all fifteen agree to the code value: | Patch | Computed from | Measured Y′CbCr | |-------|---------------|-----------------| | 75% white | (0.75, 0.75, 0.75) | 180, 128, 128 | | 75% yellow | (0.75, 0.75, 0) | 168, 44, 136 | | 75% cyan | (0, 0.75, 0.75) | 145, 147, 44 | | 75% green | (0, 0.75, 0) | 133, 63, 52 | | 75% magenta | (0.75, 0, 0.75) | 63, 193, 204 | | 75% red | (0.75, 0, 0) | 51, 109, 212 | | 75% blue | (0, 0, 0.75) | 28, 212, 120 | | 100% cyan | (0, 1, 1) | 188, 154, 16 | | 100% blue | (0, 0, 1) | 32, 240, 118 | | 100% yellow | (1, 1, 0) | 219, 16, 138 | | 100% red | (1, 0, 0) | 63, 102, 240 | | 40% grey | 40% of the black→white span | 104, 128, 128 | | 15% grey | 15% | 49, 128, 128 | | black / white | 0% / 100% | 16 / 235 | | pluge | −2%, +2%, +4% | 12, 20, 25 | **+I and +Q are the exception.** They are the NTSC I/Q-axis chroma reference signals, not a colour, and cannot be derived from an R′G′B′ triple. They are carried as measured constants: +I = (57, 156, 97), +Q = (44, 171, 147). That everything else falls out of the matrix is itself the strongest evidence the measurement is sound — fifteen independent values reproduced by one formula. ## One deliberate difference from ffmpeg ffmpeg rounds each bar **up** and dumps the accumulated remainder into the final column, so its right-hand grey block is narrower than its left-hand one — by 2px at 1920x1080 and by **6px at 1280x720**. This implementation rounds each cumulative boundary to nearest, keeping the two grey blocks equal and every edge within half a pixel of nominal. An asymmetric test pattern is a defect, so matching ffmpeg exactly here would be the wrong call. `rp219.test.ts` therefore requires the **row** boundaries to match exactly and allows column boundaries to differ by up to 8px, while separately asserting that the bands tile the raster with no gaps and that the total area is exact. ## The level range question RP 219 is defined in Y′CbCr studio swing. A PNG is RGB, and there is no way for a PNG to say which convention it was written in — so the app asks, and records the answer in the ZIP manifest. - **Full range (0–255)** — reference black is 0, white is 255. What a media server, a GPU output or an HDMI link set to full range expects. The pluge patches at −2% clip to 0 and cannot be judged. - **Legal range (16–235)** — studio swing. Sub-black and super-white survive, so the pluge is meaningful. On a full-range display it looks washed out, correctly. Full range is the default because the common job is checking a media server's outputs, not lining up an SDI chain.