Compile

From script to native executable — one click. The receiver installs nothing.

This is the thing that sets OpticScript apart: every script you write in the Playground is one button away from a real, self-contained .exe (or Linux / macOS binary). No Python runtime, no Node install, no pip install, no Docker image, no node_modules. One file. Ship it. Done.

$ ls -lh bin/
-rwxr-xr-x  1 mlc  staff   16M  May 17 12:33  enhance
$ ./enhance --input portrait.jpg --output portrait_warm.jpg \
            --brightness 1.15 --saturation 1.4 --vignette 0.5
✓ portrait_warm.jpg

That binary is ~16 MB, embeds the whole image engine plus your script, and runs at the same native speed as the Playground itself.

In the Playground — one click

Click the Make Exe button in the Editor toolbar. A dialog opens, asks for a tool name, picks your current platform by default, and a moment later hands you a path to the finished binary. The build takes milliseconds, not minutes — the engine ships as a pre-built stub and the compiler just appends your script as a blob to the tail.

The same workflow on the command line:

mlcos-compile-rs enhance.js
# → ./enhance       (Linux/macOS)
# → ./enhance.exe   (Windows)

Run it anywhere — the recipient does not need OpticScript, the playground, Rust, Go, Python, or any runtime. The binary is fully self-contained.

What ends up in the binary

Embedded Why it matters
The image engine (~16 MB) Native pixel ops, no external runtime.
Your script As JS source — --help extracts it back so future-you knows what the tool does.
Both built-in fonts (Inter + JetBrains Mono) Text overlays / watermarks need no font files.
Installed engine extensions Custom JS modules (e.g. the cube extension) ride along automatically.

What's not embedded (kept external on purpose):

  • AI model weights (GFPGAN, Real-ESRGAN). Tools that call those run the binary alongside the AI Tools Pack — same model files as the Playground uses.

Your script's directives become CLI flags

Every //!INPUT:, //!OUTPUT:, and //!PARAM: line at the top of the script turns into a typed command-line flag. The script:

// enhance.js
//!INPUT: INPUT
//!OUTPUT: OUTPUT
//!PARAM: BRIGHTNESS:number=1.10,min=0.5,max=2.0
//!PARAM: SATURATION:number=1.20,min=0,max=3
//!PARAM: VIGNETTE:number=0.30,min=0,max=1

Engine.loadImage(INPUT)
  .brightness(BRIGHTNESS)
  .saturation(SATURATION)
  .vignette(VIGNETTE)
  .save(OUTPUT);

Compiled and asked for help:

$ ./enhance --help
enhance
Created with MLC OpticScript v2.1.3 · https://mlcgo.eu
Copyright (c) 2026 Michael Lechner · Elastic License 2.0

USAGE:
    enhance --INPUT=<path> --OUTPUT=<path> --BRIGHTNESS=<value>
            --SATURATION=<value> --VIGNETTE=<value>

INPUTS:
    --INPUT=<path>          (load image)

OUTPUTS:
    --OUTPUT=<path>         (save target; `-` for stdout)

PARAMETERS:
    --BRIGHTNESS=<value>    (default: 1.10)
    --SATURATION=<value>    (default: 1.20)
    --VIGNETTE=<value>      (default: 0.30)

The min/max ranges declared in //!PARAM: are enforced at runtime — out-of-range values are rejected before the script even starts.

Batch a whole folder — no script change

The same binary handles batch processing. Point --INPUT at a glob and --OUTPUT at a template, and the engine stays "warm" across the loop:

./enhance --INPUT='photos/*.jpg' --OUTPUT='enhanced/{stem}.png'
batch: 17 file(s) via --INPUT
✓ OUTPUT → enhanced/IMG_0001.png
✓ OUTPUT → enhanced/IMG_0002.png
…
Input pattern Example
Glob 'photos/*.jpg', '**/*.png'
File list (one per line) @todo.txt (# comments allowed)
Comma list a.png,b.png,c.png

Output templates support {stem} / {name} / {ext} / {dir} / {idx} / {n} placeholders.

Filter mode — - is stdin / stdout

Every compiled binary doubles as a Unix-style filter:

# Read PNG from stdin, write JPEG to stdout
cat photo.png | ./enhance --INPUT=- --OUTPUT=- \
                          --stdout-format=jpg --silent > out.jpg

# Chain two filters
cat in.jpg | ./grayscale --INPUT=- --OUTPUT=- --silent \
           | ./watermark --INPUT=- --OUTPUT=- --silent \
                         --BRAND="© Acme" > final.png

Feature checklist

Things that "just work" inside every compiled binary, with no external dependencies:

  • PNG / JPEG / WebP / AVIF / TIFF read + write
  • Animated WebP + APNG + multi-page TIFF for animations
  • EXIF read on every load, write on JPEG save (meta.exif.*)
  • SVG import (resvg) and SVG export (vtracer) — full vector loop
  • 35+ pixel operations — blur, sharpen, warp, blend, color grading, edge detection, …
  • Text rendering with two embedded variable fonts (Inter, JetBrains Mono)
  • WarpGrid mesh transforms for perspective / 3D-surface projection
  • Custom convolution kernels for sharpen / emboss / motion-blur / etc.

Cross-platform builds

The Playground's "Make Exe" button defaults to your current OS. Cross-compilation (build a Windows .exe from macOS, for example) is a Pro feature — see the pricing page.

On the command line:

# Native (host platform)
mlcos-compile-rs enhance.js --overwrite

# Override the output name / path
mlcos-compile-rs enhance.js --name colour-enhancer --output ~/bin/

# Cross-target (Pro): point at a pre-downloaded stub for the target OS
mlcos-compile-rs enhance.js --stub ./stubs/mlcos-run-rs-windows-x64.exe

Three ways to distribute, in order of effort

You don't always need a full compile. The mlcos installer is small; when your audience already has it, lighter options work too:

Audience What to ship What they install
Your team / internal enhance.js + mlcos-run-rs enhance.js … OpticScript
Polished tool, internal mlcos-compile-rs --wrap=auto enhance.jsenhance.cmd / enhance.sh OpticScript
External user enhance.exe (compiled) Nothing

Compiled binaries are the answer for the third row — the one where you want zero questions from the recipient.

Troubleshooting

  • INPUT is not defined at runtime — the script uses INPUT but doesn't declare //!INPUT: INPUT (or the relevant key). Add the directive so the compiler knows what to wire up.
  • batch mode … requires a placeholder — your input matched multiple files but the output is a fixed path. Add {stem} (or another placeholder) to the output.
  • two inputs are multi-valued patterns; only one input may batch — fix one input to a single file; the other can batch.

See also