Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

PixelFlow User Book

PixelFlow is a Rust-first video processing framework inspired by VapourSynth.

What this book covers

This book documents current Phase 1 user-facing workflows for:

  • rendering .pf scripts with pixelflow,
  • writing scripts with source(...) and filters,
  • using built-in std filters,
  • configuring FFMS2-backed sources, and
  • writing Rust plugins with pixelflow-plugin-sdk.

Start here

Getting started

Prerequisites

  • pixelflow binary available locally.
  • An input video file readable through FFMS2.
  • Write access to output path.

Create a minimal script

Create hello.pf:

output = source("input.mkv")

Render output

Run:

pixelflow hello.pf -o out.y4m

This command reads script, indexes reachable source(...) nodes, validates final graph, and writes an output stream selected from the final clip format. Integer Gray/YUV clips write Y4M; planar RGB clips write rawvideo.

Write to stdout instead of a file

Run:

pixelflow hello.pf -o - > out.y4m

Video bytes go to stdout. Progress, diagnostics, and timing output stay on stderr.

Next steps

CLI

Current command model

pixelflow supports three top-level workflows: rendering a script, printing final-output metadata for a script with --info, and printing version information with the version subcommand. Normal render invocation requires script path and -o output path. --info requires a script path but does not require -o.

Logging

The CLI writes PixelFlow log records at warn and error level to stderr by default. Use --loglevel LEVEL to lower or raise the minimum level. Accepted levels are trace, debug, info, warn, and error.

Use --logfile PATH to write log records to a file instead of stderr. This redirects only PixelFlow log records; render output, --info metadata, progress bars, timing reports, final render status line, and fatal top-level errors keep their documented stdout/stderr behavior.

Tracing

When built with the tracing Cargo feature, --trace FILE writes a Perfetto-compatible trace that can be opened in https://ui.perfetto.dev/. Trace output is separate from stdout, stderr diagnostics, log records, progress bars, timing reports, rendered video bytes, and props JSON.

Builds without the tracing feature accept the flag as a no-op so the same command line can be used with tracing and non-tracing binaries.

Plugin directories

pixelflow scans conventional platform plugin directories by default. Set PF_PLUGIN_DIRS to replace that list entirely. The same directory list is used for compiled plugins and script plugins.

PF_PLUGIN_DIRS uses the operating system path-list rules. In typical shell usage:

PF_PLUGIN_DIRS=/opt/pixelflow/plugins:$HOME/pixelflow/plugins pixelflow script.pf -o out.y4m

On Windows, use ; between entries instead of :. Empty path-list entries are ignored. Set PF_PLUGIN_DIRS to an empty value to disable plugin directory scanning.

Info pipeline

pixelflow --info <SCRIPT> reads and evaluates the script, indexes reachable FFMS2 sources so source media is concrete, validates the graph topology, prints final output clip dimensions, frame count, constant frame rate, and PixelFlow format name to stdout, then exits before executor construction and frame rendering.

Render pipeline

Current CLI flow is:

  1. read script source,
  2. evaluate script through pixelflow_script::ScriptEngine,
  3. index reachable FFMS2 sources,
  4. validate final graph,
  5. build built-in executors for reachable built-in filters,
  6. verify all reachable graph nodes have executors,
  7. render ordered frames to Y4M or rawvideo based on the final clip format.

Output limits

Current CLI output accepts only final clips with:

  • fixed format,
  • fixed resolution,
  • finite frame count,
  • constant frame rate, and
  • either Y4M-compatible integer Gray/YUV format or planar RGB format.

Convert unsupported final clips explicitly before rendering.

Render scripts

Required arguments

pixelflow <SCRIPT> -o <PATH>
  • <SCRIPT> is path to script file.
  • -o <PATH> selects output destination. PixelFlow chooses the stream format from the final clip format.
  • -o - writes the selected stream to stdout.

Command reads script, indexes reachable source(...) nodes, validates final graph, and writes either Y4M or rawvideo output.

Flags

--set NAME=VALUE

Pass script parameters as named values.

--start N

Start render at zero-based frame index N.

--end N

Stop before frame index N.

--threads N

Set the maximum render worker thread count. N must be a positive non-zero integer. PixelFlow may use fewer workers when the render graph’s ordering or concurrency limits would leave extra workers idle.

--timings

Print a per-node timing report to stderr after render completes and before the final render status line. Each row reports the number of frames produced by that node and its cumulative exclusive execution time. Dependency waits and ordered commit waits are not counted in the node total.

--trace FILE

Write a Perfetto-compatible trace to FILE when the binary is built with the tracing Cargo feature. The trace captures runtime setup, source indexing, scheduler waits, dependency requests, cache behavior, executor prepare/commit work, selected expensive filter phases, frame plane allocation/deallocation events, output writes, and final flushes.

Builds without the tracing feature accept --trace FILE as a no-op and do not create the file. --trace always requires a file path, cannot use -, and cannot use the same path as -o, --props, or --logfile.

DHAT heap profiling

Use the dhat-heap Cargo feature when you need call stacks and lifetime data for all heap allocations, not only frame plane storage. DHAT writes dhat-heap.json when the process exits normally or after a runtime error path that reaches PixelFlow’s main error handler.

cargo run -p pixelflow-cli --release --features dhat-heap -- demo.pf -o out.y4m

Open dhat-heap.json in DHAT’s viewer. Frame plane allocations should include AlignedBuffer::new_zeroed in their stack.

--props FILE

Write frame properties to FILE as JSON after rendering completes. The output is an array with one object per rendered frame, in the same order as the video output. Frame ranges from --start and --end also apply to the props array.

Unset properties are written as null. Boolean, integer, float, string, and array values use their matching JSON types. Rational values are written as objects with numerator and denominator fields. Binary blob values are written as arrays of byte values.

--props always writes to a file. It cannot be combined with --info, cannot use -, and cannot use the same path as -o.

--loglevel LEVEL

Set the minimum PixelFlow log level. The default is warn, which emits warnings and errors. Accepted values are trace, debug, info, warn, and error.

--logfile PATH

Write PixelFlow log records to PATH instead of stderr. Progress bars, timing reports, and the final render status line still use stderr.

Examples

pixelflow demo.pf -o out.y4m
pixelflow demo.pf -o - > out.y4m
pixelflow demo.pf -o out.y4m --set width=1280 --set fps=24000/1001
pixelflow demo.pf -o out.y4m --start 100 --end 200
pixelflow demo.pf -o out.y4m --threads 4 --timings
pixelflow demo.pf -o out.y4m --trace trace.pftrace
cargo run -p pixelflow-cli --features tracing -- demo.pf -o out.y4m --trace trace.pftrace
cargo run -p pixelflow-cli --release --features dhat-heap -- demo.pf -o out.y4m
pixelflow demo.pf -o out.y4m --props props.json
pixelflow demo.pf -o out.y4m --loglevel info --logfile pixelflow.log

Output formats

PixelFlow auto-detects the final clip format:

  • Integer Gray and YUV clips are written as Y4M.
  • Planar RGB clips are written as FFmpeg-compatible rawvideo.

Rawvideo has no header. When piping or importing RGB output into ffmpeg, pass the pixel format, frame size, and frame rate explicitly. PixelFlow planar RGB maps to FFmpeg planar GBR names:

PixelFlow formatFFmpeg -pixel_format
rgbp8gbrp
rgbp10gbrp10le
rgbp12gbrp12le
rgbp16gbrp16le
rgbpf32gbrpf32le

Example for a 1920x1080 RGB clip at 24000/1001 fps:

pixelflow rgb.pf -o - | ffmpeg -f rawvideo -pixel_format gbrp -video_size 1920x1080 -framerate 24000/1001 -i - out.mkv

Progress output

When stderr is connected to an interactive terminal, PixelFlow shows indexing and rendering progress with transient progress bars. Progress bars are hidden in non-interactive shells so CI logs and redirected diagnostics avoid transient progress output.

After a successful render, PixelFlow prints a final render status line to stderr:

Output 500 frames in 2.40 seconds (208.73 fps)

When rendering to stdout with -o -, stdout carries only the selected output stream. Indexing progress may still appear on interactive stderr, rendering progress is suppressed, and the final render status line still goes to stderr.

Info output

pixelflow --info <SCRIPT>
pixelflow -i <SCRIPT>

--info evaluates a PixelFlow script, indexes reachable source(...) nodes, reads the final output clip metadata, prints it to stdout, and exits without rendering the full output video.

--set NAME=VALUE, --threads N, --loglevel LEVEL, --logfile PATH, and --trace FILE may be combined with --info. Render-only options such as -o, --start, --end, and --timings are not accepted with --info.

Output format

Type: Video
Width: 1920
Height: 1080
Frames: 14315
FPS: 24/1 (24.000 fps)
Format Name: yuv420p10

The metadata describes the evaluated script’s final output clip. Indexing progress, when shown in an interactive terminal, is written to stderr. PixelFlow log records are written to stderr by default or to --logfile PATH when configured. Trace output, when enabled with --trace FILE, is written only to the trace file. The metadata block is the only stdout output.

GUI previewer

pixelflow-gui opens a desktop preview window for PixelFlow .pf scripts.

Launching

Run without arguments to open a blank window with a centered Open Script button:

pixelflow-gui

Run with a script path to open it immediately:

pixelflow-gui path/to/script.pf

While the startup script is loading, the blank window shows a loading indicator instead of the Open Script button. The GUI accepts only .pf input paths.

Plugin directories

pixelflow-gui scans conventional platform plugin directories by default. Set PF_PLUGIN_DIRS to replace that list entirely. The same directory list is used for compiled plugins and script plugins.

PF_PLUGIN_DIRS uses the operating system path-list rules. In typical shell usage, separate multiple entries with : on Unix and macOS or ; on Windows. Empty path-list entries are ignored. Set PF_PLUGIN_DIRS to an empty value to disable plugin directory scanning.

Preview workflow

When a script opens successfully, the previewer evaluates the script, indexes reachable FFMS2 sources, validates the final graph, prepares built-in executors, and renders frame 0 by default.

Use any of the navigation controls below the preview image to inspect the clip:

  • frame number input,
  • zoom dropdown, and
  • position bar.

Entering an empty frame number seeks to frame 0. Entering a frame number greater than the final frame seeks to the final frame instead.

Side panel

The side panel has two tabs:

  • Props lists metadata props for the current rendered frame.
  • Histogram shows one level histogram per plane for the current rendered frame.

Histogram graphs use the pixel value on the X axis and the number of pixels in each bucket on the Y axis. Integer formats use the logical pixel-value range from 0 to the maximum value supported by the format bit depth. Float formats use 0.0 to 1.0. Higher bit depths and float formats are bucketed to fit the side panel.

The histogram tints the left and right edges of each graph to show values outside the limited legal range for that plane.

Keyboard shortcuts

When the preview window has keyboard focus, use these shortcuts:

ActionWindows/LinuxmacOS
Move back 1 frameLeftLeft
Move forward 1 frameRightRight
Move back 25 framesCtrl+LeftCmd+Left
Move forward 25 framesCtrl+RightCmd+Right
Reload the current script and refresh the selected frameCtrl+RCmd+R
Quit the GUICtrl+QCmd+Q

When the frame number input has keyboard focus, plain Left and Right edit the text cursor instead of navigating frames. The Ctrl/Cmd shortcuts still act on the preview.

Zoom and scrolling

The zoom dropdown offers these fixed percentages:

  • 25%
  • 50%
  • 75%
  • 100%
  • 200%
  • 400%
  • 800%
  • 1600%

Hold Ctrl and scroll the mouse wheel while the pointer is over the preview panel to zoom one step at a time. Plain mouse-wheel scrolling pans the zoomed preview when scrollbars are available. You can also pan by dragging the preview with the primary mouse button or by dragging the scrollbars.

Status bar

When a script is open, the status bar shows:

  • format,
  • resolution,
  • frame count,
  • FPS, and
  • duration.

While a frame render is pending, the status bar appends a small text spinner and Loading frame. While the current script is reloading, it appends the same spinner and Reloading script.

Version output

Basic version output

Run:

pixelflow version

This prints current PixelFlow version string.

Verbose version output

Run:

pixelflow version --verbose

Current verbose output includes:

  • PixelFlow core version,
  • enabled feature summary,
  • plugin ABI version,
  • loaded compiled plugin list, and
  • loaded script plugin filter list.

The enabled feature summary includes tracing when the binary was built with the tracing Cargo feature.

--loglevel LEVEL, --logfile PATH, and --trace FILE may be supplied before the version subcommand. --trace FILE is most useful with version --verbose, which initializes runtime plugin discovery and script plugin discovery before printing the plugin list. Discovery uses conventional platform plugin directories unless PF_PLUGIN_DIRS is set, in which case the env path list replaces the conventional list.

version --verbose validates script plugin namespaces and filter definitions during discovery. Namespace or symbol collisions fail the command. Malformed script plugin files that do not collide are skipped with warning diagnostics through the configured logger.

If no compiled plugins or script plugin filters load, verbose output prints plugins: none.

Scripting

PixelFlow scripts build a graph. They do not render frames directly.

Required output

Every valid script must assign exactly one top-level final clip to output.

These cases are errors:

  • missing output,
  • multiple top-level output assignments,
  • non-clip value assigned to output.

Script responsibilities

Current script layer is responsible for:

  • parameter injection,
  • source and filter call coercion,
  • namespace sugar rewrite,
  • graph construction.

Real source probing and FFMS2 indexing happen later in source indexing, not while parsing script text.

Diagnostics

Script parse and evaluation failures can include source-backed diagnostic frames. When PixelFlow can map an error back to script text, it reports the script or plugin path, the original line number, and the source line content in a script backtrace block.

Syntax and call forms

Use supported call form that best fits your script. Paired examples in same section are alternate syntax for same operation.

Generic filter calls

output = filter(source("input.mkv"), "resize", #{ width: 1280, height: 720 })
output = source("input.mkv").resize(#{ width: 1280, height: 720 })

Zero-input filters can also use the function form directly:

output = filter("blank_clip", #{ width: 1280, height: 720 })

Built-in std namespace

output = std.resize(source("input.mkv"), #{ width: 1280, height: 720 })
output = source("input.mkv").std.resize(#{ width: 1280, height: 720 })

Zero-input built-ins use the namespace form without a clip argument:

output = std.blank_clip(#{ width: 1280, height: 720 })

Third-party plugin namespace

output = plugin.acme.blur(source("input.mkv"), #{ radius: 2 })
output = source("input.mkv").plugin.acme.blur(#{ radius: 2 })

Multi-input filters

Use array input form when filter consumes more than one clip:

y = source("input.mkv").std.split_plane(#{ plane: 0 })
u = source("input.mkv").std.split_plane(#{ plane: 1 })
v = source("input.mkv").std.split_plane(#{ plane: 2 })
output = std.merge_planes([y, u, v], #{ format: "yuv420p8" })

Metadata property reads

Use prop(clip, key) to read a metadata property from frame 0, or prop(clip, frame, key) to read a specific zero-based frame.

clip = source("input.mkv")
output = clip

if prop(clip, 0, "core:matrix") == "bt709" {
    output = clip.std.convert_colorspace(#{ matrix: "bt2020_ncl" })
}

The method form is equivalent:

clip = source("input.mkv")
frame_number = clip.prop(10, "core:frame_number")
output = clip

None metadata values return none(). Use is_none(value) before comparing or passing optional metadata into filters.

Top-level frame metadata reads

Use these accessors to read structural frame fields from frame 0, or pass a zero-based frame number to inspect another frame. They are runtime-backed like prop(...): the script host may index sources and render the requested frame while evaluating the script.

clip = source("input.mkv")

width = clip.width()
height = clip.height()
format = clip.format()
family = clip.format_family()
subsampling = clip.chroma_subsampling()
bits = clip.bit_depth()
planes = clip.plane_count()

output = clip.std.resize(#{ width: width / 2, height: height / 2 })

Function forms are also supported:

clip = source("input.mkv")
width_10 = width(clip, 10)
format_10 = format(clip, 10)
output = clip

Accessors return these values:

  • width, height, bit_depth, and plane_count return integers.
  • format returns PixelFlow’s canonical pixel format string, such as yuv420p10.
  • format_family returns gray, yuv, or planar_rgb.
  • chroma_subsampling returns 4:4:4, 4:2:2, 4:2:0, or none() for non-YUV formats.

Negative frame numbers are rejected. Frame numbers outside the rendered clip range fail during runtime resolution.

Script parameters

Pass script parameters with --set NAME=VALUE.

Name rules

Parameter names must be valid script identifiers.

Value coercions

Current coercion rules are:

  • true / false -> bool,
  • whole numbers -> int,
  • finite decimal numbers -> float,
  • rational text such as 24000/1001 -> rational,
  • anything else -> string.

After injection, script can use parameter names like normal variables.

Example

pixelflow demo.pf -o out.y4m --set width=1280 --set fps=24000/1001 --set title=sample

Sources

Use source("path") to create FFMS2-backed source nodes in scripts.

Relative paths

Relative source paths resolve against script directory during source indexing.

Script evaluation only records source requests in graph. Real probing and indexing happen later.

Source options

Pass source options as map:

output = source("input.mkv", #{ cache: "input.ffms2.pfidx", format: "yuv420p10" })

Read FFMS2 source for exact option rules.

Composing filters

Chaining one-input filters

output = source("input.mkv")
    .std.resize(#{ width: 1280, height: 720 })
    .std.crop(#{ left: 0, top: 0, width: 1280, height: 700 })

Combining multiple clips

y = source("input.mkv").std.split_plane(#{ plane: 0 })
u = source("input.mkv").std.split_plane(#{ plane: 1 })
v = source("input.mkv").std.split_plane(#{ plane: 2 })
output = std.merge_planes([y, u, v], #{ format: "yuv420p8" })
a = source("part-1.mkv")
b = source("part-2.mkv")
output = std.join([a, b])
left = source("left.mkv")
right = source("right.mkv")
output = std.stack_horizontal([left, right])

Multi-input filters use array input form. Every array entry must be a clip. Filters define their own media-matching rules. For example, std.join([a, b]) requires fixed format, fixed resolution, finite frame count, and constant positive frame rate on every input; matching format, resolution, and frame rate across inputs; and allows different input frame counts.

Metadata-only multi-input filters also use array input form:

target = source("graded.mkv")
metadata_source = source("original.mkv")
output = std.copy_props([metadata_source, target])

Metadata-aware workflows

register_prop extends metadata schema at script time so metadata-validating filters can plan against custom keys.

Register key before filter validates or writes plugin-defined metadata:

register_prop("acme/filter:enabled", "bool")

output = source("input.mkv")
    .std.set_prop(#{ key: "acme/filter:enabled", value: true })
    .std.require_prop(#{ key: "acme/filter:enabled", kind: "bool", value: true })

Use this when downstream filter needs custom metadata key with known type.

Reading metadata in scripts

prop retrieves a frame metadata value during script evaluation. This lets a script choose graph structure or filter options from metadata on an already-built clip prefix.

register_prop("acme/filter:enabled", "bool")

clip = source("input.mkv")
    .std.set_prop(#{ key: "acme/filter:enabled", value: true })

enabled = clip.prop("acme/filter:enabled")
output = clip

if enabled == true {
    output = clip.std.resize(#{ width: 1280, height: 720 })
}

Use prop(clip, frame, key) when the value may differ by frame. Prop reads can index sources and render the requested frame while the script is being evaluated, so prefer reading only the metadata needed to choose the graph.

Built-in filters

Official built-in filters register under std. The FFMS2-backed source(...) call is documented alongside them because it is the normal entry point for clips used with those filters.

For the canonical pixel format and color metadata strings accepted by source, convert_format, convert_colorspace, and merge_planes, see Format and color metadata strings.

Call forms

  • Use bare source(...) for FFMS2 input.
  • Use std.blank_clip(...) for generated clips that do not need an input clip.
  • Use std.<filter>(input, #{ ... }) or clip.std.<filter>(#{ ... }) for one-input built-in filters such as add_borders, binarize, convolution, deflate, flip_horizontal, flip_vertical, inflate, levels, median, resize, reverse, rotate, crop, expr, limit, and convert_colorspace.
  • Use array input form for multi-input filters such as std.expr([a, b], #{ expr: "x y + 2 /" }), std.masked_merge([a, b, mask]), std.interleave_frames([a, b]), std.join([a, b]), std.stack_horizontal([left, right]), std.stack_vertical([top, bottom]), std.merge_planes([y, u, v], #{ format: "yuv420p8" }), and std.copy_props([metadata_source, target]).

Filter families

  • source input: source(...)
  • generation: blank_clip
  • conversion: convert_bit_depth, convert_colorspace, convert_format
  • adjustment and math: binarize, deflate, expr, inflate, levels, limit, masked_merge
  • spatial: add_borders, convolution, crop, flip_horizontal, flip_vertical, median, resize, rotate, split_plane, merge_planes, stack_horizontal, stack_vertical
  • metadata: set_prop, clear_prop, copy_props, require_prop
  • temporal: assume_fps, delete_frames, duplicate_frames, interleave_frames, join, replace_frames, reverse, select, select_every, trim

Format and color metadata strings

This reference lists the canonical string values used by built-in filters that accept format, matrix, transfer, or primaries options.

Pixel format strings are matched case-insensitively and normalized to the canonical lowercase names below. Color metadata strings use canonical lowercase names only; aliases such as "rec709" are not accepted.

Pixel format strings

These canonical pixel format names are accepted by filters that use PixelFlow’s general format parser, including convert_format and merge_planes.

Canonical stringFamilyNotes
gray8GraySingle-plane 8-bit integer gray.
gray10GraySingle-plane 10-bit integer gray.
gray12GraySingle-plane 12-bit integer gray.
gray16GraySingle-plane 16-bit integer gray.
grayf32GraySingle-plane 32-bit float gray.
yuv420p8YUV 4:2:0Three-plane 8-bit integer YUV.
yuv420p10YUV 4:2:0Three-plane 10-bit integer YUV.
yuv420p12YUV 4:2:0Three-plane 12-bit integer YUV.
yuv420p16YUV 4:2:0Three-plane 16-bit integer YUV.
yuv420pf32YUV 4:2:0Three-plane 32-bit float YUV.
yuv422p8YUV 4:2:2Three-plane 8-bit integer YUV.
yuv422p10YUV 4:2:2Three-plane 10-bit integer YUV.
yuv422p12YUV 4:2:2Three-plane 12-bit integer YUV.
yuv422p16YUV 4:2:2Three-plane 16-bit integer YUV.
yuv422pf32YUV 4:2:2Three-plane 32-bit float YUV.
yuv444p8YUV 4:4:4Three-plane 8-bit integer YUV.
yuv444p10YUV 4:4:4Three-plane 10-bit integer YUV.
yuv444p12YUV 4:4:4Three-plane 12-bit integer YUV.
yuv444p16YUV 4:4:4Three-plane 16-bit integer YUV.
yuv444pf32YUV 4:4:4Three-plane 32-bit float YUV.
rgbp8Planar RGBThree-plane 8-bit integer RGB.
rgbp10Planar RGBThree-plane 10-bit integer RGB.
rgbp12Planar RGBThree-plane 12-bit integer RGB.
rgbp16Planar RGBThree-plane 16-bit integer RGB.
rgbpf32Planar RGBThree-plane 32-bit float RGB.

Accepted pixel format aliases

These aliases resolve to the canonical names above anywhere PixelFlow accepts a pixel format string:

Accepted aliasCanonical name
graygray8
yuv420pyuv420p8
yuv422pyuv422p8
yuv444pyuv444p8
rgbprgbp8
gbrprgbp8
gbrp8rgbp8
gbrp10rgbp10
gbrp12rgbp12
gbrp16rgbp16
gbrpf32rgbpf32

FFMS2 source() pixel format subset

source(..., #{ format: ... }) accepts only this subset of the pixel format names above. The general aliases still work when they resolve to one of these canonical names. When format is omitted, native FFMS2 source detection must also map to one of these canonical formats; unsupported native formats require an explicit format conversion request.

Supported canonical string
gray8
gray10
gray12
gray16
yuv420p8
yuv420p10
yuv420p12
yuv420p16
yuv422p8
yuv422p10
yuv422p12
yuv422p16
yuv444p8
yuv444p10
yuv444p12
yuv444p16

Matrix coefficients strings

Use these exact strings for matrix and source_matrix options.

StringMeaning
identityIdentity matrix coefficients.
bt709ITU-R BT.709 matrix coefficients.
bt470mITU-R BT.470 System M matrix coefficients.
bt470bgITU-R BT.470 System B/G matrix coefficients.
smpte170mSMPTE 170M matrix coefficients.
smpte240mSMPTE 240M matrix coefficients.
ycgcoYCgCo matrix coefficients.
bt2020_nclITU-R BT.2020 non-constant-luminance matrix coefficients.
bt2020_clITU-R BT.2020 constant-luminance matrix coefficients.
smpte2085SMPTE ST 2085 matrix coefficients.
chromaticity_derived_nclChromaticity-derived non-constant-luminance matrix coefficients.
chromaticity_derived_clChromaticity-derived constant-luminance matrix coefficients.
ictcpICtCp matrix coefficients.

Range strings

Use these exact strings for range options and core:range metadata.

StringMeaning
fullFull-range code values.
limitedLimited-range code values.

Transfer characteristic strings

Use these exact strings for transfer and source_transfer options.

StringMeaning
bt1886ITU-R BT.1886 transfer function.
bt470mITU-R BT.470 System M transfer function.
bt470bgITU-R BT.470 System B/G transfer function.
smpte170mSMPTE 170M transfer function.
smpte240mSMPTE 240M transfer function.
linearLinear-light transfer function.
log100Logarithmic curve with 100:1 range.
log316Logarithmic curve with 316:1 range.
xvyccxvYCC transfer function.
bt1361eITU-R BT.1361 extended-color-gamut transfer function.
srgbIEC sRGB transfer function.
bt2020_10ITU-R BT.2020 10-bit transfer function.
bt2020_12ITU-R BT.2020 12-bit transfer function.
pqSMPTE ST 2084 perceptual quantizer.
smpte428SMPTE ST 428 transfer function.
hlgARIB STD-B67 hybrid log-gamma.

Color primaries strings

Use these exact strings for primaries and source_primaries options.

StringMeaning
bt709ITU-R BT.709 primaries.
bt470mITU-R BT.470 System M primaries.
bt470bgITU-R BT.470 System B/G primaries.
smpte170mSMPTE 170M primaries.
smpte240mSMPTE 240M primaries.
filmFilm primaries.
bt2020ITU-R BT.2020 primaries.
smpte428SMPTE ST 428 primaries.
dci_p3DCI-P3 primaries.
display_p3Display P3 primaries.

Chroma siting strings

Use these exact strings for chroma_siting options and core:chroma_siting metadata.

StringMeaning
leftChroma samples sit on the left edge and are centered vertically.
centerChroma samples are centered horizontally and vertically.
top_leftChroma samples sit on the top-left corner.
topChroma samples are centered horizontally on the top edge.
bottom_leftChroma samples sit on the bottom-left corner.
bottomChroma samples are centered horizontally on the bottom edge.

Field order strings

Use these exact strings for core:field_order metadata.

StringMeaning
progressiveProgressive frame.
top_field_firstInterlaced frame whose top field is displayed first.
bottom_field_firstInterlaced frame whose bottom field is displayed first.

source

source(
  path: String,
  #{
    cache?: String,
    fps?: Rational,
    vfr?: String,
    format?: String,
    track?: Int,
    threads?: Int,
  }
)

Creates a clip from an FFMS2-backed video source.

Relative source paths and relative cache paths resolve against the script directory when one is known. If the source file does not report a usable frame rate, you must supply fps. When format is omitted, PixelFlow preserves the source track’s native format when it maps to PixelFlow’s supported FFMS2 source format subset. Sources whose timestamps drift away from the reported or explicit CFR schedule are normalized to CFR by default; timestamp rounding from coarse container time bases is tolerated. vfr: "passthrough" is reserved but unsupported. When you do not set cache, PixelFlow derives a default .ffms2.pfidx path automatically. If FFMS2 rejects a matching cache because it was generated by an incompatible FFMS2/FFmpeg/libav version, PixelFlow treats it as stale, rebuilds it, and overwrites the cache.

FFMS2 sources declare random-access scheduling to PixelFlow, but the current FFMS2 executor still uses one active source request per source node. PixelFlow starts those limit-one source requests in increasing frame-number order so parallel render workers do not turn sequential source-only renders into random seeks. The threads option controls FFMS2 decoder threads.

Examples

output = source("input.mkv")
output = source(
    "input.mkv",
    #{
        cache: "custom.ffms2.pfidx",
        fps: 24000/1001,
        format: "yuv420p10",
        track: 0,
        threads: 4,
        vfr: "normalize",
    },
)
output = source("input.mkv", #{ threads: 0 })

Input

A source file path plus optional FFMS2 indexing and decode options.

Output

A CFR clip decoded from the selected video track in the native or requested pixel format.

source() prepopulates all PixelFlow core metadata keys. When FFMS2 reports a supported value, frames include core:matrix, core:transfer, core:primaries, core:range, core:chroma_siting, core:field_order, core:frame_number, core:duration, and core:source_path. Unsupported or unspecified source values remain none.

Options

  • cache (String) – Override the cache file path used for the FFMS2 index.
  • fps (Rational) – Override the source frame rate used for output timing and VFR detection; required when the file does not provide a usable one.
  • vfr (String, default: implicit CFR normalization) – Select VFR handling; only "normalize" is accepted explicitly. Detection compares source timestamps against the reported or explicit CFR schedule with tolerance for container timestamp rounding.
  • format (String, default: omitted/native) – Select the output pixel format from the FFMS2 source() pixel format subset. When omitted, PixelFlow preserves the detected native source format if it maps to that subset. If the native format is unsupported, pass format to request conversion to a supported format.
  • track (Int) – Select a specific zero-based video track; when omitted, PixelFlow uses the first indexed video track.
  • threads (Int, default: auto-detected available CPU parallelism) – Set FFMS2 decode threads. Use 0 to request the same auto-detected default.

add_borders

add_borders(
  input: Clip,
  #{
    left?: Int,
    right?: Int,
    top?: Int,
    bottom?: Int,
    color?: String | Array<Int> | Array<Float>,
  }
)

Expands one clip by adding constant-color borders around the input frame.

The input must be one fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip. Border sizes are measured in full-resolution pixels and must be non-negative. For subsampled YUV formats, border sizes must align to the chroma plane divisors.

color defaults to "#000000". Hex colors use #RRGGBB form and are interpreted as full-range RGB display colors. Gray and YUV clips convert that RGB color with a fixed BT.709-style mapping; the conversion does not inspect clip color metadata. For exact plane values, pass an array whose length matches the input plane count. Integer formats require integer array values in the valid sample range. Float formats accept finite integer or float array values.

Examples

output = source("input.mkv").std.add_borders(#{ left: 16, right: 16, top: 8, bottom: 8 })
output = source("input.mkv").std.add_borders(#{
  left: 2,
  right: 2,
  top: 2,
  bottom: 2,
  color: [16, 128, 128],
})
output = source("input.mkv")
  .std.convert_format(#{ format: "rgbp8" })
  .std.add_borders(#{ left: 24, right: 24, color: "#336699" })

Input

One fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, timing, and metadata as the input, expanded by the requested border sizes.

Options

  • left (Int, default: 0) – Set the left border size in full-resolution pixels.
  • right (Int, default: 0) – Set the right border size in full-resolution pixels.
  • top (Int, default: 0) – Set the top border size in full-resolution pixels.
  • bottom (Int, default: 0) – Set the bottom border size in full-resolution pixels.
  • color (String | Array<Int> | Array<Float>, default: "#000000") – Set the border color as #RRGGBB or exact per-plane sample values.

assume_fps

assume_fps(
  input: Clip,
  #{
    fps: Rational | Int | String,
  }
)

Changes a clip’s declared constant frame rate without adding, dropping, reordering, or modifying frames.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip. fps must be positive. String values must use numerator/denominator syntax, integer values are treated as n/1, and floats are rejected.

Examples

output = source("input.mkv").std.assume_fps(#{ fps: "24000/1001" })

Pass fps through a script parameter:

output = source("input.mkv").std.assume_fps(#{ fps: fps })
pixelflow retime.pf -o out.y4m --set fps=30000/1001

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, resolution, frame count, and per-frame metadata, but a new declared constant frame rate.

Options

  • fps (Rational | Int | String) – Set the output constant frame rate.

binarize

binarize(
  input: Clip,
  #{
    threshold?: Int | Float | Array<Int> | Array<Float>,
    lower?: Int | Float | Array<Int> | Array<Float>,
    upper?: Int | Float | Array<Int> | Array<Float>,
    planes?: Array<Int>,
  }
)

Sets each processed pixel to upper when the input sample is greater than or equal to threshold, or to lower when the input sample is below threshold.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip. Integer formats use Int option values within the format’s code range, such as 0..=255 for 8-bit or 0..=1023 for 10-bit. Float formats use Float option values within 0.0..=1.0.

If an option is supplied as a scalar value, it applies to every processed plane. If an option is supplied as an array and planes is omitted, the array length must match the input plane count. If planes is supplied, each array length must match the number of listed planes and array entries map to the listed planes by position. Duplicate plane indexes are rejected.

upper does not need to be greater than lower. This lets you create inverted binary output by setting upper lower than lower.

For float clips, NaN input samples compare as below threshold, so they map to lower.

Examples

output = source("input.mkv").std.binarize()
output = source("input.mkv").std.binarize(#{
  threshold: 128,
  lower: 16,
  upper: 235,
  planes: [0],
})
output = source("input.mkv").std.binarize(#{
  threshold: [96, 128, 160],
  lower: [0, 128, 128],
  upper: [255, 255, 255],
})
output = source("input.mkv")
  .std.convert_format(#{ format: "grayf32" })
  .std.binarize(#{ threshold: 0.5, lower: 0.0, upper: 1.0 })

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, resolution, timing, and metadata, with selected planes replaced by binary lower/upper samples.

Options

  • threshold (Int | Float | Array<Int> | Array<Float>, default: midpoint of the format range) – Compare each input sample against this value. Integer defaults use the upper midpoint, such as 128 for 8-bit and 512 for 10-bit. Float defaults use 0.5.
  • lower (Int | Float | Array<Int> | Array<Float>, default: minimum format value) – Output sample for input samples below threshold. Integer defaults use 0. Float defaults use 0.0.
  • upper (Int | Float | Array<Int> | Array<Float>, default: maximum format value) – Output sample for input samples greater than or equal to threshold. Integer defaults use the format maximum. Float defaults use 1.0.
  • planes (Array<Int>, default: all planes) – Zero-based plane indexes to process. Unlisted planes are preserved without modification. An empty array processes no planes.

blank_clip

blank_clip(
  clip?: Clip,
  #{
    width?: Int,
    height?: Int,
    format?: String,
    length?: Int,
    frame_rate?: Rational | Int | String,
    color?: String | Array<Int> | Array<Float>,
  }
)

Generates a fixed-format, fixed-resolution clip filled with one constant color.

When clip is provided, blank_clip copies the input clip’s format, width, height, length, and frame rate. Any explicitly set option overrides the copied property. Built-in defaults are used only when there is no input clip for that property. color is not inherited and defaults to "#000000".

width, height, and length must be positive. The selected frame rate must be positive. For subsampled formats, the selected width and height must align to the format’s chroma plane divisors. format accepts the canonical pixel format strings and aliases listed in Format and color metadata strings.

color accepts #RRGGBB or exact per-plane sample values. Hex colors are interpreted as full-range RGB display colors. Gray and YUV clips convert that RGB color with the same fixed BT.709-style mapping used by add_borders; the conversion does not inspect clip color metadata. For exact plane values, pass an array whose length matches the output plane count. Integer formats require integer array values in the valid sample range. Float formats accept finite integer or float array values.

frame_rate accepts rational values, integers treated as n/1, or strings in numerator/denominator form. If length is omitted and no input clip is provided, blank_clip defaults to ceil(10 * frame_rate) frames.

Examples

output = std.blank_clip()
output = std.blank_clip(#{
  width: 1920,
  height: 1080,
  format: "rgbp8",
  length: 120,
  frame_rate: "24000/1001",
  color: "#336699",
})
input = source("input.mkv")
output = input.std.blank_clip(#{ color: [16, 128, 128] })

Input

Zero or one clip. If a clip is provided, its media properties are used as defaults.

Output

A generated clip with the selected format, dimensions, length, frame rate, and constant color.

Options

  • width (Int, default: 1280 without input) – Set the output width in pixels.
  • height (Int, default: 720 without input) – Set the output height in pixels.
  • format (String, default: "yuv420p8" without input) – Set the output pixel format.
  • length (Int, default: ceil(10 * frame_rate) without input) – Set the output frame count.
  • frame_rate (Rational | Int | String, default: 30/1 without input) – Set the output constant frame rate.
  • color (String | Array<Int> | Array<Float>, default: "#000000") – Set the generated color as #RRGGBB or exact per-plane sample values.

convert_bit_depth

convert_bit_depth(
  input: Clip,
  #{
    bits: Int,
    dither?: String,
  }
)

Changes a clip’s bit depth without changing its resolution or timing.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip. The target depth must be one of PixelFlow’s supported bit depths: 8, 10, 12, 16, or 32 for float output. dither only affects lossy integer down-conversion and float-to-integer conversion.

Examples

output = source("input.mkv").std.convert_bit_depth(#{ bits: 10 })
output = source("input.mkv").std.convert_bit_depth(#{ bits: 8, dither: "none" })

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with the same family, resolution, timing, and metadata, converted to the requested sample depth.

Options

  • bits (Int) – Select the target bit depth.
  • dither (String, default: "fruit") – Select the dither mode for lossy conversions; supported values are "fruit" and "none".

convert_colorspace

convert_colorspace(
  input: Clip,
  #{
    range?: String,
    matrix?: String,
    transfer?: String,
    primaries?: String,
    chroma_siting?: String,
    dither?: String,
  }
)

Changes color metadata and, when needed, pixel values without changing the clip format or resolution.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip, and you must set at least one colorspace option. PixelFlow expects canonical metadata names such as "bt709" rather than aliases such as "rec709". Some conversions are metadata-only, while others need render-time source metadata such as core:range, core:matrix, core:transfer, core:primaries, and core:chroma_siting. Missing or unsupported metadata can still fail render.

Examples

output = source("input.mkv").std.convert_colorspace(#{ range: "full" })
output = source("input.mkv").std.convert_colorspace(#{
    matrix: "bt709",
    transfer: "bt1886",
    primaries: "bt709",
    chroma_siting: "left",
})

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, resolution, and timing, with colorspace metadata rewritten and pixel values converted as needed.

Options

  • range (String) – Set the output color range; supported values are "full" and "limited".
  • matrix (String) – Set the output matrix using PixelFlow’s canonical matrix names. See Matrix coefficients strings.
  • transfer (String) – Set the output transfer characteristic using PixelFlow’s canonical transfer names. See Transfer characteristic strings.
  • primaries (String) – Set the output color primaries using PixelFlow’s canonical primaries names. See Color primaries strings.
  • chroma_siting (String) – Set the output chroma siting; supported values are "left", "center", "top_left", "top", "bottom_left", and "bottom".
  • dither (String, default: "fruit") – Select the dither mode for lossy range conversions; supported values are "fruit" and "none".

convert_format

convert_format(
  input: Clip,
  #{
    format: String,
    range?: String,
    matrix?: String,
    transfer?: String,
    primaries?: String,
    chroma_siting?: String,
    source_range?: String,
    source_matrix?: String,
    source_transfer?: String,
    source_primaries?: String,
    source_chroma_siting?: String,
  }
)

Changes a clip’s pixel format without changing its resolution or timing.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip. convert_format can change family and subsampling, but it cannot change bit depth or sample storage type; use convert_bit_depth first when needed. Cross-family conversions depend on correct source color metadata or explicit source_* overrides, and some target or source metadata axes are only valid for certain families. Planar RGB to YUV or Gray conversion also requires a target matrix.

Examples

output = source("input.mkv").std.convert_format(#{ format: "yuv444p10" })
output = source("input.mkv").std.convert_format(#{
    format: "rgbp10",
    source_matrix: "bt709",
    source_transfer: "bt1886",
    source_primaries: "bt709",
})

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with the requested pixel format, the same resolution and timing, and metadata preserved or normalized for the output family.

Options

  • format (String) – Select the target pixel format. See Pixel format strings.
  • range (String) – Override the target output range.
  • matrix (String) – Override the target output matrix. See Matrix coefficients strings.
  • transfer (String) – Override the target output transfer characteristic. See Transfer characteristic strings.
  • primaries (String) – Override the target output color primaries. See Color primaries strings.
  • chroma_siting (String) – Override the target output chroma siting.
  • source_range (String) – Override the source range metadata used during conversion.
  • source_matrix (String) – Override the source matrix metadata used during conversion. See Matrix coefficients strings.
  • source_transfer (String) – Override the source transfer metadata used during conversion. See Transfer characteristic strings.
  • source_primaries (String) – Override the source primaries metadata used during conversion. See Color primaries strings.
  • source_chroma_siting (String) – Override the source chroma siting metadata used during conversion.

convolution

convolution(
  input: Clip,
  #{
    matrix: Array<Int | Float>,
    bias?: Int | Float,
    divisor?: Int | Float,
    planes?: Array<Int>,
    saturate?: Bool,
    mode?: String,
  }
)

Performs spatial convolution on selected planes using a user-supplied kernel.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip. All sample types (u8, u16, f32) are supported.

The kernel is supplied as a flat row-major array. Square mode ("s", the default) requires exactly 9 values (3x3) or 25 values (5x5). Horizontal, vertical, and separable modes accept any odd length from 3 through 25.

Edge pixels use periodic reflection. Border coordinates are mirrored without repeating the edge sample, so the filter always evaluates a full neighborhood even at image borders, on single-pixel-wide planes, and when the kernel is larger than the plane.

Matrix coefficients must be finite and within -1023..=1023. For integer sample formats, f64 coefficient values are rounded to the nearest i16 before use. For float sample formats, coefficients are cast to f32.

When divisor is omitted or set to 0, the effective divisor is the sum of the runtime coefficients. If that sum is near zero (absolute value below f32::EPSILON), the divisor defaults to 1. An explicit non-zero divisor must have a finite f32 reciprocal.

Integer output uses f32 division and bias, rounds ties to even, and clamps to the declared bit-depth code range. Float output is not clamped.

When saturate is true (the default), float output preserves signed values and integer output clamps negative results to 0 through the normal output clamp. When saturate is false, the absolute value of the result is taken before any clamping.

If planes is omitted or set to (), every plane is processed. If planes is provided, only the listed zero-based plane indexes are processed; all other planes are preserved without modification and share their input storage. An empty array processes no planes and the input frames are returned unchanged.

Examples

Gaussian-like 3x3 blur with default divisor (coefficient sum = 16):

output = source("input.mkv").std.convolution(#{
  matrix: [1, 2, 1, 2, 4, 2, 1, 2, 1],
})

Horizontal Sobel edge detection on the luma plane only, with absolute output:

output = source("input.mkv").std.convolution(#{
  matrix: [-1, 0, 1],
  mode: "h",
  divisor: 1,
  saturate: false,
  planes: [0],
})

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, resolution, timing, and metadata, with selected planes replaced by the convolution result.

Options

  • matrix (Array<Int | Float>) – Flat row-major kernel coefficients. Square mode requires 9 or 25 values. One-dimensional modes require an odd length from 3 through 25. Values must be finite and within -1023..=1023.
  • bias (Int | Float, default: 0) – Additive value applied to the output sample after division.
  • divisor (Int | Float, default: 0) – Divides the accumulated sum before bias is added. A value of 0 selects the coefficient sum as the divisor; if the sum is near zero, the divisor becomes 1.
  • planes (Array<Int>, default: all planes) – Zero-based plane indexes to process. Unlisted planes are preserved without modification. An empty array processes no planes.
  • saturate (Bool, default: true) – When true, float output preserves signed values and integer output clamps negative results to 0. When false, the absolute value of the result is taken.
  • mode (String, default: "s") – Kernel arrangement. "s" for square (3x3 or 5x5), "h" for horizontal, "v" for vertical, "hv" or "vh" for separable (vertical pass then horizontal pass, each finalized independently). Case-insensitive.

crop

crop(
  input: Clip,
  #{
    left?: Int,
    top?: Int,
    width?: Int,
    height?: Int,
    right?: Int,
    bottom?: Int,
  }
)

Returns a rectangular view of one clip without changing its format or timing.

The input must be one fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip. left, top, right, and bottom must be non-negative. width and height must be positive when supplied. Use exactly one of width and right, and exactly one of height and bottom. The crop rectangle must stay inside the source frame. For subsampled YUV formats, all crop edges must also stay aligned to the chroma plane divisors.

Examples

output = source("input.mkv").std.crop(#{ left: 0, top: 0, width: 1280, height: 720 })
output = source("input.mkv").std.crop(#{ left: 8, top: 4, right: 8, bottom: 4 })

Input

One fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, timing, and metadata, cropped to the requested rectangle.

Options

  • left (Int, default: 0) – Set the left crop offset in full-resolution pixels.
  • top (Int, default: 0) – Set the top crop offset in full-resolution pixels.
  • width (Int) – Set the cropped output width. Mutually exclusive with right.
  • height (Int) – Set the cropped output height. Mutually exclusive with bottom.
  • right (Int) – Set the number of full-resolution pixels to crop from the right side. Mutually exclusive with width.
  • bottom (Int) – Set the number of full-resolution pixels to crop from the bottom side. Mutually exclusive with height.

delete_frames

delete_frames(
  input: Clip,
  #{
    frames: Array<Int>,
  }
)

Removes specific source frames from an input clip. Remaining frames keep their original order and are re-indexed after all deletions are applied.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count. frames contains zero-indexed source frame numbers from the input clip. The array may be empty, which returns a no-op clip. Entries must be non-negative, unique, and less than the input frame count. frames may be in any order, but it must not name every frame in the input clip.

Examples

output = source("input.mkv").std.delete_frames(#{ frames: [0, 10, 20] })
# No-op; useful when a script parameter expands to an empty deletion list.
output = source("input.mkv").std.delete_frames(#{ frames: [] })

Input

One fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count.

Output

A clip with the same format, resolution, frame rate, and per-frame metadata, containing all source frames except those listed in frames.

Options

  • frames (Array<Int>) – Zero-indexed source frames to delete.

duplicate_frames

duplicate_frames(
  input: Clip,
  #{
    frames: Array<Int>,
  }
)

Duplicates specific source frames from an input clip. Each duplicate is inserted immediately after its source frame, and output frames are re-indexed after all duplications are applied.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count. frames contains zero-indexed source frame numbers from the input clip. The array may be empty, which returns a no-op clip. Entries must be non-negative and less than the input frame count. The array may contain the same frame multiple times; each entry adds one more duplicate of that source frame. Entry order does not change output order: duplicates are grouped with their source frame in source-timeline order.

Examples

output = source("input.mkv").std.duplicate_frames(#{ frames: [0, 10, 20] })
# Frame 12 appears three times total: the original plus two duplicates.
output = source("input.mkv").std.duplicate_frames(#{ frames: [12, 12] })
# No-op; useful when a script parameter expands to an empty duplicate list.
output = source("input.mkv").std.duplicate_frames(#{ frames: [] })

Input

One fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count.

Output

A clip with the same format, resolution, frame rate, and per-frame metadata, containing all source frames plus requested duplicates inserted after their source frames.

Options

  • frames (Array<Int>) – Zero-indexed source frames to duplicate.

interleave_frames

interleave_frames(
  input: [Clip, Clip, ...]
)

Interleaves frames from two or more clips in input order. For inputs with frames [A0 A1 A2 A3] and [B0 B1 B2 B3], the output is [A0 B0 A1 B1 A2 B2 A3 B3].

Use array input form: std.interleave_frames([a, b]). Every input clip must use fixed format, fixed resolution, finite frame count, and constant frame rate. All inputs must have identical format, resolution, frame count, and frame rate.

The output frame count is the input frame count multiplied by the number of inputs. The output frame rate is multiplied by the number of inputs so the output duration remains aligned with each source frame index.

Examples

a = source("angle-a.mkv")
b = source("angle-b.mkv")
output = std.interleave_frames([a, b])
a = source("a.mkv")
b = source("b.mkv")
c = source("c.mkv")
output = std.interleave_frames([a, b, c])

Input

Two or more fixed-format planar Gray, YUV, or planar RGB clips with matching resolution, frame count, and frame rate, passed in array form.

Output

A clip with the same format and resolution, interleaved frames, frame count multiplied by input count, and frame rate multiplied by input count. Per-frame metadata comes from the selected source frame.

join

join(
  input: [Clip, Clip, ...]
)

Concatenates frames from two or more clips in input order.

Use array input form: std.join([a, b]). Every input clip must use fixed format, fixed resolution, finite frame count, and constant frame rate. All inputs must match on format, resolution, and frame rate. Input frame counts may differ.

The output frame count is the sum of all input frame counts. The output frame rate matches the inputs.

Examples

a = source("part-1.mkv")
b = source("part-2.mkv")
output = std.join([a, b])
intro = source("intro.mkv")
main = source("main.mkv")
credits = source("credits.mkv")
output = std.join([intro, main, credits])

Input

Two or more fixed-format planar Gray, YUV, or planar RGB clips with matching resolution, finite frame counts, and matching constant frame rate, passed in array form.

Output

A clip with the same format, resolution, and frame rate, containing each input clip’s frames in order. The output frame count is the sum of the input frame counts, and per-frame metadata comes from the selected source frame.

replace_frames

replace_frames(
  input: Clip,
  #{
    first: Int,
    last: Int,
    replacement: Int,
  }
)

Replaces an inclusive range of output frames with one source frame from the same input clip. Frames outside the range keep their original source-frame mapping.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count. first and last are zero-indexed output-frame positions. replacement is a zero-indexed source frame from the input clip. Each index must be non-negative and less than the input frame count. first must not be greater than last.

Examples

# Replace frames 100 through 120 with frame 99.
output = source("input.mkv").std.replace_frames(#{ first: 100, last: 120, replacement: 99 })
# Hold frame 12 for a single output frame at index 42.
output = source("input.mkv").std.replace_frames(#{ first: 42, last: 42, replacement: 12 })

Input

One fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count.

Output

A clip with the same format, resolution, frame count, frame rate, and per-frame metadata behavior, with frames in the selected range sourced from replacement.

Options

  • first (Int) – First zero-indexed output frame to replace.
  • last (Int) – Last zero-indexed output frame to replace, inclusive.
  • replacement (Int) – Zero-indexed input frame used for every frame from first through last.

reverse

reverse(input: Clip)

Reverses an input clip’s frame order. The last source frame becomes output frame 0, and the first source frame becomes the last output frame.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count. reverse does not change frame contents, format, resolution, frame count, frame rate, or per-frame metadata; metadata comes from the selected source frame.

Examples

output = source("input.mkv").std.reverse()
output = std.reverse(source("input.mkv"))

Input

One fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count.

Output

A clip with the same format, resolution, frame count, and frame rate, containing all input frames in reverse order.

deflate

deflate(
  input: Clip,
  #{
    planes?: Array<Int>,
    threshold?: Int | Float,
  }
)

Replaces each processed pixel with the average of the eight pixels in its 3x3 neighborhood when that average is less than the center pixel.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip. Edge pixels use mirrored neighbor coordinates so the filter can evaluate a full eight-sample neighborhood at the image borders. Integer formats round the eight-neighbor average to the nearest integer before comparing it against the center pixel. Float formats use the exact sum / 8.0 average.

If threshold is omitted or set to (), the output sample changes all the way to the computed neighborhood average. If threshold is set, the absolute difference between the input sample and output sample is capped to that value. Integer thresholds must be non-negative and stay within the format’s code range. Float thresholds must be finite and within 0.0..=1.0.

If planes is omitted, every plane is processed. If planes is provided, only the listed zero-based plane indexes are processed; all other planes are preserved without modification. An empty array processes no planes.

Examples

output = source("input.mkv").std.deflate()
output = source("input.mkv").std.deflate(#{ threshold: 4, planes: [0] })

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, resolution, timing, and metadata, with selected planes reduced toward their mirrored-edge neighborhood averages.

Options

  • planes (Array<Int>, default: all planes) – Zero-based plane indexes to process. Unlisted planes are preserved without modification. An empty array processes no planes.
  • threshold (Int | Float, default: none) – Maximum allowed absolute change from the input sample to the output sample. Use Int for integer formats and Float in 0.0..=1.0 for float formats.

expr

expr(
  input: Clip | [Clip, Clip, ...],
  #{
    expr: String | Array<String>,
  }
)

Evaluates reverse-polish math expressions for every pixel. Use it for custom per-pixel arithmetic, comparisons, masks, and simple clip blending when no purpose-built filter matches the operation you need.

The filter accepts 1 to 26 fixed-format planar Gray, YUV, or planar RGB input clips. The output always uses the first input clip’s format, resolution, timing, and metadata. expr does not convert the output format. Use convert_format or convert_bit_depth before or after expr when you need a different output format.

Input sample values are not normalized. Integer clips use their stored code values such as 0..255 for 8-bit and 0..1023 for 10-bit. Float clips use the stored f32 values. Mixed input formats are allowed when plane geometry and timing match, so expressions must account for each input clip’s sample range.

If expr contains fewer expressions than the output has planes, the last expression is reused for the remaining planes. If it contains more expressions than the output has planes, planning fails. An empty expression copies that plane from the first input clip.

Examples

output = source("input.mkv").std.expr(#{ expr: "x 2 *" })
a = source("a.mkv")
b = source("b.mkv")
output = std.expr([a, b], #{ expr: ["x y + 2 /", "", ""] })
output = source("input.mkv").std.expr(#{
  expr: ["x 128 > 255 0 ?", "", ""],
})

Input

One to 26 fixed-format planar Gray, YUV, or planar RGB clips with matching plane geometry and timing.

Output

A clip with the same format, resolution, timing, and metadata as the first input clip, with selected planes generated by expression evaluation.

Options

  • expr (String | Array<String>) – Reverse-polish expression or per-plane expressions. Empty per-plane expressions copy from the first input clip. Arrays may not be longer than the output plane count.

Variables

  • x, y, z – Inputs 0, 1, and 2.
  • a through w – Inputs 3 through 25.

Operators

  • Unary: exp, log, sqrt, sin, cos, abs, not, dup, dupN.
  • Binary: +, -, *, /, max, min, pow, >, <, =, >=, <=, and, or, xor, swap, swapN.
  • Ternary: ?, using condition true_value false_value ? stack order.

Logical operators treat values greater than 0 as true and return 1.0 for true or 0.0 for false. Integer outputs are rounded and clamped to the output format’s valid code range. Float outputs are stored without clamping. If an expression produces a non-finite result, rendering fails.

flip_horizontal

flip_horizontal(input: Clip)

Mirrors a clip left-to-right without changing its format, resolution, timing, or metadata.

The input must be one fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip. For subsampled YUV formats, each plane is flipped using that plane’s own dimensions, so chroma planes remain aligned with the luma plane.

Examples

output = source("input.mkv").std.flip_horizontal()
output = std.flip_horizontal(source("input.mkv"))

Input

One fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, resolution, timing, and metadata, mirrored left-to-right.

flip_vertical

flip_vertical(input: Clip)

Mirrors a clip top-to-bottom without changing its format, resolution, timing, or metadata.

The input must be one fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip. For subsampled YUV formats, each plane is flipped using that plane’s own dimensions, so chroma planes remain aligned with the luma plane.

Examples

output = source("input.mkv").std.flip_vertical()
output = std.flip_vertical(source("input.mkv"))

Input

One fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, resolution, timing, and metadata, mirrored top-to-bottom.

inflate

inflate(
  input: Clip,
  #{
    planes?: Array<Int>,
    threshold?: Int | Float,
  }
)

Replaces each processed pixel with the average of the eight pixels in its 3x3 neighborhood when that average is greater than the center pixel.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip. Edge pixels use mirrored neighbor coordinates so the filter can evaluate a full eight-sample neighborhood at the image borders. Integer formats round the eight-neighbor average to the nearest integer before comparing it against the center pixel. Float formats use the exact sum / 8.0 average.

If threshold is omitted or set to (), the output sample changes all the way to the computed neighborhood average. If threshold is set, the absolute difference between the input sample and output sample is capped to that value. Integer thresholds must be non-negative and stay within the format’s code range. Float thresholds must be finite and within 0.0..=1.0.

If planes is omitted, every plane is processed. If planes is provided, only the listed zero-based plane indexes are processed; all other planes are preserved without modification. An empty array processes no planes.

Examples

output = source("input.mkv").std.inflate()
output = source("input.mkv").std.inflate(#{ threshold: 0.05, planes: [0] })

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, resolution, timing, and metadata, with selected planes increased toward their mirrored-edge neighborhood averages.

Options

  • planes (Array<Int>, default: all planes) – Zero-based plane indexes to process. Unlisted planes are preserved without modification. An empty array processes no planes.
  • threshold (Int | Float, default: none) – Maximum allowed absolute change from the input sample to the output sample. Use Int for integer formats and Float in 0.0..=1.0 for float formats.

levels

levels(
  input: Clip,
  #{
    min_in: Int | Float,
    max_in: Int | Float,
    min_out: Int | Float,
    max_out: Int | Float,
    planes?: Array<Int>,
  }
)

Linearly remaps selected pixel values from the input range [min_in, max_in] to the output range [min_out, max_out].

The input must be one fixed-format planar Gray or YUV clip. Planar RGB clips are not supported. Integer formats use Int values in the clip format’s valid sample range. Float formats use finite Float values in 0.0..=1.0. max_in must be greater than min_in, and max_out must be greater than min_out.

Samples below min_in map to min_out, and samples above max_in map to max_out. Integer outputs round half up. For float clips, non-finite input samples are mapped to min_out so output samples remain in the requested range.

If planes is omitted or set to none(), every plane is processed. If planes is provided, only the listed zero-based plane indexes are processed; all other planes are preserved without modification. An empty array processes no planes.

Examples

output = source("input.mkv").std.levels(#{
  min_in: 16,
  max_in: 235,
  min_out: 0,
  max_out: 255,
})
output = source("input.mkv").std.levels(#{
  min_in: 16,
  max_in: 235,
  min_out: 0,
  max_out: 255,
  planes: [0],
})
output = source("input.mkv")
  .std.convert_format(#{ format: "grayf32" })
  .std.levels(#{ min_in: 0.1, max_in: 0.9, min_out: 0.0, max_out: 1.0 })

Input

One fixed-format planar Gray or YUV clip.

Output

A clip with the same format, resolution, timing, and metadata, with selected planes remapped to the requested output range.

Options

  • min_in (Int | Float) – Inclusive lower input bound. Use Int for integer sample formats and Float in 0.0..=1.0 for float sample formats.
  • max_in (Int | Float) – Inclusive upper input bound. Must be greater than min_in.
  • min_out (Int | Float) – Inclusive lower output bound. Use Int for integer sample formats and Float in 0.0..=1.0 for float sample formats.
  • max_out (Int | Float) – Inclusive upper output bound. Must be greater than min_out.
  • planes (Array<Int>, default: all planes) – Zero-based plane indexes to process. Unlisted planes are preserved without modification. none() uses all planes, and an empty array processes no planes.

limit

limit(
  input: Clip,
  #{
    min: Int | Float,
    max: Int | Float,
    planes?: Array<Int>,
  }
)

Clamps pixel values to the inclusive range from min to max.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip. min and max must match the clip’s sample type: integer formats use Int, and float formats use Float values in 0.0..=1.0. min may not be greater than max.

For float clips, non-finite input samples are coerced to min so the output still remains inside the requested inclusive range.

If planes is omitted, every plane is processed. If planes is provided, only the listed zero-based plane indexes are processed; all other planes are preserved without modification.

Examples

output = source("input.mkv").std.limit(#{ min: 16, max: 235 })
output = source("input.mkv").std.limit(#{ min: 16, max: 235, planes: [0] })
output = source("input.mkv")
  .std.convert_format(#{ format: "grayf32" })
  .std.limit(#{ min: 0.0, max: 1.0 })

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, resolution, timing, and metadata, with selected planes clamped to the requested range.

Options

  • min (Int | Float) – Inclusive lower clamp bound. Use Int for integer sample formats and Float in 0.0..=1.0 for float sample formats.
  • max (Int | Float) – Inclusive upper clamp bound. Use Int for integer sample formats and Float in 0.0..=1.0 for float sample formats.
  • planes (Array<Int>, default: all planes) – Zero-based plane indexes to process. Unlisted planes are preserved without modification.

masked_merge

masked_merge(
  input: [Clip, Clip, Clip],
  #{
    planes?: Array<Int>,
  }
)

Merges two clips using per-pixel weights from a third mask clip. A mask value of 0 keeps the first clip sample, and a full-scale mask value keeps the second clip sample.

Use array input form: std.masked_merge([first, second, mask], #{ ... }). The first and second clips must have the same fixed planar Gray, YUV, or planar RGB format, fixed resolution, frame count, and frame rate. The output keeps the first clip’s format, resolution, timing, and metadata.

The mask clip must have the same fixed resolution, frame count, and frame rate as the first clip. Its format must either match the input clips or be the matching Gray format with the same bit depth. Integer mask clips use full-range code values regardless of color range metadata. Float mask clips use 0.0 to 1.0 weights and are not clamped during rendering.

If the mask is Gray, its single plane is reused for every processed output plane. For subsampled chroma planes, the corresponding full-resolution Gray mask samples are averaged before merging.

If planes is omitted, every plane is processed. If planes is provided, only the listed zero-based plane indexes are processed; all other planes are copied from the first clip.

Examples

first = source("clean.mkv")
second = source("effect.mkv")
mask = source("mask.mkv").std.convert_format(#{ format: "gray8" })
output = std.masked_merge([first, second, mask])
first = source("base.mkv")
second = source("overlay.mkv")
mask = source("luma-mask.mkv").std.convert_format(#{ format: "gray8" })
output = std.masked_merge([first, second, mask], #{ planes: [0] })

Input

Three fixed-format planar clips: first input, second input, and mask.

Output

A clip with the same format, resolution, timing, and metadata as the first input, with selected planes merged using mask weights.

Options

  • planes (Array<Int>, default: all planes) – Zero-based plane indexes to process. Unlisted planes are copied from the first clip.

median

median(
  input: Clip,
  #{
    planes?: Array<Int>,
  }
)

Replaces each processed pixel with the median of the nine samples in its 3x3 neighborhood.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip. Edge pixels use mirrored neighbor coordinates so the filter always sorts a full nine-sample neighborhood at image borders.

If planes is omitted, every plane is processed. If planes is provided, only the listed zero-based plane indexes are processed; all other planes are preserved without modification. An empty array processes no planes.

Examples

output = source("input.mkv").std.median()
output = source("input.mkv").std.median(#{ planes: [0] })

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, resolution, timing, and metadata, with selected planes replaced by mirrored-edge 3x3 median samples.

Options

  • planes (Array<Int>, default: all planes) – Zero-based plane indexes to process. Unlisted planes are preserved without modification. An empty array processes no planes.

merge_planes

merge_planes(
  input: [Clip, Clip, ...],
  #{
    format: String,
  }
)

Combines Gray plane clips into one planar Gray, YUV, or planar RGB clip.

Use array input form: std.merge_planes([a, b, c], #{ ... }). Every input clip must be Gray, use fixed format and fixed resolution, and match the frame count and frame rate of plane 0. The number of inputs and each input plane’s dimensions must match the requested target format.

Examples

y = source("input.mkv").std.split_plane(#{ plane: 0 })
u = source("input.mkv").std.split_plane(#{ plane: 1 })
v = source("input.mkv").std.split_plane(#{ plane: 2 })
output = std.merge_planes([y, u, v], #{ format: "yuv420p8" })

Input

One Gray clip per output plane, passed in array form.

Output

A planar Gray, YUV, or planar RGB clip assembled from the input planes, using plane 0 for timing and metadata.

Options

resize

resize(
  input: Clip,
  #{
    width: Int,
    height: Int,
    kernel?: String,
    b?: Float,
    c?: Float,
    taps?: Int,
    linear?: Bool,
    sigmoid?: Bool,
  }
)

Rescales a clip to a new width and height with a selectable scalar kernel.

The input must be one fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip. width and height must be positive. b and c are only valid with kernel: "bicubic", and taps is only valid with kernel: "lanczos". YUV resize uses core:chroma_siting metadata when present and defaults to center when that metadata is missing or None; unsupported chroma-siting values fail render.

Set linear: true to resize light-bearing planes in linear light. Linear-light resize requires supported core:transfer and core:range metadata. Chroma difference planes in YUV clips remain in normal code-value space because resize preserves the input format.

Set sigmoid: true when upscaling to apply a fixed sigmoid curve before resampling and invert it after resampling. sigmoid implies linear; for pure downscales it is a silent no-op.

Examples

output = source("input.mkv").std.resize(#{ width: 1280, height: 720, kernel: "lanczos" })
output = source("input.mkv").std.resize(#{
    width: 1920,
    height: 1080,
    kernel: "bicubic",
    b: 0.33333334,
    c: 0.33333334,
})
output = source("input.mkv").std.resize(#{
    width: 3840,
    height: 2160,
    kernel: "lanczos",
    sigmoid: true,
})

Input

One fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, timing, and metadata, resized to the requested dimensions.

Options

  • width (Int) – Set the output width.
  • height (Int) – Set the output height.
  • kernel (String, default: "bicubic") – Select the resize kernel; supported values are "nearest", "bilinear", "bicubic", "lanczos", "spline16", and "spline36".
  • b (Float, default: 0.33333334) – Set the bicubic blur parameter when kernel is "bicubic".
  • c (Float, default: 0.33333334) – Set the bicubic ringing parameter when kernel is "bicubic".
  • taps (Int, default: 3) – Set the Lanczos lobe count when kernel is "lanczos".
  • linear (Bool, default: false) – Resize light-bearing planes in linear light using core:transfer metadata.
  • sigmoid (Bool, default: false) – Use sigmoidized linear-light upscaling to reduce ringing artifacts; implies linear and is a no-op for pure downscales.

rotate

rotate(
  input: Clip,
  #{
    degrees: Int,
  }
)

Rotates a clip clockwise by 0, 90, 180, or 270 degrees without changing its format, timing, or metadata. Rotations of 90 or 270 swap the output width and height. A rotation of 0 degrees is allowed as a no-op for scripts that pass through user-selected rotation values.

The input must be one fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip. Quarter-turn rotations are only supported for formats whose chroma sampling remains valid after swapping axes, such as yuv420p* and yuv444p*. For asymmetric chroma formats such as yuv422p*, resample explicitly before rotating.

Examples

output = source("input.mkv").std.rotate(#{ degrees: 90 })
rotation = 0
output = source("input.mkv").std.rotate(#{ degrees: rotation })

Input

One fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip.

Output

A clip with the same format, timing, and metadata. The resolution is unchanged for 0 and 180 degrees, and width and height are swapped for 90 and 270 degrees.

Options

  • degrees (Int) – Clockwise rotation amount. Must be 0, 90, 180, or 270.

select

select(
  input: Clip,
  #{
    every?: Int,
    offset?: Int,
    drop_every?: Int,
    drop_offset?: Int,
    frames?: String,
  }
)

Builds a new clip by remapping output frames to selected source frames from one input clip.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count and a positive constant frame rate. Choose exactly one of every, drop_every, or frames. Frame expressions preserve the written order, accept single frames such as 5 and inclusive ranges such as 8-10, 8..10, or 8..=10, and reject duplicates and empty entries.

Examples

output = source("input.mkv").std.select(#{ every: 2 })
output = source("input.mkv").std.select(#{ frames: "0, 10, 20, 30" })

Input

One fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count.

Output

A clip with the same format and resolution, containing the selected frames in the mapped order and using the frame count and frame rate implied by the selection form.

Options

  • every (Int) – Keep every nth frame.
  • offset (Int, default: 0) – Start the every selection at this source frame offset.
  • drop_every (Int) – Drop one frame from every n input frames.
  • drop_offset (Int, default: last frame in each group) – Choose which frame in each drop_every group to drop.
  • frames (String) – Provide an explicit frame expression.

select_every

select_every(
  input: Clip,
  #{
    cycle: Int,
    offsets: Int | Array<Int>,
  }
)

Builds a new clip by keeping selected frame offsets from every fixed-size cycle of one input clip. Offsets are zero-indexed within each cycle, preserve the written order, and may repeat; repeated offsets duplicate the corresponding source frame in the output.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count and a positive constant frame rate. cycle must be greater than or equal to 2. offsets must be one integer or a non-empty integer array where every entry is greater than or equal to 0 and less than cycle. The output frame rate is scaled by offsets.len() / cycle; scalar offsets counts as one kept frame per cycle. In a partial final cycle, offsets beyond the input frame count are ignored.

Examples

# Return even numbered frames, starting with 0.
output = source("input.mkv").std.select_every(#{ cycle: 2, offsets: 0 })
# Return odd numbered frames, starting with 1.
output = source("input.mkv").std.select_every(#{ cycle: 2, offsets: 1 })
# Fixed pattern 1-in-5 decimation, removing the first frame in every cycle.
output = source("input.mkv").std.select_every(#{ cycle: 5, offsets: [1, 2, 3, 4] })
# Duplicate every fourth frame.
output = source("input.mkv").std.select_every(#{ cycle: 4, offsets: [0, 1, 2, 3, 3] })

Input

One fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count and a positive constant frame rate.

Output

A clip with the same format and resolution, containing the selected source frames in cycle/offset order and using a frame rate scaled by offsets.len() / cycle.

Options

  • cycle (Int) – Number of source frames in each selection cycle; must be at least 2.
  • offsets (Int | Array<Int>) – Zero-indexed frame offsets to keep from each cycle; duplicate entries duplicate frames.

stack_horizontal

stack_horizontal(
  input: [Clip, Clip, ...]
)

Stacks two or more clips horizontally in input order. The first clip is placed at the left, and each later clip is placed to the right of the previous input.

Use array input form: std.stack_horizontal([left, right]). Every input clip must use fixed format, fixed resolution, finite frame count, and constant frame rate. All inputs must have identical format, height, frame count, and frame rate.

For subsampled formats, each horizontal stack boundary must align to chroma plane divisors. For example, when stacking yuv420p8, every input except the last must have an even width.

Frame properties on each output frame are copied from the first input clip for the requested frame number.

Examples

left = source("left.mkv")
right = source("right.mkv")
output = std.stack_horizontal([left, right])
a = source("camera-a.mkv")
b = source("camera-b.mkv")
c = source("camera-c.mkv")
output = std.stack_horizontal([a, b, c])

Input

Two or more fixed-format planar Gray, YUV, or planar RGB clips with matching height, format, finite frame count, and matching constant frame rate, passed in array form.

Output

A clip with the same format, frame count, and frame rate. The output height matches the inputs, the output width is the sum of input widths, and per-frame metadata comes from the first input.

stack_vertical

stack_vertical(
  input: [Clip, Clip, ...]
)

Stacks two or more clips vertically in input order. The first clip is placed at the top, and each later clip is placed below the previous input.

Use array input form: std.stack_vertical([top, bottom]). Every input clip must use fixed format, fixed resolution, finite frame count, and constant frame rate. All inputs must have identical format, width, frame count, and frame rate.

For subsampled formats, each vertical stack boundary must align to chroma plane divisors. For example, when stacking yuv420p8, every input except the last must have an even height.

Frame properties on each output frame are copied from the first input clip for the requested frame number.

Examples

top = source("top.mkv")
bottom = source("bottom.mkv")
output = std.stack_vertical([top, bottom])
header = source("header.mkv")
main = source("main.mkv")
footer = source("footer.mkv")
output = std.stack_vertical([header, main, footer])

Input

Two or more fixed-format planar Gray, YUV, or planar RGB clips with matching width, format, finite frame count, and matching constant frame rate, passed in array form.

Output

A clip with the same format, frame count, and frame rate. The output width matches the inputs, the output height is the sum of input heights, and per-frame metadata comes from the first input.

Metadata property filters

Use these filters for schema-aware clip metadata workflows:

All four filters validate metadata keys and value kinds against the active MetadataSchema. Core keys are available immediately. Plugin-defined keys usually need register_prop(...) before script-time validation can succeed.

Example

register_prop("pixelflow/sample:enabled", "bool")

clip = source("input.mkv")
metadata_source = source("metadata.mkv")
copied = std.copy_props([metadata_source, clip])
flagged = copied.std.set_prop(#{ key: "pixelflow/sample:enabled", value: true })
validated = flagged.std.require_prop(#{ key: "pixelflow/sample:enabled" })
output = validated.std.clear_prop(#{ key: "pixelflow/sample:enabled" })

set_prop

set_prop(
  input: Clip,
  #{
    key: String,
    value: MetadataValue,
  }
)

Writes one metadata key on each output frame.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip. key must already exist in the active MetadataSchema, and value must match the registered metadata kind for that key.

Examples

register_prop("acme/filter:enabled", "bool")

output = source("input.mkv").std.set_prop(#{
    key: "acme/filter:enabled",
    value: true,
})

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with the same media properties, with the requested metadata key written on each frame.

Options

  • key (String) – Select the registered metadata key to write.
  • value (MetadataValue) – Provide the metadata value to store.

clear_prop

clear_prop(
  input: Clip,
  #{
    key: String,
  }
)

Clears one metadata key on each output frame.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip. key must already exist in the active MetadataSchema, because clearing still uses schema-aware metadata validation.

Examples

output = source("input.mkv").std.clear_prop(#{ key: "core:matrix" })

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with the same media properties, with the requested metadata key cleared on each frame.

Options

  • key (String) – Select the registered metadata key to clear.

copy_props

copy_props(
  input: [Clip, Clip]
)

Copies all frame metadata from one clip onto another clip.

Use array input form: std.copy_props([metadata_source, target]). The target clip must be a fixed-format planar Gray, YUV, or planar RGB clip. The metadata source and target must have the same frame count, but they may differ in format, resolution, and frame rate.

Examples

target = source("graded.mkv")
metadata_source = source("original.mkv")
output = std.copy_props([metadata_source, target])

Input

Two clips in array form: [metadata_source, target].

Output

A clip that keeps the target clip’s plane data and media properties, but replaces each frame’s metadata with metadata from the same-numbered source frame.

require_prop

require_prop(
  input: Clip,
  #{
    key: String,
    kind?: String,
    value?: MetadataValue,
  }
)

Validates that each input frame carries required metadata before passing the frame through unchanged.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip. key must already exist in the active MetadataSchema. If you set kind, it must match the registered kind for that key. If you set value, it must also match the registered kind, and value: none is rejected. Missing keys, cleared keys, type mismatches, and value mismatches fail render.

Examples

register_prop("acme/filter:enabled", "bool")

output = source("input.mkv")
    .std.require_prop(#{ key: "acme/filter:enabled", kind: "bool", value: true })

Input

One fixed-format planar Gray, YUV, or planar RGB clip.

Output

A clip with unchanged media and frame data when every frame satisfies the metadata requirement.

Options

  • key (String) – Select the registered metadata key to require.
  • kind (String) – Require a specific metadata kind; supported values are "bool", "int", "float", "string", "array", "rational", "blob", and "binary".
  • value (MetadataValue) – Require an exact metadata value.

split_plane

split_plane(
  input: Clip,
  #{
    plane: Int,
  }
)

Extracts one plane from a planar Gray, YUV, or planar RGB clip as a Gray clip.

The input must be one fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip. plane is zero-based and must be within the input plane count. Gray clips only accept plane: 0. For subsampled YUV formats, the output resolution matches the selected plane’s own dimensions.

Examples

y = source("input.mkv").std.split_plane(#{ plane: 0 })
output = y

Input

One fixed-format, fixed-resolution planar Gray, YUV, or planar RGB clip.

Output

A Gray clip with the selected plane’s data, matching the input sample type and bit depth.

Options

  • plane (Int) – Select the zero-based plane index to extract.

trim

trim(
  input: Clip,
  #{
    start?: Int,
    end?: Int,
  }
)

Keeps one contiguous frame range from an input clip.

The input must be one fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count. start and end must be non-negative, start must not exceed end, and end must not exceed the input frame count. start is inclusive and end is exclusive, so start == end is valid and produces an empty clip.

Examples

output = source("input.mkv").std.trim(#{ start: 100, end: 200 })

Input

One fixed-format planar Gray, YUV, or planar RGB clip with a finite frame count.

Output

A clip with the same format, resolution, frame rate, and per-frame metadata, containing frames from start up to but not including end.

Options

  • start (Int, default: 0) – Select the first source frame to keep.
  • end (Int, default: input frame count) – Select the exclusive end frame.

Plugins

Current model

  • built-in filters register first,
  • CLI and GUI scan configured plugin directories for compiled plugins,
  • CLI and GUI scan the same configured plugin directories for script plugins,
  • both plugin kinds are exposed through the same plugin. namespace in scripts,
  • verbose version output reports loaded compiled plugins and ABI version.

Configured plugin directories normally come from the conventional platform locations. Set PF_PLUGIN_DIRS to replace that list entirely. PF_PLUGIN_DIRS is parsed with the operating system path-list rules. In typical shell usage, separate entries with : on Unix and macOS or ; on Windows. Empty path-list entries are ignored. Setting PF_PLUGIN_DIRS to an empty value disables plugin directory scanning.

Plugin kinds

  • Compiled plugins use the Rust SDK and register filters into the host registry.
  • Script plugins are .pf files loaded from the same plugin directories.

Plugin load or registration failures are warned and skipped so CLI startup can continue. Name collisions stop script evaluation to avoid unexpected behavior overrides.

Current compiled authoring path

Current official compiled authoring path is Rust through pixelflow-plugin-sdk.

What compiled plugins can provide today

Compiled Rust plugins can provide:

  • filter descriptors through RegistrationContext::register_filter,
  • metadata keys through RegistrationContext::register_metadata,
  • planner callbacks that receive input media, filter options, and the metadata schema, and
  • frame executors that request upstream frames and return metadata-bearing output frames.

The host keeps successfully loaded plugin libraries alive for process lifetime so registered callback pointers remain valid.

Current limits

  • Direct C or C++ authoring is not supported in current Phase 1 workflow.
  • Compiled plugin failures do not stop CLI startup when host can safely skip failed plugins.

See also

Script plugins

Script plugins are .pf files stored in the same plugin directories as compiled plugins. They add functions under the plugin. namespace, so you call them the same way you call compiled plugin filters.

Namespace mapping

PixelFlow builds the namespace from the script plugin file path:

  • acme.pf exposes functions as plugin.acme.<function>.
  • acme/utils.pf exposes functions as plugin.acme.utils.<function>.

Namespace segments must be valid script identifiers.

Authoring rules

  • A script plugin file may contain one or more top-level fn declarations.
  • Function parameters may be plain identifiers or typed declarations like clip: Clip.
  • Type declarations may use unions like width: int | none; none() is accepted only when none appears in the union.
  • Functions may accept additional parameters after the input clip.
  • Parameters with defaults are optional at call sites and must be trailing.
  • Type annotations are checked when the script plugin function is called.
  • Each function must assign output exactly once.
  • The assigned output value becomes the function result.

Example with multiple functions in one file:

fn blur(clip) {
    output = clip.std.resize(#{ width: 1280, height: 720 })
}

fn sharpen(clip) {
    output = clip.std.crop(#{ left: 8, right: 8, top: 8, bottom: 8 })
}

Script plugin functions can call compiled plugin filters and other script plugin functions with the same plugin.<namespace>.<filter>(clip, ...) and clip.plugin.<namespace>.<filter>(...) forms that regular scripts use. Calls may target functions in the same script plugin namespace or another loaded script plugin namespace.

fn blur(clip) {
    output = clip.std.resize(#{ width: 1280, height: 720 })
}

fn preview(clip) {
    output = clip.plugin.acme.blur()
}

Example with additional options:

fn resize_to(clip, width, height) {
    output = clip.std.resize(#{ width: width, height: height })
}

fn crop_edges(clip, options) {
    output = clip.std.crop(options)
}

Example with typed parameters, unions, and defaults:

fn resize_to(clip: Clip, width: int | none = 1280, height: int = 720) {
    result = clip

    if is_none(width) {
        result = clip
    } else {
        result = clip.std.resize(#{ width: width, height: height })
    }

    output = result
}

Supported parameter types are Clip, bool, int, float, string, array, map, rational, blob, and none. Use | for unions, for example int | none.

Option map validation

Use option-map helpers when a script plugin accepts #{ ... } options. These helpers let the plugin reject typos, apply defaults, and forward only supported options.

fn resize_to(clip: Clip, options: map = #{}) {
    context = "plugin.acme.resize_to"
    allowed = ["width", "height"]

    option_reject_unknown(options, allowed, context)

    width = option_require(options, "width", "int", context)
    height = option_get_as_or(options, "height", "int", 720, context)

    output = clip.std.resize(#{ width: width, height: height })
}

Use option_pick when the plugin forwards a validated subset without changing values:

fn crop_edges(clip: Clip, options: map = #{}) {
    context = "plugin.acme.crop_edges"
    allowed = ["left", "right", "top", "bottom"]

    option_reject_unknown(options, allowed, context)

    output = clip.std.crop(option_pick(options, allowed))
}

Available helpers:

  • option_has(options, key) returns whether key is present, even when its value is none().
  • option_get(options, key) returns the value, or none() when the key is missing.
  • option_get_or(options, key, default) returns default when the key is missing or none().
  • option_get_as(options, key, expected, context) validates an optional typed value.
  • option_get_as_or(options, key, expected, default, context) validates a typed value with a default.
  • option_require(options, key, expected, context) requires a typed value.
  • option_keys(options) returns keys in stable sorted order.
  • option_unknown_keys(options, allowed) returns unsupported keys in stable sorted order.
  • option_reject_unknown(options, allowed, context) throws on the first unsupported key.
  • option_pick(options, allowed) returns a new map containing only allowed keys.

expected accepts Clip, bool, int, float, number, string, array, map, rational, blob, none, shallow arrays such as array<int>, and unions such as int|none.

For typed helpers, an explicit none() value matches only none or a union that includes none. option_get_as_or is the defaulting exception: it returns the default when the key is missing or when the value is none().

The context string appears at the start of validation errors. For example, #{ widt: 16 } with the example above reports:

plugin.acme.resize_to option 'widt' is not supported

Conditionals and loops are supported, as long as output is assigned only once:

fn conditional_resize(clip, enabled) {
    result = clip

    if enabled {
        result = clip.std.resize(#{ width: 1280, height: 720 })
    } else {
        result = clip.std.crop(#{ left: 8, right: 8, top: 8, bottom: 8 })
    }

    output = result
}

Defaults are the preferred way to make trailing parameters omittable:

fn maybe_resize(clip: Clip, width: int = 1280) {
    output = clip.std.resize(#{ width: width, height: 720 })
}

Use none() with an explicit union when a caller needs to pass an empty value:

fn maybe_resize(clip: Clip, width: int | none) {
    result = clip

    if !is_none(width) {
        result = clip.std.resize(#{ width: width, height: 720 })
    }

    output = result
}

Call forms

When a script plugin function accepts more parameters than the input clip, pass the extra values after the clip argument. In chain-style calls, the clip is still the first function argument, so you only provide the remaining values or option maps.

When trailing defaults are declared, callers may omit those arguments:

clip = source("input.mkv")
output = plugin.acme.resize_to(clip)
output = source("input.mkv").plugin.acme.resize_to(1920)

When none is in a union, callers may pass none() explicitly:

output = source("input.mkv").plugin.acme.maybe_resize(none())

Function-style call:

clip = source("input.mkv")
output = plugin.acme.blur(clip)

Chain-style call:

output = source("input.mkv").plugin.acme.blur()

Function-style call with additional options:

clip = source("input.mkv")
output = plugin.acme.resize_to(clip, 1280, 720)

Chain-style call with an options map:

output = source("input.mkv").plugin.acme.crop_edges(#{
    left: 8,
    right: 8,
    top: 16,
    bottom: 16,
})

Nested namespaces work the same way:

output = source("input.mkv").plugin.acme.utils.blur()

Conflicts and validation errors

Script plugins share the same plugin. namespace as compiled plugins. If two script plugins define the same symbol, or a script plugin conflicts with a compiled plugin, PixelFlow reports the symbol and the source file paths involved.

Invalid or unreadable script plugins are warned and skipped so PixelFlow can continue loading the rest of the plugin directory.

Name collisions are fatal. If two plugins claim the same plugin.* symbol, PixelFlow stops execution instead of letting one plugin silently override the other.

When an error happens inside a script plugin call, PixelFlow can report a script backtrace that includes the .pf file path, the original plugin source line, and the public plugin.* symbol being executed instead of the generated internal Rhai helper name.

Rust SDK authoring

Minimal plugin type

#![allow(unused)]
fn main() {
use pixelflow_plugin_sdk::{
    ExecutorRequest, FilterCompatibility, FilterPlan, FilterRegistration, Frame, FrameExecutor,
    FrameRequest, MetadataKind, MetadataRegistration, PlanRequest, Plugin,
    RegistrationContext, Result, pixelflow_plugin,
};

fn plan_identity(request: PlanRequest<'_>) -> Result<FilterPlan> {
    let input = request.input_media()[0].clone();
    Ok(FilterPlan::new(input, FilterCompatibility::Preserve))
}

struct IdentityExecutor;

impl FrameExecutor for IdentityExecutor {
    fn prepare(&self, request: FrameRequest<'_>) -> Result<Frame> {
        request.input_frame(0, request.frame_number())
    }
}

fn create_identity_executor(
    _request: ExecutorRequest<'_>,
) -> Result<Box<dyn FrameExecutor>> {
    Ok(Box::new(IdentityExecutor))
}

#[derive(Default)]
pub struct SamplePlugin;

impl Plugin for SamplePlugin {
    fn name(&self) -> &'static str {
        "pixelflow-sample-plugin"
    }

    fn register(&self, registry: &mut RegistrationContext<'_>) -> Result<()> {
        registry.register_metadata(MetadataRegistration::new(
            "pixelflow/sample:enabled",
            MetadataKind::Bool,
        ))?;
        registry.register_filter(
            FilterRegistration::new("sample.identity", "pixelflow", "sample")
                .with_planner(plan_identity)
                .with_executor(create_identity_executor),
        )
    }
}

pixelflow_plugin!(SamplePlugin);
}

What register() does

register() declares filters and metadata keys through RegistrationContext.

In current SDK surface:

  • register_filter(FilterRegistration::new(...)) declares one filter name plus publisher and plugin namespaces.
  • PlanRequest exposes input media, including clip-level metadata constants, graph-node options, and the metadata schema.
  • Planner callbacks return FilterPlan, including output media, clip-level metadata constants, compatibility, dependency pattern, and concurrency class.
  • ExecutorRequest exposes input media, output media, graph-node options, metadata schema, and output_frame_builder() for fixed outputs.
  • FrameRequest exposes the output frame number and input_frame(input_index, frame_number) for upstream requests.
  • Frame allocation and plane access use the re-exported core frame APIs such as FrameBuilder, typed planes, RawPlane, and RawPlaneMut.
  • register_metadata(MetadataRegistration::new(...)) declares one metadata key and its MetadataKind.
  • Registration is synchronous. Host accepts or rejects each declaration during plugin load.

Private helper filters and graph expansion

Rust plugins can register private filters for helper stages that should be scheduled and cached by PixelFlow but not called directly from scripts. Mark helpers with FilterRegistration::private().

A public filter can register an expander with FilterRegistration::with_expander(expand_filter). During script graph construction, the expander receives the public call inputs and options through ExpansionRequest, adds private helper filters with ExpansionRequest::add_private_filter, and returns the output Clip. Use this for multi-stage filters whose intermediate frames should be visible to the scheduler.

Private filters are still normal graph nodes once an expander adds them. They need planners and executors just like public filters, and their media must be valid after source indexing and graph replanning.

Scheduling declarations

Filters default to same-frame, stateless scheduling. Temporal filters should return a FilterPlan with explicit scheduling metadata by calling with_schedule(dependencies, concurrency).

Use DependencyPattern::window(...), DependencyPattern::frame_map(...), or DependencyPattern::dynamic(...) to declare which input frames may be requested. Use ConcurrencyClass::frame_group_serial(group_size) for cycle-based filters where several adjacent output frames scan the same input cycle and should not run that cycle work concurrently. For example, a 5-in/4-out decimator can use frame_group_serial(4) to serialize output frames 0..3, 4..7, and so on.

By default, grouped serial scheduling also limits a node to two distinct active output groups at a time. This prevents speculative cycle leaders from overwhelming ordered single-lane sources. Use ConcurrencyClass::frame_group_serial_unbounded(group_size) only when the filter genuinely benefits from unbounded cross-group speculation and its upstream access pattern is safe for random concurrent requests.

Grouped scheduling only controls core scheduler concurrency. If a filter computes expensive cycle decisions, cache those decisions inside the executor until PixelFlow has a batch/shared-cycle executor API.

Clip-level metadata constants

Planner callbacks can publish clip invariants in their planned output media. Downstream planners read those values from PlanRequest::input_media() without rendering frame 0.

Register plugin metadata keys before writing constants, then validate writes with the request schema:

#![allow(unused)]
fn main() {
use pixelflow_plugin_sdk::{
    ErrorCategory, ErrorCode, FilterCompatibility, FilterPlan, MetadataKind,
    MetadataRegistration, MetadataValue, PixelFlowError, PlanRequest, RegistrationContext, Result,
};

const SUPER_PEL: &str = "zoomvtools/super:pel";

fn plan_super(request: PlanRequest<'_>) -> Result<FilterPlan> {
    let output = request.input_media()[0]
        .clone()
        .with_metadata(request.metadata_schema(), SUPER_PEL, MetadataValue::Int(2))?;
    Ok(FilterPlan::new(output, FilterCompatibility::Preserve))
}

fn plan_finest(request: PlanRequest<'_>) -> Result<FilterPlan> {
    let input = &request.input_media()[0];
    let Some(MetadataValue::Int(pel)) = input.metadata(SUPER_PEL) else {
        return Err(PixelFlowError::new(
            ErrorCategory::Format,
            ErrorCode::new("filter.missing_clip_metadata"),
            "finest requires upstream super metadata",
        ));
    };

    let _pel = pel;
    Ok(FilterPlan::new(input.clone(), FilterCompatibility::Preserve))
}

fn register_metadata(registry: &mut RegistrationContext<'_>) -> Result<()> {
    registry.register_metadata(MetadataRegistration::new(SUPER_PEL, MetadataKind::Int))
}
}

Use clip-level constants only for values that are invariant for the whole clip. Per-frame properties still belong on Frame metadata during execution.

Keep register() focused on capability declaration. Do not hand-write ABI tables or exported symbols.

Entry point export

pixelflow_plugin! exports pixelflow_plugin_entry_v2 and is the supported path.

Macro requires plugin type to implement:

  • Plugin
  • Default
  • 'static

SDK wraps entry-table export, planner callbacks, executor factory callbacks, and executor calls with panic guards so plugin panics do not unwind across FFI boundary.

Source of truth

Start with examples/sample-rust-plugin/src/lib.rs for smallest working plugin. Read crates/pixelflow-plugin-sdk/src/lib.rs for Plugin, pixelflow_plugin!, ABI versioning, and entry-point behavior.

Also useful while authoring:

  • crates/pixelflow-plugin-sdk/src/builders.rs for FilterRegistration and MetadataRegistration
  • crates/pixelflow-plugin-sdk/src/registration.rs for RegistrationContext
  • crates/pixelflow-plugin-sdk/tests/plugin_contracts.rs for SDK behavior and failure contracts

Practical authoring notes

  • Keep plugin name() stable. Host uses it for diagnostics.
  • Register plugin metadata keys before filters or scripts rely on them.
  • Keep planner callbacks deterministic and side-effect free. Sources are placeholder media during script evaluation and are replanned after reachable sources are indexed, so a planner may run more than once for the same graph node.
  • Use committed sample plugin as smallest working template, not handwritten FFI.
  • Return PixelFlowError for expected failures. Structured plugin errors cross the ABI boundary without unwinding.
  • If host rejects registration callback, SDK surfaces structured plugin error instead of silent success.

C ABI note

PixelFlow commits a generated ABI header at:

crates/pixelflow-plugin-sdk/include/pixelflow_plugin.h

This header exists for ABI review and compatibility checks.

Direct C or C++ plugin authoring is unsupported in current Phase 1 workflow. Use the Rust SDK instead.

The header includes planner and executor callback shapes because Rust SDK plugins use those C-callable trampolines internally. The supported authoring contract remains the safe Rust SDK, not direct construction of ABI tables from C or C++.

What header is for

Use header when reviewing ABI layout, exported symbol shape, and compatibility drift across host and plugin changes.

What header is not for

Do not treat header as supported direct authoring workflow for production PixelFlow plugins today.

Rust SDK remains only documented authoring path in this book.