Private/ConvertFrom-PngData.ps1
|
function ConvertFrom-PngData { <# .SYNOPSIS Decodes a PNG byte array into flat RGBA pixel data. .DESCRIPTION Pure PowerShell PNG decoder that extracts RGBA pixel data from PNG files. Handles RGBA (color type 6) and RGB (color type 2) with bit depth 8. Applies PNG scanline unfiltering (None, Sub, Up, Average, Paeth). .PARAMETER Data Byte array containing PNG file data. .OUTPUTS PSCustomObject with Width, Height, and RgbaData (flat byte array) properties. .NOTES Only supports bit depth 8 with color types 2 (RGB) and 6 (RGBA). Does not support interlaced PNGs. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [byte[]]$Data ) # validate PNG signature $signature = @(137, 80, 78, 71, 13, 10, 26, 10) for ($i = 0; $i -lt 8; $i++) { if ($Data[$i] -ne $signature[$i]) { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.FormatException]::new('Invalid PNG signature'), 'InvalidPngSignature', [System.Management.Automation.ErrorCategory]::InvalidData, $Data ) $PSCmdlet.ThrowTerminatingError($errorRecord) } } # parse chunks to extract IHDR metadata and IDAT compressed data $offset = 8 $width = 0 $height = 0 $bitDepth = 0 $colorType = 0 $bytesPerPixel = 0 $idatData = [System.Collections.Generic.List[byte]]::new() while ($offset -lt $Data.Length) { $chunkLength = ([int]$Data[$offset] -shl 24) -bor ([int]$Data[$offset + 1] -shl 16) -bor ([int]$Data[$offset + 2] -shl 8) -bor [int]$Data[$offset + 3] $chunkType = [System.Text.Encoding]::ASCII.GetString($Data, $offset + 4, 4) $chunkDataOffset = $offset + 8 switch ($chunkType) { 'IHDR' { $width = ([int]$Data[$chunkDataOffset] -shl 24) -bor ([int]$Data[$chunkDataOffset + 1] -shl 16) -bor ([int]$Data[$chunkDataOffset + 2] -shl 8) -bor [int]$Data[$chunkDataOffset + 3] $height = ([int]$Data[$chunkDataOffset + 4] -shl 24) -bor ([int]$Data[$chunkDataOffset + 5] -shl 16) -bor ([int]$Data[$chunkDataOffset + 6] -shl 8) -bor [int]$Data[$chunkDataOffset + 7] $bitDepth = $Data[$chunkDataOffset + 8] $colorType = $Data[$chunkDataOffset + 9] $interlace = $Data[$chunkDataOffset + 12] if ($bitDepth -ne 8) { Write-Warning "Unsupported PNG bit depth: $bitDepth (only 8 supported)" return $null } if ($interlace -ne 0) { Write-Warning 'Interlaced PNGs are not supported' return $null } $bytesPerPixel = switch ($colorType) { 2 { 3 } # RGB 6 { 4 } # RGBA default { Write-Warning "Unsupported PNG color type: $colorType (only RGB=2 and RGBA=6 supported)" return $null } } Write-Verbose "PNG IHDR: ${width}x${height}, bitDepth=$bitDepth, colorType=$colorType" } 'IDAT' { for ($i = 0; $i -lt $chunkLength; $i++) { $idatData.Add($Data[$chunkDataOffset + $i]) } } 'IEND' { break } } $offset = $chunkDataOffset + $chunkLength + 4 # skip CRC } if ($idatData.Count -eq 0) { Write-Warning 'No IDAT data found in PNG' return $null } # decompress zlib data (skip 2-byte zlib header, use raw deflate) $compressedBytes = $idatData.ToArray() $memoryStream = [System.IO.MemoryStream]::new($compressedBytes, 2, $compressedBytes.Length - 2) $deflateStream = [System.IO.Compression.DeflateStream]::new( $memoryStream, [System.IO.Compression.CompressionMode]::Decompress ) $outputStream = [System.IO.MemoryStream]::new() try { $deflateStream.CopyTo($outputStream) } finally { $deflateStream.Dispose() $memoryStream.Dispose() } $rawData = $outputStream.ToArray() $outputStream.Dispose() # unfilter scanlines $stride = $width * $bytesPerPixel $rgbaData = [byte[]]::new($width * $height * 4) $previousRow = [byte[]]::new($stride) for ($row = 0; $row -lt $height; $row++) { $rowOffset = $row * ($stride + 1) # +1 for filter byte $filterType = $rawData[$rowOffset] $currentRow = [byte[]]::new($stride) for ($col = 0; $col -lt $stride; $col++) { $rawByte = $rawData[$rowOffset + 1 + $col] $a = if ($col -ge $bytesPerPixel) { $currentRow[$col - $bytesPerPixel] } else { 0 } $b = $previousRow[$col] $c = if ($col -ge $bytesPerPixel) { $previousRow[$col - $bytesPerPixel] } else { 0 } $unfiltered = switch ($filterType) { 0 { $rawByte } 1 { ($rawByte + $a) -band 0xFF } 2 { ($rawByte + $b) -band 0xFF } 3 { ($rawByte + [Math]::Floor(($a + $b) / 2)) -band 0xFF } 4 { $p = $a + $b - $c $pa = [Math]::Abs($p - $a) $pb = [Math]::Abs($p - $b) $pc = [Math]::Abs($p - $c) $predictor = if ($pa -le $pb -and $pa -le $pc) { $a } elseif ($pb -le $pc) { $b } else { $c } ($rawByte + $predictor) -band 0xFF } default { Write-Warning "Unknown PNG filter type: $filterType at row $row" $rawByte } } $currentRow[$col] = [byte]$unfiltered } # write to output RGBA array $outputOffset = $row * $width * 4 for ($px = 0; $px -lt $width; $px++) { $srcOffset = $px * $bytesPerPixel $dstOffset = $outputOffset + ($px * 4) $rgbaData[$dstOffset] = $currentRow[$srcOffset] $rgbaData[$dstOffset + 1] = $currentRow[$srcOffset + 1] $rgbaData[$dstOffset + 2] = $currentRow[$srcOffset + 2] if ($colorType -eq 6) { $rgbaData[$dstOffset + 3] = $currentRow[$srcOffset + 3] } else { $rgbaData[$dstOffset + 3] = 255 # RGB: fully opaque } } $previousRow = $currentRow } return [PSCustomObject]@{ Width = $width Height = $height RgbaData = $rgbaData } } |