Shadow / highlight recovery — before / after comparison

Showcases the shadow and highlight recovery filters with a three-panel comparison — original, shadow lifting, and combined shadow/highlight adjustment.

INPUT
INPUT — Shadow / highlight recovery — before / after comparison
OUTPUT
OUTPUT — Shadow / highlight recovery — before / after comparison
JavaScript
// Shadow / highlight recovery — before / after comparison
// demo_shadows_highlights.js
//!OUTPUT: OUTPUT
//!PARAM: SHADOWS:number=0.30,min=-1.0,max=1.0, HIGHLIGHTS:number=-0.20,min=-1.0,max=1.0

// demo_shadows_highlights.js
// Demonstrates shadowsHighlights() with a three-panel layout:
//
//   [ Original ]  |  [ Shadows only ]  |  [ Both ]
//
// Left panel    — the unmodified source image.
// Middle panel  — shadow lift only (open up dark areas).
// Right panel   — shadow lift + highlight recovery applied together.
//
// Tune SHADOWS (>0 = lift darks) and HIGHLIGHTS (<0 = pull back brights)
// via the //!PARAM directives above to find the sweet spot for your image.

const PANEL_W = 280;
const PANEL_H = 280;
const LABEL_H = 30;
const GAP     = 3;

// ── Load & normalise to panel size ────────────────────────────────────────
const src = Engine.loadImage(INPUT).resize(PANEL_W, PANEL_H);

// ── Panel builder ─────────────────────────────────────────────────────────

/**
 * Clones the source, applies optional shadows/highlights, then overlays a
 * coloured top-strip as a visual label.
 */
function makePanel(shadowsVal, highlightsVal, stripColor) {
    const tile = src.clone();
    if (shadowsVal !== 0 || highlightsVal !== 0) {
        tile.shadowsHighlights({ shadows: shadowsVal, highlights: highlightsVal });
    }

    // Label strip: crop top rows and tint
    const strip = tile.clone().crop(0, 0, PANEL_W, LABEL_H).tint(stripColor, 0.70);
    tile.blendAt(strip, px(0, 0), 1.0, Blend.Over);
    strip.free();

    return tile;
}

// ── Build panels ──────────────────────────────────────────────────────────

const panelOriginal = makePanel(
    0, 0,
    new Pixel(0.35, 0.35, 0.35, 1.0),   // grey — "Original"
);

const panelShadows = makePanel(
    SHADOWS, 0,
    new Pixel(0.18, 0.48, 0.85, 1.0),   // blue — "Shadow lift"
);

const panelBoth = makePanel(
    SHADOWS, HIGHLIGHTS,
    new Pixel(0.20, 0.72, 0.35, 1.0),   // green — "Shadows + Highlights"
);

// ── Divider ───────────────────────────────────────────────────────────────
const totalH = PANEL_H + LABEL_H;
const divider = Engine.createImage(GAP, totalH).tint(new Pixel(0.06, 0.06, 0.06, 1.0), 1.0);

// ── Montage & save ────────────────────────────────────────────────────────
panelOriginal
    .montageH(divider, panelShadows)
    .montageH(divider, panelBoth)
    .save(OUTPUT);

// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0