Private/Png.ps1

# Generation of a solid PNG image, with no external dependency.
#
# The opaque backdrop (Install-TerminalColorsBackdrop) needs a single-colour image
# to lay over the pane. System.Drawing would do the job on 5.1 but is not
# guaranteed on PowerShell 7 (Windows-only assembly, absent from some
# installations), so the PNG is encoded by hand.
#
# The resulting file is tiny (under 100 bytes) and perfectly deterministic: the
# same colour always produces the same bytes.

$script:TcCrcTable = $null
$script:TcPngWidth = 2
$script:TcPngHeight = 2

function Get-TcCrcTable {
    <#
        .SYNOPSIS
        CRC-32 table (polynomial 0xEDB88320), computed only once.
    #>

    [CmdletBinding()]
    param()

    if ($null -ne $script:TcCrcTable) { return $script:TcCrcTable }

    $table = New-Object 'System.Int64[]' 256
    for ($n = 0; $n -lt 256; $n++) {
        $c = [int64]$n
        for ($k = 0; $k -lt 8; $k++) {
            if ($c -band 1) {
                # 3988292384 = 0xEDB88320; PowerShell would read the hex literal as
                # a negative Int32.
                $c = 3988292384 -bxor ($c -shr 1)
            } else {
                $c = $c -shr 1
            }
        }
        $table[$n] = $c -band 4294967295
    }

    $script:TcCrcTable = $table
    return $table
}

function Get-TcCrc32 {
    [CmdletBinding()]
    param([byte[]] $Bytes)

    $table = Get-TcCrcTable
    $c = [int64]4294967295
    foreach ($b in $Bytes) {
        $index = [int](($c -bxor [int64]$b) -band 255)
        $c = $table[$index] -bxor ($c -shr 8)
    }
    return (($c -bxor 4294967295) -band 4294967295)
}

function Get-TcAdler32 {
    <#
        .SYNOPSIS
        Adler-32 checksum, required at the end of a zlib stream.
    #>

    [CmdletBinding()]
    param([byte[]] $Bytes)

    $a = [int64]1
    $b = [int64]0
    foreach ($x in $Bytes) {
        $a = ($a + [int64]$x) % 65521
        $b = ($b + $a) % 65521
    }
    return ((($b -shl 16) -bor $a) -band 4294967295)
}

function ConvertTo-TcUInt32Bytes {
    <#
        .SYNOPSIS
        32-bit big-endian integer, as the PNG format requires.
    #>

    [CmdletBinding()]
    param([int64] $Value)

    return [byte[]] @(
        [byte](($Value -shr 24) -band 255),
        [byte](($Value -shr 16) -band 255),
        [byte](($Value -shr 8) -band 255),
        [byte]($Value -band 255)
    )
}

function New-TcPngChunk {
    <#
        .SYNOPSIS
        Assembles a PNG chunk: length, type, data, CRC of type+data.
    #>

    [CmdletBinding()]
    param(
        [string] $Type,
        [byte[]] $Data
    )

    if ($null -eq $Data) { $Data = [byte[]] @() }

    # The [byte[]] casts are essential: a value returned by a PowerShell function
    # comes back as System.Object[], which AddRange refuses.
    $body = New-Object 'System.Collections.Generic.List[byte]'
    $body.AddRange([byte[]][System.Text.Encoding]::ASCII.GetBytes($Type))
    if ($Data.Length -gt 0) { $body.AddRange([byte[]]$Data) }

    $bytes = $body.ToArray()
    $out = New-Object 'System.Collections.Generic.List[byte]'
    $out.AddRange([byte[]](ConvertTo-TcUInt32Bytes -Value ([int64]$Data.Length)))
    $out.AddRange([byte[]]$bytes)
    $out.AddRange([byte[]](ConvertTo-TcUInt32Bytes -Value (Get-TcCrc32 -Bytes $bytes)))
    return $out.ToArray()
}

function New-TcSolidPngBytes {
    <#
        .SYNOPSIS
        Bytes of a solid PNG in the requested colour (8-bit truecolor).
 
        .DESCRIPTION
        The zlib stream of the IDAT chunk uses a [stored] (uncompressed) block: for
        a handful of bytes that is as compact as a real deflate, and it avoids
        depending on System.IO.Compression.
    #>

    [CmdletBinding()]
    [OutputType([byte[]])]
    param([hashtable] $Rgb)

    if ($null -eq $Rgb) { throw 'TerminalColors: missing colour for PNG generation.' }

    $r = [byte]([int]$Rgb.R)
    $g = [byte]([int]$Rgb.G)
    $b = [byte]([int]$Rgb.B)

    # --- Raw data: one filter byte (0 = none) per row -----------------------
    $raw = New-Object 'System.Collections.Generic.List[byte]'
    for ($y = 0; $y -lt $script:TcPngHeight; $y++) {
        $raw.Add([byte]0)
        for ($x = 0; $x -lt $script:TcPngWidth; $x++) {
            $raw.Add($r); $raw.Add($g); $raw.Add($b)
        }
    }
    $rawBytes = $raw.ToArray()

    # --- zlib stream: header, uncompressed block, Adler-32 ------------------
    $zlib = New-Object 'System.Collections.Generic.List[byte]'
    $zlib.Add([byte]0x78)   # CM = deflate, CINFO = 32 KB window
    $zlib.Add([byte]0x01)   # no dictionary; (0x7801 % 31) == 0
    $zlib.Add([byte]0x01)   # BFINAL = 1, BTYPE = 00 (stored)
    $len = $rawBytes.Length
    $zlib.Add([byte]($len -band 255))
    $zlib.Add([byte](($len -shr 8) -band 255))
    $zlib.Add([byte]((-bnot $len) -band 255))
    $zlib.Add([byte](((-bnot $len) -shr 8) -band 255))
    $zlib.AddRange([byte[]]$rawBytes)
    $zlib.AddRange([byte[]](ConvertTo-TcUInt32Bytes -Value (Get-TcAdler32 -Bytes $rawBytes)))

    # --- IHDR ---------------------------------------------------------------
    $ihdr = New-Object 'System.Collections.Generic.List[byte]'
    $ihdr.AddRange([byte[]](ConvertTo-TcUInt32Bytes -Value ([int64]$script:TcPngWidth)))
    $ihdr.AddRange([byte[]](ConvertTo-TcUInt32Bytes -Value ([int64]$script:TcPngHeight)))
    $ihdr.Add([byte]8)   # 8 bits per channel
    $ihdr.Add([byte]2)   # type 2: RGB without alpha
    $ihdr.Add([byte]0)   # deflate compression
    $ihdr.Add([byte]0)   # standard filtering
    $ihdr.Add([byte]0)   # not interlaced

    $png = New-Object 'System.Collections.Generic.List[byte]'
    $png.AddRange([byte[]] @(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A))
    $png.AddRange([byte[]](New-TcPngChunk -Type 'IHDR' -Data $ihdr.ToArray()))
    $png.AddRange([byte[]](New-TcPngChunk -Type 'IDAT' -Data $zlib.ToArray()))
    $png.AddRange([byte[]](New-TcPngChunk -Type 'IEND' -Data ([byte[]] @())))
    return $png.ToArray()
}

function Write-TcSolidPng {
    <#
        .SYNOPSIS
        Writes a solid PNG to the requested location, creating the folder if needed.
    #>

    [CmdletBinding()]
    param(
        [string] $Path,
        [hashtable] $Rgb
    )

    $directory = Split-Path $Path -Parent
    if ($directory -and -not (Test-Path -LiteralPath $directory)) {
        New-Item -ItemType Directory -Path $directory -Force | Out-Null
    }

    [System.IO.File]::WriteAllBytes($Path, (New-TcSolidPngBytes -Rgb $Rgb))
    return $Path
}