← Back to miito.dev

Digital Garden

November 9
Garden HubScriptingAutomotiveTypeScriptTestingSVGSQLSecurityReactPerformanceNode.jsModdingJavaScriptGame DevDevOpsDesignCSSCommunication
miito.dev
© Brian Dinh (miito.dev)
✕
✕
§ GARDEN · SCRIPTING · CLASS OPEN
NODE FILE · ENTRY

DisplayFusion Wallpaper Dimming Script

FILED UNDER · SCRIPTING
①Read
②Meta

A PowerShell script that makes a darker copy of a DisplayFusion wallpaper profile, so your desktop icon labels are easier to read. It can pick how dark each image should be for you.

I like bright wallpapers, but they make the white labels under my desktop icons hard to read. So I wrote a script that makes darker copies of the images in a DisplayFusion wallpaper profile, along with a new profile that uses them. The original images and profile are never touched.

Pick a profile, then either set a dim level for each image yourself or press Auto and let the script choose. You can preview the result on your actual desktop before anything is created, and the script backs up your DisplayFusion settings before it changes them.

Dimming is the easy part. Every pixel's red, green and blue values get multiplied by the same number, so a 40% dim multiplies each of them by 0.6.

Auto takes a bit more work. It's looking for the smallest dim level that still keeps white text readable. WCAG, the web's accessibility guidelines, asks for a contrast of at least 4.5:1 between text and its background, and for white text that works out to a background no brighter than about 0.18 in luminance, where black is 0 and white is 1. Luminance is brightness the way your eye sees it, so green counts for a lot more than blue.

To check an image, the script shrinks it to 512 pixels wide and measures every pixel. It doesn't need every last one to be dark enough, only the darkest 90%, so a small bright spot like the sun or a reflection on water doesn't drag the whole picture down. Then it narrows in on the answer: try a 50% dim, go darker if that's still too bright or lighter if it's already dark enough, and keep halving the gap until it lands on the smallest dim level that works.

To run it, save both files below into the same folder and double-click Dim DisplayFusion Wallpaper.cmd.

Dim DisplayFusion Wallpaper.cmd
@echo off
rem Launches the DisplayFusion wallpaper dimmer (no execution-policy change needed).
powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File "%~dp0Dim-DisplayFusionWallpaper.ps1" %*
if errorlevel 1 pause
Dim-DisplayFusionWallpaper.ps1
<#
.SYNOPSIS
    Creates a dimmed copy of a DisplayFusion Pro wallpaper profile.

.DESCRIPTION
    1. Lets you pick a DisplayFusion wallpaper profile and a dim level FOR EACH IMAGE
       (manual, or Auto), with a live preview in the window and on the desktop itself.
    2. Finds every local image (and image folder) the profile uses, and writes darkened
       COPIES into a separate folder:  Pictures\DisplayFusion Dimmed\<new profile name>__<id>\
       Your original images are only ever read, never written, moved or renamed.
    3. Backs up your DisplayFusion settings (.reg) before changing anything.
    4. Clones the profile into a NEW profile (new GUID) that points at the dimmed copies.
       The source profile is never modified.
    5. Restarts DisplayFusion and (optionally) loads the new profile.

    Run it with no parameters for the GUI, or with -DimPercent / -AutoDim for an unattended run:
        .\Dim-DisplayFusionWallpaper.ps1 -SourceProfile "Main" -DimPercent 40   # same level for all
        .\Dim-DisplayFusionWallpaper.ps1 -SourceProfile "Main" -AutoDim         # own level per image

    Auto dim level (per image): the smallest dim at which 90% of the image is dark enough for
    white text (icon labels) to keep a WCAG contrast ratio of 4.5:1 (-AutoContrast to change).

.NOTES
    Works with Windows PowerShell 5.1 (built into Windows 11). Use the included .cmd
    launcher to run it without changing your execution policy.
    Online/random image sources (Bing, Flickr, etc.) can't be dimmed - only local files.
#>
[CmdletBinding()]
param(
    [string]$SourceProfile,                 # profile name; default = current profile
    [ValidateRange(1, 95)][int]$DimPercent, # if given, runs without the GUI
    [string]$NewProfileName,                # default: "<source> (Dim N%)" / "<source> (Auto dim)"
    [string]$OutputRoot = (Join-Path ([Environment]::GetFolderPath('MyPictures')) 'DisplayFusion Dimmed'),
    [switch]$AutoDim,                       # pick a dim level for each image automatically
    [ValidateRange(1.5, 21)][double]$AutoContrast = 4.5,  # contrast white text must keep (WCAG AA = 4.5)
    [switch]$NoLoad,                        # don't switch to the new profile when done
    [switch]$Force                          # replace an existing profile with the same new name
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 2

# ---------------------------------------------------------------------------------------------
# Image helper (compiled C#; C# 5 syntax so it builds on Windows PowerShell 5.1)
# Decodes via WIC, so it reads JPG/PNG/BMP/GIF/TIFF and WebP/HEIC/AVIF when the codec is installed.
# ---------------------------------------------------------------------------------------------
Add-Type -AssemblyName PresentationCore, WindowsBase
if (-not ('DfDimmer' -as [type])) {
    Add-Type -ReferencedAssemblies PresentationCore, WindowsBase, System.Xaml, System.Windows.Forms, System.Drawing -TypeDefinition @'
using System;
using System.IO;
using System.Collections.ObjectModel;
using System.Windows.Media;
using System.Windows.Media.Imaging;

public class DfImage
{
    public BitmapSource Bitmap;
    public ReadOnlyCollection<ColorContext> ColorContexts;
}

public static class DfDimmer
{
    // Loads the first frame, applies EXIF orientation, optionally downscales to maxWidth (0 = full size).
    public static DfImage Load(string path, int maxWidth)
    {
        BitmapFrame frame;
        int orientation = 1;
        using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
        {
            BitmapDecoder dec = BitmapDecoder.Create(fs, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
            frame = dec.Frames[0];
            orientation = ReadOrientation(frame);
        }

        TransformGroup tg = new TransformGroup();
        switch (orientation)
        {
            case 2: tg.Children.Add(new ScaleTransform(-1, 1)); break;
            case 3: tg.Children.Add(new RotateTransform(180)); break;
            case 4: tg.Children.Add(new ScaleTransform(1, -1)); break;
            case 5: tg.Children.Add(new RotateTransform(90));  tg.Children.Add(new ScaleTransform(-1, 1)); break;
            case 6: tg.Children.Add(new RotateTransform(90));  break;
            case 7: tg.Children.Add(new RotateTransform(270)); tg.Children.Add(new ScaleTransform(-1, 1)); break;
            case 8: tg.Children.Add(new RotateTransform(270)); break;
        }
        bool rotated = (orientation >= 5 && orientation <= 8);
        int w = rotated ? frame.PixelHeight : frame.PixelWidth;
        if (maxWidth > 0 && w > maxWidth)
        {
            double s = (double)maxWidth / w;
            tg.Children.Add(new ScaleTransform(s, s));
        }

        BitmapSource src = frame;
        if (tg.Children.Count > 0)
        {
            TransformedBitmap tb = new TransformedBitmap(frame, tg);
            tb.Freeze();
            src = tb;
        }

        DfImage result = new DfImage();
        result.Bitmap = src;
        try { result.ColorContexts = frame.ColorContexts; } catch { result.ColorContexts = null; }
        return result;
    }

    static int ReadOrientation(BitmapFrame frame)
    {
        try
        {
            BitmapMetadata md = frame.Metadata as BitmapMetadata;
            if (md == null) return 1;
            string[] queries = { "/app1/ifd/{ushort=274}", "/ifd/{ushort=274}", "System.Photo.Orientation" };
            foreach (string q in queries)
            {
                try
                {
                    if (md.ContainsQuery(q))
                    {
                        object o = md.GetQuery(q);
                        if (o != null)
                        {
                            int v = Convert.ToInt32(o);
                            if (v >= 1 && v <= 8) return v;
                        }
                    }
                }
                catch { }
            }
        }
        catch { }
        return 1;
    }

    // Multiplies R, G and B by (1 - dimPercent/100). Alpha is untouched.
    public static BitmapSource Dim(BitmapSource src, double dimPercent)
    {
        double factor = 1.0 - (dimPercent / 100.0);
        if (factor < 0) factor = 0;
        if (factor > 1) factor = 1;

        FormatConvertedBitmap conv = new FormatConvertedBitmap(src, PixelFormats.Bgra32, null, 0);
        int w = conv.PixelWidth, h = conv.PixelHeight, stride = w * 4;
        byte[] px = new byte[stride * h];
        conv.CopyPixels(px, stride, 0);

        int f = (int)Math.Round(factor * 65536.0);
        for (int i = 0; i < px.Length; i += 4)
        {
            px[i]     = (byte)((px[i]     * f + 32768) >> 16);
            px[i + 1] = (byte)((px[i + 1] * f + 32768) >> 16);
            px[i + 2] = (byte)((px[i + 2] * f + 32768) >> 16);
        }

        double dx = src.DpiX > 1 ? src.DpiX : 96.0;
        double dy = src.DpiY > 1 ? src.DpiY : 96.0;
        BitmapSource outBmp = BitmapSource.Create(w, h, dx, dy, PixelFormats.Bgra32, null, px, stride);
        outBmp.Freeze();
        return outBmp;
    }

    // Writes to a temp file first, then moves into place. Refuses to overwrite the source file.
    public static void Save(BitmapSource bmp, ReadOnlyCollection<ColorContext> cc, string sourcePath, string outPath, bool jpeg)
    {
        if (string.Equals(Path.GetFullPath(sourcePath), Path.GetFullPath(outPath), StringComparison.OrdinalIgnoreCase))
            throw new InvalidOperationException("Refusing to overwrite the original image: " + sourcePath);

        string tmp = outPath + ".tmp";
        try
        {
            WriteFile(bmp, cc, tmp, jpeg);
        }
        catch
        {
            if (cc == null) throw;
            WriteFile(bmp, null, tmp, jpeg);   // some encoders reject colour profiles; retry without
        }
        if (File.Exists(outPath)) File.Delete(outPath);
        File.Move(tmp, outPath);
    }

    static void WriteFile(BitmapSource bmp, ReadOnlyCollection<ColorContext> cc, string path, bool jpeg)
    {
        BitmapEncoder enc;
        BitmapSource toWrite = bmp;
        if (jpeg)
        {
            JpegBitmapEncoder j = new JpegBitmapEncoder();
            j.QualityLevel = 95;
            enc = j;
            toWrite = new FormatConvertedBitmap(bmp, PixelFormats.Bgr24, null, 0);
        }
        else
        {
            enc = new PngBitmapEncoder();
        }
        enc.Frames.Add(BitmapFrame.Create(toWrite, null, null, cc));
        using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
        {
            enc.Save(fs);
        }
    }

    // ---- Auto dim level -------------------------------------------------------------------
    static double[] _lin;
    static double[] LinearLut()
    {
        if (_lin == null)
        {
            double[] a = new double[256];
            for (int i = 0; i < 256; i++)
            {
                double c = i / 255.0;
                a[i] = c <= 0.04045 ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4);   // sRGB -> linear (WCAG)
            }
            _lin = a;
        }
        return _lin;
    }

    static byte[] Pixels(BitmapSource src)
    {
        FormatConvertedBitmap conv = new FormatConvertedBitmap(src, PixelFormats.Bgra32, null, 0);
        int stride = conv.PixelWidth * 4;
        byte[] px = new byte[stride * conv.PixelHeight];
        conv.CopyPixels(px, stride, 0);
        return px;
    }

    // Relative luminance (0..1) that `percentile` of the pixels are at or below,
    // after the image has been multiplied by `factor` exactly like Dim() does.
    static double PercentileLuminance(byte[] px, double factor, double percentile)
    {
        double[] lin = LinearLut();
        int f = (int)Math.Round(factor * 65536.0);
        const int bins = 4096;
        int[] hist = new int[bins];
        int n = px.Length / 4;
        for (int i = 0; i < px.Length; i += 4)
        {
            int b = (px[i] * f + 32768) >> 16, g = (px[i + 1] * f + 32768) >> 16, r = (px[i + 2] * f + 32768) >> 16;
            double lum = 0.2126 * lin[r] + 0.7152 * lin[g] + 0.0722 * lin[b];
            hist[(int)(lum * (bins - 1) + 0.5)]++;
        }
        long need = (long)Math.Ceiling(n * percentile);
        long acc = 0;
        for (int i = 0; i < bins; i++)
        {
            acc += hist[i];
            if (acc >= need) return i / (double)(bins - 1);
        }
        return 1.0;
    }

    // Smallest dim % (0-95) that brings the image's `percentile` luminance down to `targetLuminance`.
    public static int SuggestDim(BitmapSource src, double targetLuminance, double percentile)
    {
        byte[] px = Pixels(src);
        if (PercentileLuminance(px, 1.0, percentile) <= targetLuminance) return 0;
        double lo = 0.0, hi = 1.0;              // brightness factor; lo always meets the target
        for (int i = 0; i < 24; i++)
        {
            double mid = (lo + hi) / 2.0;
            if (PercentileLuminance(px, mid, percentile) <= targetLuminance) lo = mid; else hi = mid;
        }
        int dim = (int)Math.Ceiling((1.0 - lo) * 100.0 - 1e-9);
        return Math.Max(0, Math.Min(95, dim));
    }

    public static byte[] ToPngBytes(BitmapSource bmp)
    {
        PngBitmapEncoder enc = new PngBitmapEncoder();
        enc.Frames.Add(BitmapFrame.Create(bmp));
        using (MemoryStream ms = new MemoryStream())
        {
            enc.Save(ms);
            return ms.ToArray();
        }
    }
}

// Borderless black window used for the live desktop preview.
// Click-through, never takes focus, hidden from the taskbar and Alt+Tab.
public class DfOverlay : System.Windows.Forms.Form
{
    public DfOverlay(System.Drawing.Rectangle bounds)
    {
        FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
        ShowInTaskbar = false;
        StartPosition = System.Windows.Forms.FormStartPosition.Manual;
        BackColor = System.Drawing.Color.Black;
        Bounds = bounds;
        Opacity = 0;
    }

    protected override bool ShowWithoutActivation { get { return true; } }

    protected override System.Windows.Forms.CreateParams CreateParams
    {
        get
        {
            System.Windows.Forms.CreateParams cp = base.CreateParams;
            cp.ExStyle |= 0x00000020 | 0x00080000 | 0x00000080 | 0x08000000; // TRANSPARENT | LAYERED | TOOLWINDOW | NOACTIVATE
            return cp;
        }
    }
}
'@
}

# ---------------------------------------------------------------------------------------------
# Constants / helpers
# ---------------------------------------------------------------------------------------------
$DfRegPath   = 'Software\Binary Fortress Software\DisplayFusion'
$WpRegPath   = "$DfRegPath\Wallpaper"
$ImageExts   = @('.jpg', '.jpeg', '.jpe', '.jfif', '.png', '.bmp', '.dib', '.gif', '.tif', '.tiff',
                 '.webp', '.heic', '.heif', '.avif', '.jxr', '.wdp')
$JpegExts    = @('.jpg', '.jpeg', '.jpe', '.jfif')
$script:Gui  = $false

function Write-Status([string]$msg) {
    if ($script:Gui -and $script:StatusLabel) {
        $script:StatusLabel.Text = $msg
        [System.Windows.Forms.Application]::DoEvents()
    }
    Write-Host $msg
}

function Show-Error([string]$msg) {
    if ($script:Gui) { [void][System.Windows.Forms.MessageBox]::Show($msg, 'Dim DisplayFusion Wallpaper', 'OK', 'Error') }
    else { Write-Host $msg -ForegroundColor Red }
}

function Get-ShortHash([string]$text) {
    $md5 = [System.Security.Cryptography.MD5]::Create()
    try {
        $bytes = $md5.ComputeHash([Text.Encoding]::UTF8.GetBytes($text.ToLowerInvariant()))
        return (($bytes[0..3] | ForEach-Object { $_.ToString('x2') }) -join '')
    } finally { $md5.Dispose() }
}

function Get-SafeName([string]$name) {
    $bad = [IO.Path]::GetInvalidFileNameChars()
    $sb = New-Object Text.StringBuilder
    foreach ($ch in $name.ToCharArray()) { if ($bad -contains $ch) { [void]$sb.Append('_') } else { [void]$sb.Append($ch) } }
    return $sb.ToString()
}

function Get-ErrMsg($err) {
    $e = $err.Exception
    while ($e.InnerException) { $e = $e.InnerException }
    return $e.Message
}

function Test-IsImagePath([string]$path) {
    return $ImageExts -contains ([IO.Path]::GetExtension($path).ToLowerInvariant())
}

function Get-DisplayFusionPaths {
    $dirs = New-Object System.Collections.Generic.List[string]
    try {
        $p = Get-Process -Name DisplayFusion -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($p -and $p.Path) { $dirs.Add((Split-Path $p.Path)) }
    } catch { }
    foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)}, $env:LOCALAPPDATA)) {
        if ($base) { $dirs.Add((Join-Path $base 'DisplayFusion')) }
    }
    foreach ($d in $dirs) {
        $cmd = Join-Path $d 'DisplayFusionCommand.exe'
        if (Test-Path -LiteralPath $cmd) {
            return [pscustomobject]@{ Command = $cmd; App = (Join-Path $d 'DisplayFusion.exe') }
        }
    }
    return $null
}

# ---------------------------------------------------------------------------------------------
# Registry: read / copy wallpaper profiles
# ---------------------------------------------------------------------------------------------
function Get-WallpaperProfiles {
    $root = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($WpRegPath)
    if (-not $root) { return @() }
    try {
        $list = @(foreach ($sub in $root.GetSubKeyNames()) {
            $k = $root.OpenSubKey($sub)
            try {
                $name = $k.GetValue('Name')
                if ($name) { [pscustomobject]@{ KeyName = $sub; Name = [string]$name } }
            } finally { $k.Close() }
        })
        return @($list | Sort-Object Name)
    } finally { $root.Close() }
}

function Get-CurrentProfileId {
    $k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($DfRegPath)
    if (-not $k) { return $null }
    try { return [string]$k.GetValue('WallpaperCurrentProfile') } finally { $k.Close() }
}

# Reads a key (values + subkeys, recursively) into a plain object tree.
function Read-KeyTree([Microsoft.Win32.RegistryKey]$key) {
    $values = @(foreach ($n in $key.GetValueNames()) {
        [pscustomobject]@{
            Name = $n
            Kind = $key.GetValueKind($n)
            Data = $key.GetValue($n, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
        }
    })
    $subs = @{}
    foreach ($s in $key.GetSubKeyNames()) {
        $sk = $key.OpenSubKey($s)
        try { $subs[$s] = Read-KeyTree $sk } finally { $sk.Close() }
    }
    return [pscustomobject]@{ Values = @($values); SubKeys = $subs }
}

function Write-KeyTree([Microsoft.Win32.RegistryKey]$key, $tree) {
    foreach ($v in $tree.Values) {
        $kind = $v.Kind
        if ($kind -eq [Microsoft.Win32.RegistryValueKind]::Unknown) { $kind = [Microsoft.Win32.RegistryValueKind]::None }
        $key.SetValue($v.Name, $v.Data, $kind)
    }
    foreach ($s in $tree.SubKeys.Keys) {
        $sk = $key.CreateSubKey($s)
        try { Write-KeyTree $sk $tree.SubKeys[$s] } finally { $sk.Close() }
    }
}

# Every string stored anywhere in the profile (REG_SZ, REG_EXPAND_SZ, REG_MULTI_SZ).
function Get-TreeStrings($tree) {
    foreach ($v in $tree.Values) {
        if ($v.Data -is [string]) { $v.Data }
        elseif ($v.Data -is [string[]]) { $v.Data }
    }
    foreach ($s in $tree.SubKeys.Values) { Get-TreeStrings $s }
}

# Returns a new tree with every string run through $mapper (scriptblock).
function Convert-Tree($tree, [scriptblock]$mapper) {
    $values = @(foreach ($v in $tree.Values) {
        $d = $v.Data
        if ($d -is [string]) { $d = & $mapper $d }
        elseif ($d -is [string[]]) { $d = [string[]]@($d | ForEach-Object { & $mapper $_ }) }
        [pscustomobject]@{ Name = $v.Name; Kind = $v.Kind; Data = $d }
    })
    $subs = @{}
    foreach ($s in $tree.SubKeys.Keys) { $subs[$s] = Convert-Tree $tree.SubKeys[$s] $mapper }
    return [pscustomobject]@{ Values = @($values); SubKeys = $subs }
}

function Read-ProfileTree([string]$keyName) {
    $k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey("$WpRegPath\$keyName")
    if (-not $k) { throw "Wallpaper profile key not found: $keyName" }
    try { return Read-KeyTree $k } finally { $k.Close() }
}

# ---------------------------------------------------------------------------------------------
# Find the local images a profile uses
# ---------------------------------------------------------------------------------------------
function Get-ProfileSources($tree) {
    $files   = New-Object 'System.Collections.Generic.Dictionary[string,string]' ([StringComparer]::OrdinalIgnoreCase)  # raw -> expanded
    $folders = New-Object 'System.Collections.Generic.Dictionary[string,string]' ([StringComparer]::OrdinalIgnoreCase)
    foreach ($raw in (Get-TreeStrings $tree)) {
        if ([string]::IsNullOrWhiteSpace($raw)) { continue }
        $s = $raw.Trim().Trim('"')
        if ($s -notmatch '^(?:[A-Za-z]:\\|\\\\|%)') { continue }       # only things that look like paths
        $exp = [Environment]::ExpandEnvironmentVariables($s)
        try {
            if ((Test-IsImagePath $exp) -and (Test-Path -LiteralPath $exp -PathType Leaf)) {
                if (-not $files.ContainsKey($raw)) { $files[$raw] = $exp }
            }
            elseif (Test-Path -LiteralPath $exp -PathType Container) {
                if (-not $folders.ContainsKey($raw)) { $folders[$raw] = $exp.TrimEnd('\') }
            }
        } catch { }
    }
    $folderImages = @{}
    foreach ($kv in $folders.GetEnumerator()) {
        $folderImages[$kv.Key] = @(Get-ChildItem -LiteralPath $kv.Value -Recurse -File -ErrorAction SilentlyContinue |
            Where-Object { Test-IsImagePath $_.Name } | Select-Object -ExpandProperty FullName)
    }
    $count = $files.Count
    foreach ($v in $folderImages.Values) { $count += $v.Count }
    return [pscustomobject]@{ Files = $files; Folders = $folders; FolderImages = $folderImages; ImageCount = $count }
}

# Which image each monitor shows right now (Wallpaper_<n>_FileName), with the monitor's
# position if the profile stores it (Screen_<n>_X / _Y). Ordered left-to-right when possible.
function Get-MonitorEntries($tree, $sources) {
    $vals = @{}
    foreach ($v in $tree.Values) { $vals[$v.Name] = $v.Data }
    $list = @(foreach ($v in $tree.Values) {
        if ($v.Name -match '^Wallpaper_(\d+)_FileName$' -and $v.Data -is [string] -and $sources.Files.ContainsKey($v.Data)) {
            $n = [int]$Matches[1]
            $x = $null; $y = $null
            if ($vals.ContainsKey("Screen_${n}_X")) { $x = [int]$vals["Screen_${n}_X"]; $y = [int]$vals["Screen_${n}_Y"] }
            [pscustomobject]@{ Index = $n; Path = $sources.Files[$v.Data]; X = $x; Y = $y; Item = $null }
        }
    })
    if ($list.Count -eq 0) { return @() }
    if (@($list | Where-Object { $null -eq $_.X }).Count -eq 0) { return @($list | Sort-Object X, Y) }
    return @($list | Sort-Object Index)
}

# One entry per distinct image in the profile, each with its own dim level.
function Get-ImageItems($tree, $sources, [int]$defaultDim) {
    $items = New-Object System.Collections.ArrayList
    $byPath = @{}
    $mons = @(Get-MonitorEntries $tree $sources)

    function New-Item2([string]$path, [string]$where) {
        $it = [pscustomobject]@{ Path = $path; Name = [IO.Path]::GetFileName($path); Role = $where; Dim = $defaultDim; Thumb = $null }
        [void]$items.Add($it); $byPath[$path] = $it
        return $it
    }

    for ($k = 0; $k -lt $mons.Count; $k++) {
        $label = if ($mons.Count -eq 1) { 'Desktop' } else { "Monitor $($k + 1)" }
        if ($byPath.ContainsKey($mons[$k].Path)) {
            $it = $byPath[$mons[$k].Path]
            $it.Role = $it.Role + ", $($k + 1)"
        } else { $it = New-Item2 $mons[$k].Path $label }
        $mons[$k].Item = $it
    }
    foreach ($p in $sources.Files.Values) {
        if (-not $byPath.ContainsKey($p)) { [void](New-Item2 $p 'Slideshow') }
    }
    foreach ($kv in $sources.Folders.GetEnumerator()) {
        $leaf = Split-Path $kv.Value -Leaf
        foreach ($p in $sources.FolderImages[$kv.Key]) {
            if (-not $byPath.ContainsKey($p)) { [void](New-Item2 $p "Folder: $leaf") }
        }
    }
    return [pscustomobject]@{ Items = $items; Monitors = $mons }
}

# Contrast ratio C against white text  ->  max background luminance:  (1 + 0.05) / (L + 0.05) = C
function Get-AutoTargetLuminance([double]$contrast) { return (1.05 / $contrast) - 0.05 }

# Smallest dim that keeps white text at $contrast over 90% of this image.
function Get-AutoDimForFile([string]$path, [double]$contrast) {
    $bmp = ([DfDimmer]::Load($path, 512)).Bitmap
    return [DfDimmer]::SuggestDim($bmp, (Get-AutoTargetLuminance $contrast), 0.90)
}

function Get-DimmedFileName([string]$sourcePath, [int]$dim) {
    $ext  = [IO.Path]::GetExtension($sourcePath).ToLowerInvariant()
    $base = [IO.Path]::GetFileNameWithoutExtension($sourcePath)
    $outExt = if ($dim -le 0) { $ext } elseif ($JpegExts -contains $ext) { '.jpg' } else { '.png' }
    return "{0}__{1}__d{2}{3}" -f $base, (Get-ShortHash $sourcePath), $dim, $outExt
}

# Writes one dimmed copy (or a plain copy at 0%) unless an up-to-date one already exists.
function Invoke-DimImage([string]$src, [string]$outPath, [int]$dim) {
    if ([IO.Path]::GetFullPath($src) -ieq [IO.Path]::GetFullPath($outPath)) { throw "Refusing to overwrite the original image: $src" }
    $outDir = Split-Path $outPath
    if (-not (Test-Path -LiteralPath $outDir)) { [void](New-Item -ItemType Directory -Path $outDir -Force) }
    if (Test-Path -LiteralPath $outPath) {
        if ((Get-Item -LiteralPath $outPath).LastWriteTimeUtc -ge (Get-Item -LiteralPath $src).LastWriteTimeUtc) { return }
    }
    if ($dim -le 0) { Copy-Item -LiteralPath $src -Destination $outPath -Force; return }
    $img = [DfDimmer]::Load($src, 0)
    $dimmed = [DfDimmer]::Dim($img.Bitmap, $dim)
    $isJpeg = ([IO.Path]::GetExtension($outPath).ToLowerInvariant() -eq '.jpg')
    [DfDimmer]::Save($dimmed, $img.ColorContexts, $src, $outPath, $isJpeg)
}

# Builds the dimmed copies for one new profile. $dimFor maps image path -> dim %.
# Returns a raw-registry-string -> new-path map, plus the list of files that belong to this profile.
function New-DimmedCopies($sources, $dimFor, [string]$outDir, [scriptblock]$progress) {
    $map      = New-Object 'System.Collections.Generic.Dictionary[string,string]' ([StringComparer]::OrdinalIgnoreCase)
    $expected = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
    $done     = New-Object 'System.Collections.Generic.Dictionary[string,string]' ([StringComparer]::OrdinalIgnoreCase)
    $failed   = New-Object System.Collections.Generic.List[string]
    $total = [Math]::Max(1, $sources.ImageCount); $n = 0

    # Single files (per-monitor images, slideshow lists). 0% = keep pointing at the original.
    foreach ($kv in $sources.Files.GetEnumerator()) {
        $path = $kv.Value
        $d = [int]$dimFor[$path]
        if ($d -gt 0) {
            if (-not $done.ContainsKey($path)) {
                $out = Join-Path $outDir (Get-DimmedFileName $path $d)
                try { Invoke-DimImage $path $out $d; $done[$path] = $out; [void]$expected.Add($out) }
                catch { $failed.Add("$path  ->  $(Get-ErrMsg $_)"); $done[$path] = $null }
            }
            if ($done[$path]) { $map[$kv.Key] = $done[$path] }
        }
        $n++; & $progress $n $total $path
    }

    # Folders: mirrored with every image at its own level. All 0% = keep the original folder.
    foreach ($kv in $sources.Folders.GetEnumerator()) {
        $srcDir = $kv.Value
        $imgs = $sources.FolderImages[$kv.Key]
        if (@($imgs | Where-Object { [int]$dimFor[$_] -gt 0 }).Count -eq 0) { $n += $imgs.Count; continue }
        $mirror = Join-Path $outDir ("Folders\{0}__{1}" -f (Get-SafeName (Split-Path $srcDir -Leaf)), (Get-ShortHash $srcDir))
        foreach ($f in $imgs) {
            $d = [int]$dimFor[$f]
            $relDir = Split-Path ($f.Substring($srcDir.Length).TrimStart('\'))
            $target = if ($relDir) { Join-Path $mirror $relDir } else { $mirror }
            $out = Join-Path $target (Get-DimmedFileName $f $d)
            try { Invoke-DimImage $f $out $d; [void]$expected.Add($out) }
            catch { $failed.Add("$f  ->  $(Get-ErrMsg $_)") }
            $n++; & $progress $n $total $f
        }
        $map[$kv.Key] = $mirror
    }
    return [pscustomobject]@{ Map = $map; Failed = $failed; Expected = $expected; OutDir = $outDir }
}

# Deletes this tool's own leftover copies (e.g. from an earlier run at other levels) in the
# profile's output folder. Only touches files named like our copies; never touches source images.
function Remove-StaleCopies($result, $sources) {
    $outDir = $result.OutDir
    if (-not (Test-Path -LiteralPath $outDir)) { return }
    $prefix = $outDir.TrimEnd('\') + '\'
    $srcPaths = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
    foreach ($p in $sources.Files.Values) { [void]$srcPaths.Add($p) }
    foreach ($list in $sources.FolderImages.Values) { foreach ($p in $list) { [void]$srcPaths.Add($p) } }
    foreach ($p in $srcPaths) { if ($p.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) { return } }  # source lives here: leave it all alone

    Get-ChildItem -LiteralPath $outDir -Recurse -File -ErrorAction SilentlyContinue |
        Where-Object { ($_.Name -match '__[0-9a-f]{8}__d\d+\.[^.]+$' -or $_.Name -like '*.tmp') -and
                       -not $result.Expected.Contains($_.FullName) -and -not $srcPaths.Contains($_.FullName) } |
        ForEach-Object { try { Remove-Item -LiteralPath $_.FullName -Force } catch { } }
}

# ---------------------------------------------------------------------------------------------
# Write the new profile (clone of the source, pointing at the dimmed copies)
# ---------------------------------------------------------------------------------------------
function New-DimmedProfile($srcProfile, $tree, $pathMap, [string]$newName, [string]$replaceKeyName) {
    if ($replaceKeyName -and ($replaceKeyName -eq $srcProfile.KeyName)) { throw 'Refusing to overwrite the source profile.' }

    if ($replaceKeyName) { $newKeyName = $replaceKeyName }
    else {
        # New GUID, formatted like the existing key names (braces / upper-case)
        $g = [guid]::NewGuid().ToString()
        $newKeyName = if ($srcProfile.KeyName.StartsWith('{')) { "{$g}" } else { $g }
        if ($srcProfile.KeyName -cmatch '[A-F]') { $newKeyName = $newKeyName.ToUpperInvariant() }
    }
    $srcId = $srcProfile.KeyName.Trim('{', '}')
    $newId = $newKeyName.Trim('{', '}')

    $mapper = {
        param($s)
        if ($null -eq $s) { return $s }
        if ($pathMap.ContainsKey($s)) { return $pathMap[$s] }
        if ($s -ieq $srcId) { return $newId }                 # any self-reference to the profile ID
        if ($s -ieq "{$srcId}") { return "{$newId}" }
        return $s
    }.GetNewClosure()

    $newTree = Convert-Tree $tree $mapper
    $nameValue = @($newTree.Values | Where-Object { $_.Name -eq 'Name' })[0]
    $nameValue.Data = $newName

    $root = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($WpRegPath, $true)
    try {
        if ($replaceKeyName) { $root.DeleteSubKeyTree($replaceKeyName, $false) }
        $k = $root.CreateSubKey($newKeyName)
        try { Write-KeyTree $k $newTree } finally { $k.Close() }
    } finally { $root.Close() }
    return $newKeyName
}

function Wait-DisplayFusion([bool]$running, [int]$seconds) {
    $deadline = (Get-Date).AddSeconds($seconds)
    while ((Get-Date) -lt $deadline) {
        $isUp = [bool](Get-Process -Name DisplayFusion -ErrorAction SilentlyContinue)
        if ($isUp -eq $running) { return $true }
        Start-Sleep -Milliseconds 400
        if ($script:Gui) { [System.Windows.Forms.Application]::DoEvents() }
    }
    return $false
}

# ---------------------------------------------------------------------------------------------
# The whole job
# ---------------------------------------------------------------------------------------------
function Invoke-DimProfile($srcProfile, $dimFor, [string]$newName, [bool]$load, [string]$replaceKeyName, [scriptblock]$progress) {
    $df = Get-DisplayFusionPaths
    if (-not $df) { throw 'Could not find DisplayFusionCommand.exe (looked in Program Files and Program Files (x86)).' }

    Write-Status "Reading profile '$($srcProfile.Name)'..."
    $tree = Read-ProfileTree $srcProfile.KeyName
    $sources = Get-ProfileSources $tree
    if ($sources.ImageCount -eq 0) {
        throw "No local image files or folders were found in '$($srcProfile.Name)'. Online/random image sources can't be dimmed."
    }
    $levels = @($dimFor.Values | ForEach-Object { [int]$_ })
    if (@($levels | Where-Object { $_ -gt 0 }).Count -eq 0) { throw 'Every image is set to 0%, so there is nothing to dim.' }

    # 1. Dimmed copies (originals are only read), in a folder of their own for this profile
    $outDir = Join-Path $OutputRoot ('{0}__{1}' -f (Get-SafeName $newName), (Get-ShortHash $newName))
    Write-Status "Dimming $($sources.ImageCount) image(s)..."
    $result = New-DimmedCopies $sources $dimFor $outDir $progress
    if ($result.Map.Count -eq 0) { throw ("None of the images could be dimmed:`n" + ($result.Failed -join "`n")) }

    # 2. Close DisplayFusion so it can't overwrite the registry with its cached settings
    $wasRunning = [bool](Get-Process -Name DisplayFusion -ErrorAction SilentlyContinue)
    if ($wasRunning) {
        Write-Status 'Closing DisplayFusion...'
        & $df.Command -closeall | Out-Null
        if (-not (Wait-DisplayFusion $false 30)) {
            throw "DisplayFusion didn't close, so no settings were changed.`nThe dimmed images are ready in: $outDir"
        }
        Start-Sleep -Seconds 1
    }

    $newKey = $null
    $backup = $null
    try {
        # 3. Full settings backup
        $backupDir = Join-Path $OutputRoot 'Settings backups'
        if (-not (Test-Path -LiteralPath $backupDir)) { [void](New-Item -ItemType Directory -Path $backupDir -Force) }
        $backup = Join-Path $backupDir ('DisplayFusion settings {0:yyyy-MM-dd HHmmss}.reg' -f (Get-Date))
        Write-Status 'Backing up DisplayFusion settings...'
        & reg.exe export "HKCU\$DfRegPath" "$backup" /y | Out-Null
        if ($LASTEXITCODE -ne 0) { throw 'Settings backup failed, so no settings were changed.' }

        # 4. New profile
        Write-Status "Creating profile '$newName'..."
        $newKey = New-DimmedProfile $srcProfile $tree $result.Map $newName $replaceKeyName
    }
    finally {
        # 5. Bring DisplayFusion back
        if ($wasRunning -or $load) {
            Write-Status 'Starting DisplayFusion...'
            Start-Process -FilePath $df.App
        }
    }

    # 6. The profile now points only at this run's copies: tidy older copies in its folder
    Remove-StaleCopies $result $sources

    # 7. Load it
    if ($load) {
        if (Wait-DisplayFusion $true 30) {
            Start-Sleep -Seconds 6     # give DisplayFusion time to finish starting up
            Write-Status "Loading profile '$newName'..."
            & $df.Command -wallpaperloadprofile "$newName" | Out-Null
        }
    }

    $pos = @($levels | Where-Object { $_ -gt 0 } | Sort-Object)
    return [pscustomobject]@{
        NewKey   = $newKey
        Dimmed   = $pos.Count
        Kept     = $levels.Count - $pos.Count
        MinDim   = $pos[0]
        MaxDim   = $pos[-1]
        Failed   = $result.Failed
        ImageDir = $outDir
        Backup   = $backup
    }
}

function Format-Summary($r, [string]$newName) {
    $range = if ($r.MinDim -eq $r.MaxDim) { "$($r.MinDim)%" } else { "$($r.MinDim)-$($r.MaxDim)%" }
    $msg = "Created wallpaper profile '$newName'.`n$($r.Dimmed) image(s) dimmed ($range)"
    if ($r.Kept -gt 0) { $msg += ", $($r.Kept) left at 0%" }
    $msg += ".`n`nDimmed copies:`n$($r.ImageDir)`n`nSettings backup (double-click to restore):`n$($r.Backup)"
    if ($r.Failed.Count -gt 0) {
        $msg += "`n`nCouldn't dim $($r.Failed.Count) image(s); the new profile still uses the originals for these:`n" +
                (($r.Failed | Select-Object -First 8) -join "`n")
    }
    return $msg
}

# ---------------------------------------------------------------------------------------------
# Live desktop preview: minimise other windows, lay a click-through black layer over every
# monitor, and set each layer's opacity to that monitor's image's dim level. A black layer at
# N% opacity gives exactly the same pixels as the dimmed copy.
# ---------------------------------------------------------------------------------------------
$script:Overlays = @()
$script:Previewing = $false
$script:ShellApp = $null

function Update-Overlays {
    if (-not $script:Previewing) { return }
    $mons = @($script:Monitors)
    $sel = @(Get-SelectedIndices)
    $fallback = if ($sel.Count -gt 0) { $script:Items[$sel[0]].Dim } else { $script:Track.Value }
    for ($i = 0; $i -lt $script:Overlays.Count; $i++) {
        # Pair monitors left-to-right with the profile's monitor images; if the counts don't
        # match (or the image is spanned), use one level everywhere.
        $dim = if ($mons.Count -eq $script:Overlays.Count) { $mons[$i].Item.Dim }
               elseif ($mons.Count -eq 1) { $mons[0].Item.Dim }
               else { $fallback }
        $script:Overlays[$i].Opacity = [Math]::Min(0.95, [Math]::Max(0.0, $dim / 100.0))
    }
}

function Start-DesktopPreview {
    if ($script:Previewing) { return }
    $script:Previewing = $true
    try {
        $script:ShellApp = New-Object -ComObject Shell.Application
        $script:ShellApp.MinimizeAll()                     # same as Win+M; undone when the preview ends
    } catch { $script:ShellApp = $null }
    Start-Sleep -Milliseconds 450
    [System.Windows.Forms.Application]::DoEvents()

    $script:Form.TopMost = $true
    $script:Form.WindowState = 'Normal'
    $screens = @([System.Windows.Forms.Screen]::AllScreens | Sort-Object { $_.Bounds.X }, { $_.Bounds.Y })
    $script:Overlays = @(foreach ($scr in $screens) { New-Object DfOverlay -ArgumentList $scr.Bounds })
    Update-Overlays
    foreach ($o in $script:Overlays) { $o.Show() }
    $script:Form.Activate()
    $script:BtnPreview.Text = 'End desktop preview'
    $script:PreviewHint.Text = 'Each monitor shows its own image''s level. Icons dim in the preview only.'
}

function Stop-DesktopPreview {
    if (-not $script:Previewing) { return }
    $script:Previewing = $false
    foreach ($o in $script:Overlays) { try { $o.Close(); $o.Dispose() } catch { } }
    $script:Overlays = @()
    if ($script:Form -and -not $script:Form.IsDisposed) {
        $script:Form.TopMost = $false
        $script:BtnPreview.Text = 'Preview on desktop'
        $script:PreviewHint.Text = 'Minimises your windows while previewing.'
    }
    if ($script:ShellApp) {
        try { $script:ShellApp.UndoMinimizeALL() } catch { }
        $script:ShellApp = $null
    }
}

# ---------------------------------------------------------------------------------------------
# GUI helpers
# ---------------------------------------------------------------------------------------------
function Get-SelectedIndices { return @($script:List.SelectedIndices | ForEach-Object { [int]$_ }) }

function Set-ItemDim([int]$index, [int]$dim) {
    $script:Items[$index].Dim = $dim
    $script:List.Items[$index].SubItems[2].Text = "$dim%"
}

function Show-ItemPreview($it) {
    if ($null -eq $it) { return }
    if ($null -eq $it.Thumb) {
        try { $it.Thumb = ([DfDimmer]::Load($it.Path, 650)).Bitmap } catch { $it.Thumb = $false }
    }
    $old = $script:Pic.Image
    if ($it.Thumb -is [bool]) {
        $script:Pic.Image = $null
        $script:StatusLabel.Text = "Can't preview $($it.Name) (format not supported by Windows imaging)."
    } else {
        $bmp = [DfDimmer]::Dim($it.Thumb, [double]$it.Dim)
        $ms = New-Object IO.MemoryStream (, [DfDimmer]::ToPngBytes($bmp))
        $script:Pic.Image = [System.Drawing.Image]::FromStream($ms)
    }
    if ($old) { $old.Dispose() }
}

# Shows the first selected image's level on the slider without changing the others.
function Sync-SliderToSelection {
    $sel = @(Get-SelectedIndices)
    if ($sel.Count -eq 0) { return }
    $first = $script:Items[$sel[0]]
    $script:Syncing = $true
    $script:Track.Value = $first.Dim
    $script:Num.Value = $first.Dim
    $script:Syncing = $false
    $script:SelLabel.Text = if ($sel.Count -gt 1) { "Dim level for the $($sel.Count) selected images:" } else { "Dim level for $($first.Name):" }
    Show-ItemPreview $first
}

function Invoke-AutoForIndices([int[]]$indices) {
    if ($indices.Count -eq 0) { return }
    $script:Form.Cursor = 'WaitCursor'
    $script:Bar.Maximum = $indices.Count; $script:Bar.Value = 0
    $bad = 0; $n = 0
    foreach ($i in $indices) {
        try { Set-ItemDim $i (Get-AutoDimForFile $script:Items[$i].Path 4.5) } catch { $bad++ }
        $n++; $script:Bar.Value = $n
        $script:StatusLabel.Text = "Auto: analysed $n of $($indices.Count)..."
        [System.Windows.Forms.Application]::DoEvents()
    }
    $levels = @($indices | ForEach-Object { $script:Items[$_].Dim } | Sort-Object)
    $range = if ($levels[0] -eq $levels[-1]) { "$($levels[0])%" } else { "$($levels[0])-$($levels[-1])%" }
    $script:StatusLabel.Text = "Auto set $($indices.Count) image(s) to $range." + $(if ($bad) { " $bad couldn't be read." } else { '' })
    $script:Bar.Value = 0
    $script:Form.Cursor = 'Default'
    Sync-SliderToSelection
    Update-Overlays
}

# ---------------------------------------------------------------------------------------------
# GUI
# ---------------------------------------------------------------------------------------------
function Show-Gui($profiles, [string]$currentId) {
    Add-Type -AssemblyName System.Windows.Forms, System.Drawing
    [System.Windows.Forms.Application]::EnableVisualStyles()
    $script:Gui = $true
    $script:Profiles = $profiles
    $script:Items = New-Object System.Collections.ArrayList
    $script:Monitors = @()
    $script:Syncing = $false
    $script:SettingName = $false
    $script:NameEdited = $false

    $form = New-Object System.Windows.Forms.Form
    $form.Text = 'Dim DisplayFusion Wallpaper'
    $form.StartPosition = 'CenterScreen'
    $form.FormBorderStyle = 'FixedDialog'
    $form.MaximizeBox = $false
    $form.Font = New-Object System.Drawing.Font('Segoe UI', 9)
    $form.ClientSize = New-Object System.Drawing.Size(760, 560)
    $script:Form = $form

    function Add-Label($text, $x, $y, $w = 110, $h = 20) {
        $l = New-Object System.Windows.Forms.Label
        $l.Text = $text; $l.Location = New-Object System.Drawing.Point($x, $y); $l.Size = New-Object System.Drawing.Size($w, $h)
        $form.Controls.Add($l); return $l
    }
    function Add-Button($text, $x, $y, $w, $h = 27) {
        $b = New-Object System.Windows.Forms.Button
        $b.Text = $text; $b.Location = New-Object System.Drawing.Point($x, $y); $b.Size = New-Object System.Drawing.Size($w, $h)
        $form.Controls.Add($b); return $b
    }

    # --- source profile
    [void](Add-Label 'Source profile:' 15 16)
    $script:Combo = New-Object System.Windows.Forms.ComboBox
    $Combo.DropDownStyle = 'DropDownList'
    $Combo.Location = New-Object System.Drawing.Point(130, 12); $Combo.Size = New-Object System.Drawing.Size(615, 24)
    foreach ($p in $profiles) { [void]$Combo.Items.Add($p.Name) }
    $form.Controls.Add($Combo)

    # --- image list (left)
    $script:List = New-Object System.Windows.Forms.ListView
    $List.View = 'Details'; $List.FullRowSelect = $true; $List.MultiSelect = $true; $List.HideSelection = $false
    $List.ShowItemToolTips = $true
    $List.Location = New-Object System.Drawing.Point(15, 48); $List.Size = New-Object System.Drawing.Size(390, 330)
    [void]$List.Columns.Add('Image', 200)
    [void]$List.Columns.Add('Used as', 120)
    [void]$List.Columns.Add('Dim', 50, 'Right')
    $form.Controls.Add($List)

    # --- preview + controls (right)
    $script:Pic = New-Object System.Windows.Forms.PictureBox
    $Pic.Location = New-Object System.Drawing.Point(420, 48); $Pic.Size = New-Object System.Drawing.Size(325, 183)
    $Pic.SizeMode = 'Zoom'; $Pic.BorderStyle = 'FixedSingle'; $Pic.BackColor = [System.Drawing.Color]::Black
    $form.Controls.Add($Pic)

    $script:SelLabel = Add-Label 'Dim level:' 420 240 325
    $script:Track = New-Object System.Windows.Forms.TrackBar
    $Track.Minimum = 0; $Track.Maximum = 95; $Track.TickFrequency = 5; $Track.SmallChange = 1; $Track.LargeChange = 5; $Track.Value = 40
    $Track.Location = New-Object System.Drawing.Point(415, 260); $Track.Size = New-Object System.Drawing.Size(230, 45)
    $form.Controls.Add($Track)
    $script:Num = New-Object System.Windows.Forms.NumericUpDown
    $Num.Minimum = 0; $Num.Maximum = 95; $Num.Value = 40
    $Num.Location = New-Object System.Drawing.Point(650, 264); $Num.Size = New-Object System.Drawing.Size(50, 24)
    $form.Controls.Add($Num)
    [void](Add-Label '%' 703 267 20)

    $script:BtnAuto    = Add-Button 'Auto (selected)' 420 305 110
    $script:BtnAutoAll = Add-Button 'Auto (all)' 535 305 85
    $script:BtnAll     = Add-Button 'Set all to this' 625 305 120
    $script:Info = Add-Label '' 420 340 325 40

    $script:Tips = New-Object System.Windows.Forms.ToolTip
    $Tips.AutoPopDelay = 30000
    $autoTip = "Smallest dim level that keeps white icon labels readable`n(4.5:1 contrast) over 90% of the image - worked out per image."
    $Tips.SetToolTip($BtnAuto, $autoTip); $Tips.SetToolTip($BtnAutoAll, $autoTip)

    # --- bottom
    [void](Add-Label 'New profile name:' 15 395)
    $script:NameBox = New-Object System.Windows.Forms.TextBox
    $NameBox.Location = New-Object System.Drawing.Point(130, 392); $NameBox.Size = New-Object System.Drawing.Size(615, 24)
    $form.Controls.Add($NameBox)

    $script:LoadBox = New-Object System.Windows.Forms.CheckBox
    $LoadBox.Text = 'Switch to the new profile when done'; $LoadBox.Checked = $true
    $LoadBox.Location = New-Object System.Drawing.Point(130, 422); $LoadBox.Size = New-Object System.Drawing.Size(300, 22)
    $form.Controls.Add($LoadBox)

    $script:BtnPreview = Add-Button 'Preview on desktop' 130 450 150 28
    $script:PreviewHint = Add-Label 'Minimises your windows while previewing.' 288 456 457
    $PreviewHint.ForeColor = [System.Drawing.SystemColors]::GrayText

    $script:Bar = New-Object System.Windows.Forms.ProgressBar
    $Bar.Location = New-Object System.Drawing.Point(15, 492); $Bar.Size = New-Object System.Drawing.Size(730, 14)
    $form.Controls.Add($Bar)
    $script:StatusLabel = Add-Label 'Ready.' 15 520 555

    $script:BtnOk = Add-Button 'Create' 580 514 80 28
    $form.AcceptButton = $BtnOk
    $BtnCancel = Add-Button 'Cancel' 665 514 80 28
    $BtnCancel.DialogResult = 'Cancel'; $form.CancelButton = $BtnCancel

    # --- events
    $Combo.add_SelectedIndexChanged({
        $p = $script:Profiles[$script:Combo.SelectedIndex]
        $script:Info.Text = 'Scanning profile...'
        $script:Form.Cursor = 'WaitCursor'
        [System.Windows.Forms.Application]::DoEvents()
        $script:List.Items.Clear()
        $script:Items = New-Object System.Collections.ArrayList
        $script:Monitors = @()
        if ($script:Pic.Image) { $script:Pic.Image.Dispose(); $script:Pic.Image = $null }
        try {
            $tree = Read-ProfileTree $p.KeyName
            $src = Get-ProfileSources $tree
            $r = Get-ImageItems $tree $src ([int]$script:Track.Value)
            $script:Items = $r.Items
            $script:Monitors = $r.Monitors
            $script:List.BeginUpdate()
            foreach ($it in $script:Items) {
                $lvi = New-Object System.Windows.Forms.ListViewItem ($it.Name)
                [void]$lvi.SubItems.Add($it.Role)
                [void]$lvi.SubItems.Add("$($it.Dim)%")
                $lvi.ToolTipText = $it.Path
                [void]$script:List.Items.Add($lvi)
            }
            $script:List.EndUpdate()
            if ($script:Items.Count -eq 0) {
                $script:Info.Text = 'No local images in this profile (online/random sources can''t be dimmed).'
            } else {
                $script:Info.Text = "$($script:Items.Count) image(s), $($script:Monitors.Count) on monitors. Ctrl/Shift-click to set several at once."
                $script:List.Items[0].Selected = $true
            }
        } catch { $script:Info.Text = "Couldn't read profile: $(Get-ErrMsg $_)" }
        $script:Form.Cursor = 'Default'
        if (-not $script:NameEdited) {
            $script:SettingName = $true
            $script:NameBox.Text = '{0} (Dimmed)' -f $p.Name
            $script:SettingName = $false
        }
        Update-Overlays
    })

    $List.add_SelectedIndexChanged({ Sync-SliderToSelection })

    $Track.add_ValueChanged({
        if ($script:Num.Value -ne $script:Track.Value) { $script:Num.Value = $script:Track.Value }
        if ($script:Syncing) { return }
        $sel = @(Get-SelectedIndices)
        foreach ($i in $sel) { Set-ItemDim $i $script:Track.Value }
        if ($sel.Count -gt 0) { Show-ItemPreview $script:Items[$sel[0]] }
        Update-Overlays
    })
    $Num.add_ValueChanged({
        if ($script:Track.Value -ne [int]$script:Num.Value) { $script:Track.Value = [int]$script:Num.Value }
    })

    $BtnAuto.add_Click({
        $sel = @(Get-SelectedIndices)
        if ($sel.Count -eq 0) { Show-Error 'Select one or more images first.'; return }
        Invoke-AutoForIndices $sel
    })
    $BtnAutoAll.add_Click({
        if ($script:Items.Count -eq 0) { return }
        Invoke-AutoForIndices @(0..($script:Items.Count - 1))
    })
    $BtnAll.add_Click({
        for ($i = 0; $i -lt $script:Items.Count; $i++) { Set-ItemDim $i $script:Track.Value }
        $script:StatusLabel.Text = "All $($script:Items.Count) image(s) set to $($script:Track.Value)%."
        Sync-SliderToSelection
        Update-Overlays
    })

    $NameBox.add_TextChanged({ if (-not $script:SettingName) { $script:NameEdited = $true } })
    $BtnPreview.add_Click({ if ($script:Previewing) { Stop-DesktopPreview } else { Start-DesktopPreview } })
    $form.add_FormClosing({ Stop-DesktopPreview })

    $BtnOk.add_Click({
        $p = $script:Profiles[$script:Combo.SelectedIndex]
        $name = $script:NameBox.Text.Trim()
        if ($script:Items.Count -eq 0) { Show-Error 'This profile has no local images to dim.'; return }
        if (-not $name) { Show-Error 'Enter a name for the new profile.'; return }
        $dimFor = @{}
        foreach ($it in $script:Items) { $dimFor[$it.Path] = [int]$it.Dim }
        if (@($script:Items | Where-Object { $_.Dim -gt 0 }).Count -eq 0) { Show-Error 'Every image is at 0%. Set a dim level first.'; return }

        $replaceKey = $null
        $existing = @($script:Profiles | Where-Object { $_.Name -ieq $name })
        if ($existing.Count -gt 0) {
            if ($existing[0].KeyName -eq $p.KeyName) { Show-Error 'The new name matches the source profile. Pick a different name - the source is never overwritten.'; return }
            $ans = [System.Windows.Forms.MessageBox]::Show("A profile named '$name' already exists.`n`nReplace it? (Its monitor-profile links are kept.)", 'Profile exists', 'YesNo', 'Question')
            if ($ans -ne 'Yes') { return }
            $replaceKey = $existing[0].KeyName
        }

        Stop-DesktopPreview
        foreach ($c in $script:Form.Controls) { $c.Enabled = $false }
        $script:StatusLabel.Enabled = $true; $script:Bar.Enabled = $true
        $progress = {
            param($i, $n, $f)
            $script:Bar.Maximum = $n; $script:Bar.Value = [Math]::Min($i, $n)
            $script:StatusLabel.Text = "Dimming $i of $n..."
            [System.Windows.Forms.Application]::DoEvents()
        }
        try {
            $r = Invoke-DimProfile $p $dimFor $name $script:LoadBox.Checked $replaceKey $progress
            Write-Status 'Done.'
            [void][System.Windows.Forms.MessageBox]::Show((Format-Summary $r $name), 'Done', 'OK', 'Information')
            $script:Form.DialogResult = 'OK'
            $script:Form.Close()
        } catch {
            Write-Status 'Failed.'
            Show-Error (Get-ErrMsg $_)
            foreach ($c in $script:Form.Controls) { $c.Enabled = $true }
        }
    })

    # Default selection: the profile that's currently loaded
    $idx = 0
    for ($i = 0; $i -lt $profiles.Count; $i++) {
        if ($currentId -and ($profiles[$i].KeyName.Trim('{', '}') -ieq $currentId.Trim('{', '}'))) { $idx = $i }
    }
    $script:InitialIndex = $idx
    $form.add_Shown({ $script:Combo.SelectedIndex = $script:InitialIndex })
    [void]$form.ShowDialog()
    $form.Dispose()
}

# ---------------------------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------------------------
$profiles = Get-WallpaperProfiles
$currentId = Get-CurrentProfileId

if ($PSBoundParameters.ContainsKey('DimPercent') -or $AutoDim) {
    if ($profiles.Count -eq 0) { Show-Error "No DisplayFusion wallpaper profiles found under HKCU\$WpRegPath."; exit 1 }
    $src = if ($SourceProfile) { @($profiles | Where-Object { $_.Name -ieq $SourceProfile })[0] }
           else { @($profiles | Where-Object { $currentId -and $_.KeyName.Trim('{', '}') -ieq $currentId.Trim('{', '}') })[0] }
    if (-not $src) { Show-Error "Wallpaper profile not found. Available: $(($profiles.Name) -join ', ')"; exit 1 }

    $tree = Read-ProfileTree $src.KeyName
    $srcInfo = Get-ProfileSources $tree
    $items = (Get-ImageItems $tree $srcInfo $DimPercent).Items
    if ($items.Count -eq 0) { Show-Error "No local images in '$($src.Name)' to dim."; exit 1 }

    if ($AutoDim) {
        $n = 0
        foreach ($it in $items) {
            $n++
            Write-Progress -Activity 'Choosing a dim level for each image' -Status "$n of $($items.Count)" -PercentComplete (100 * $n / $items.Count) -CurrentOperation $it.Path
            try { $it.Dim = Get-AutoDimForFile $it.Path $AutoContrast }
            catch { Write-Warning "Couldn't analyse $($it.Path); using $DimPercent%."; $it.Dim = $DimPercent }
            Write-Host ("  {0,3}%  {1}" -f $it.Dim, $it.Path)
        }
        Write-Progress -Activity 'Choosing a dim level for each image' -Completed
    }
    $dimFor = @{}
    foreach ($it in $items) { $dimFor[$it.Path] = [int]$it.Dim }
    if (@($items | Where-Object { $_.Dim -gt 0 }).Count -eq 0) {
        Write-Host "Every image in '$($src.Name)' is already dark enough - nothing to do." -ForegroundColor Green; exit 0
    }

    if (-not $NewProfileName) {
        $NewProfileName = if ($AutoDim) { '{0} (Auto dim)' -f $src.Name } else { '{0} (Dim {1}%)' -f $src.Name, $DimPercent }
    }
    $replaceKey = $null
    $existing = @($profiles | Where-Object { $_.Name -ieq $NewProfileName })
    if ($existing.Count -gt 0) {
        if ($existing[0].KeyName -eq $src.KeyName) { Show-Error 'The new name matches the source profile; the source is never overwritten.'; exit 1 }
        if (-not $Force) { Show-Error "A profile named '$NewProfileName' already exists. Use -Force to replace it or pick another -NewProfileName."; exit 1 }
        $replaceKey = $existing[0].KeyName
    }

    $progress = { param($i, $n, $f) Write-Progress -Activity 'Dimming wallpaper images' -Status "$i of $n" -PercentComplete (100 * $i / $n) -CurrentOperation $f }
    try {
        $r = Invoke-DimProfile $src $dimFor $NewProfileName (-not $NoLoad) $replaceKey $progress
        Write-Progress -Activity 'Dimming wallpaper images' -Completed
        Write-Host (Format-Summary $r $NewProfileName) -ForegroundColor Green
    } catch { Show-Error (Get-ErrMsg $_); exit 1 }
}
else {
    Add-Type -AssemblyName System.Windows.Forms
    if ($profiles.Count -eq 0) {
        $script:Gui = $true
        Show-Error "No DisplayFusion wallpaper profiles were found under HKCU\$WpRegPath.`n`nOpen DisplayFusion > Desktop Wallpaper, make sure 'Allow DisplayFusion to manage the desktop wallpaper' is on, and click Apply once."
        exit 1
    }
    Show-Gui $profiles $currentId
}
DTNRWORKSHOP ARMAMENTSSN 2012.OTT.0176