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.pfexposes functions asplugin.acme.<function>.acme/utils.pfexposes functions asplugin.acme.utils.<function>.
Namespace segments must be valid script identifiers.
Authoring rules
- A script plugin file may contain one or more top-level
fndeclarations. - 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 whennoneappears 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
outputexactly once. - The assigned
outputvalue 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 whetherkeyis present, even when its value isnone().option_get(options, key)returns the value, ornone()when the key is missing.option_get_or(options, key, default)returnsdefaultwhen the key is missing ornone().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.