Private/ConvertTo-VellumColor.ps1

function ConvertTo-VellumColor {
    <#
    .SYNOPSIS
        Normalises a colour value to a [double[3]] of 0..1 RGB components.
    .DESCRIPTION
        Internal helper so every public colour parameter (-Color, -BorderColor,
        -HeaderBackground, -AlternateRowBackground, and rich-cell backgrounds)
        accepts the same set of forms:

          - an R,G,B array of three numbers in 0..1 (the original contract);
          - a hex string '#RRGGBB', 'RRGGBB', '#RGB', or 'RGB';
          - a known colour name (the 16 HTML basic colours plus about 35 more CSS names).

        Returns the normalised triple, or $null when the input is $null (callers
        treat $null as "not specified"). Throws a clear error on anything else.
        Named colours are kept to a small curated table on purpose: System.Drawing
        colour parsing is not dependable cross-platform on .NET 10.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Pure conversion; no state change.')]
    [CmdletBinding()]
    [OutputType([double[]])]
    param(
        [Parameter(Mandatory)]
        [AllowNull()]
        [object]$Color,

        # The document the colour is for and the cmdlet resolving it: a PDF/A document whose output intent
        # is CMYK then gets a one-time warning that RGB colour fails validation (Write-VellumPdfRgbIntentWarning).
        [object]$Document,

        [string]$CommandName
    )

    if ($null -eq $Color) { return $null }
    if ($Document) {
        $resolved = ConvertTo-VellumColor -Color $Color
        Write-VellumPdfRgbIntentWarning -Document $Document -CommandName $CommandName
        return , $resolved
    }

    # Already an array of components.
    if ($Color -is [System.Array]) {
        $vals = @($Color)
        if ($vals.Count -ne 3) {
            throw (New-VellumPdfErrorRecord InvalidArgument "ConvertTo-VellumColor: an RGB array must have exactly 3 components; got $($vals.Count).")
        }
        $triple = foreach ($v in $vals) {
            # Cast straight to [double] (PowerShell converts numbers and numeric
            # strings invariantly). Avoid [string]+TryParse: that round-trips
            # through the current culture and misreads '0.2' as 2 where '.' is a
            # thousands separator.
            try { $d = [double]$v }
            catch { throw (New-VellumPdfErrorRecord InvalidArgument "ConvertTo-VellumColor: RGB component '$v' is not a number.") }
            # IsFinite first: NaN fails both comparisons and would otherwise be written as 'NaN 0 0 rg'.
            if (-not [double]::IsFinite($d) -or $d -lt 0 -or $d -gt 1) {
                throw (New-VellumPdfErrorRecord InvalidArgument "ConvertTo-VellumColor: RGB components must be finite numbers between 0 and 1; got $d.")
            }
            $d
        }
        return [double[]]$triple
    }

    $text = ([string]$Color).Trim()

    # Hex MUST be #-prefixed: #RRGGBB or #RGB shorthand. Requiring the '#' avoids
    # the trap where a bare number like '255' parses as hex (-> '225555') instead
    # of being rejected; a caller meaning an 8-bit channel would get a silently
    # wrong colour.
    if ($text.StartsWith('#')) {
        $hex = $text.Substring(1)
        if ($hex -match '^[0-9a-fA-F]{6}$') {
            return [double[]]@(
                [Convert]::ToInt32($hex.Substring(0, 2), 16) / 255.0
                [Convert]::ToInt32($hex.Substring(2, 2), 16) / 255.0
                [Convert]::ToInt32($hex.Substring(4, 2), 16) / 255.0
            )
        }
        if ($hex -match '^[0-9a-fA-F]{3}$') {
            # Each nibble doubles: '#abc' -> 'aabbcc'.
            return [double[]]@(
                [Convert]::ToInt32("$($hex[0])$($hex[0])", 16) / 255.0
                [Convert]::ToInt32("$($hex[1])$($hex[1])", 16) / 255.0
                [Convert]::ToInt32("$($hex[2])$($hex[2])", 16) / 255.0
            )
        }
        throw (New-VellumPdfErrorRecord InvalidArgument "ConvertTo-VellumColor: '$Color' is not a valid hex colour. Use #RRGGBB or #RGB.")
    }

    # Curated named colours (HTML basic 16 plus common aliases).
    $named = @{
        black = @(0, 0, 0);            white  = @(1, 1, 1)
        red   = @(1, 0, 0);            lime   = @(0, 1, 0)
        green = @(0, 0.5, 0);          blue   = @(0, 0, 1)
        yellow = @(1, 1, 0);           cyan   = @(0, 1, 1)
        aqua  = @(0, 1, 1);            magenta = @(1, 0, 1)
        fuchsia = @(1, 0, 1);          silver = @(0.7529, 0.7529, 0.7529)
        gray  = @(0.5, 0.5, 0.5);      grey   = @(0.5, 0.5, 0.5)
        maroon = @(0.5, 0, 0);         olive  = @(0.5, 0.5, 0)
        navy  = @(0, 0, 0.5);          teal   = @(0, 0.5, 0.5)
        purple = @(0.5, 0, 0.5);       orange = @(1, 0.6471, 0)
    }
    $key = $text.ToLowerInvariant()
    if ($named.ContainsKey($key)) {
        return [double[]]$named[$key]
    }

    # More CSS colour names, as 0xRRGGBB.
    $css = @{
        pink = 0xFFC0CB; hotpink = 0xFF69B4; brown = 0xA52A2A; gold = 0xFFD700; coral = 0xFF7F50
        salmon = 0xFA8072; crimson = 0xDC143C; indigo = 0x4B0082; violet = 0xEE82EE; turquoise = 0x40E0D0
        khaki = 0xF0E68C; beige = 0xF5F5DC; ivory = 0xFFFFF0; lavender = 0xE6E6FA; tan = 0xD2B48C
        chocolate = 0xD2691E; sienna = 0xA0522D; orchid = 0xDA70D6; plum = 0xDDA0DD; skyblue = 0x87CEEB
        steelblue = 0x4682B4; royalblue = 0x4169E1; slategray = 0x708090; slategrey = 0x708090
        darkgray = 0xA9A9A9; darkgrey = 0xA9A9A9; lightgray = 0xD3D3D3; lightgrey = 0xD3D3D3
        darkgreen = 0x006400; forestgreen = 0x228B22; seagreen = 0x2E8B57; darkblue = 0x00008B
        darkred = 0x8B0000; orangered = 0xFF4500; darkorange = 0xFF8C00; lightblue = 0xADD8E6
        lightgreen = 0x90EE90; midnightblue = 0x191970; whitesmoke = 0xF5F5F5; gainsboro = 0xDCDCDC
    }
    if ($css.ContainsKey($key)) {
        $rgb = [int]$css[$key]
        return [double[]]@(
            ((($rgb -shr 16) -band 255) / 255.0),
            ((($rgb -shr 8) -band 255) / 255.0),
            (($rgb -band 255) / 255.0))
    }

    throw (New-VellumPdfErrorRecord InvalidArgument ("ConvertTo-VellumColor: unrecognised colour '$Color'. Use an R,G,B array of three " +
        "numbers in 0..1, a hex string like '#3366cc' or '#36c', or a known name " +
        '(black, white, red, lime, green, blue, yellow, cyan, magenta, silver, gray, ' +
        'maroon, olive, navy, teal, purple, orange, pink, brown, gold, coral, crimson, indigo, ' +
        'violet, turquoise, khaki, beige, ivory, lavender, tan, steelblue, skyblue, royalblue, ' +
        'lightgray, darkgray, lightblue, lightgreen, darkgreen, darkblue, darkred, ... - any standard CSS name).'))
}