347 lines
13 KiB
PowerShell
347 lines
13 KiB
PowerShell
[CmdletBinding(DefaultParameterSetName = 'Convert')]
|
|||
|
|
param(
|
||
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Convert')]
|
||
|
|
[string]$InputRaw,
|
||
|
|
|
||
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Convert')]
|
||
|
|
[string]$CameraJson,
|
||
|
|
|
||
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Convert')]
|
||
|
|
[ValidateSet('37', '43', '44', '50')]
|
||
|
|
[int]$VkFormat,
|
||
|
|
|
||
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Convert')]
|
||
|
|
[string]$OutputPng,
|
||
|
|
|
||
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'SelfTest')]
|
||
|
|
[switch]$SelfTest
|
||
|
|
)
|
||
|
|
|
||
|
|
$ErrorActionPreference = 'Stop'
|
||
|
|
Add-Type -AssemblyName System.Drawing
|
||
|
|
|
||
|
|
function Assert-ReadbackByteLength {
|
||
|
|
param(
|
||
|
|
[long]$ActualLength,
|
||
|
|
[int]$Width,
|
||
|
|
[int]$Height
|
||
|
|
)
|
||
|
|
|
||
|
|
$maximumLength = [long]64 * 1024 * 1024
|
||
|
|
if ($Width -le 0 -or $Height -le 0) {
|
||
|
|
throw [System.IO.InvalidDataException]::new('Readback dimensions must be positive.')
|
||
|
|
}
|
||
|
|
$expectedLength = [long]$Width * [long]$Height * 4
|
||
|
|
if ($expectedLength -gt $maximumLength) {
|
||
|
|
throw [System.IO.InvalidDataException]::new("Readback exceeds the 64 MiB limit: $expectedLength bytes.")
|
||
|
|
}
|
||
|
|
if ($ActualLength -ne $expectedLength) {
|
||
|
|
throw [System.IO.InvalidDataException]::new(
|
||
|
|
"Readback must contain exactly width * height * 4 bytes; got $ActualLength for ${Width}x${Height} (expected $expectedLength)."
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function ConvertTo-ViewportCoordinate {
|
||
|
|
param(
|
||
|
|
[object]$Value,
|
||
|
|
[string]$Name
|
||
|
|
)
|
||
|
|
|
||
|
|
try {
|
||
|
|
$number = [System.Convert]::ToDecimal($Value, [System.Globalization.CultureInfo]::InvariantCulture)
|
||
|
|
}
|
||
|
|
catch {
|
||
|
|
throw "Camera JSON viewport $Name must be an integer."
|
||
|
|
}
|
||
|
|
if ($number -ne [decimal]::Truncate($number) -or
|
||
|
|
$number -lt [int]::MinValue -or $number -gt [int]::MaxValue) {
|
||
|
|
throw "Camera JSON viewport $Name must be an in-range 32-bit integer."
|
||
|
|
}
|
||
|
|
return [int]$number
|
||
|
|
}
|
||
|
|
|
||
|
|
function Convert-ReadbackToBgra {
|
||
|
|
param(
|
||
|
|
[byte[]]$Source,
|
||
|
|
[int]$Format,
|
||
|
|
[int]$Width,
|
||
|
|
[int]$Height
|
||
|
|
)
|
||
|
|
|
||
|
|
Assert-ReadbackByteLength -ActualLength $Source.Length -Width $Width -Height $Height
|
||
|
|
|
||
|
|
# VK_FORMAT_R8G8B8A8_* (37, 43) needs R/B swapped for System.Drawing's
|
||
|
|
# little-endian Format32bppArgb storage. VK_FORMAT_B8G8R8A8_* (44, 50)
|
||
|
|
# already has the byte order expected by that bitmap format.
|
||
|
|
if ($Format -eq 44 -or $Format -eq 50) {
|
||
|
|
return ,$Source
|
||
|
|
}
|
||
|
|
|
||
|
|
$converted = New-Object byte[] $Source.Length
|
||
|
|
for ($offset = 0; $offset -lt $Source.Length; $offset += 4) {
|
||
|
|
$converted[$offset] = $Source[$offset + 2]
|
||
|
|
$converted[$offset + 1] = $Source[$offset + 1]
|
||
|
|
$converted[$offset + 2] = $Source[$offset]
|
||
|
|
$converted[$offset + 3] = $Source[$offset + 3]
|
||
|
|
}
|
||
|
|
|
||
|
|
return ,$converted
|
||
|
|
}
|
||
|
|
|
||
|
|
function Assert-ViewportWithinCapture {
|
||
|
|
param(
|
||
|
|
[int]$Left,
|
||
|
|
[int]$Top,
|
||
|
|
[int]$Right,
|
||
|
|
[int]$Bottom,
|
||
|
|
[int]$CaptureWidth,
|
||
|
|
[int]$CaptureHeight
|
||
|
|
)
|
||
|
|
|
||
|
|
if ($CaptureWidth -le 0 -or $CaptureHeight -le 0) {
|
||
|
|
throw [System.IO.InvalidDataException]::new('Camera JSON pixel_capture dimensions must be positive.')
|
||
|
|
}
|
||
|
|
if ($Left -lt 0 -or $Top -lt 0 -or $Right -gt $CaptureWidth -or $Bottom -gt $CaptureHeight) {
|
||
|
|
throw [System.IO.InvalidDataException]::new('Camera JSON viewport must fit within the full pixel_capture image bounds.')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Save-BgraPngToStream {
|
||
|
|
param(
|
||
|
|
[byte[]]$Pixels,
|
||
|
|
[int]$Width,
|
||
|
|
[int]$Height,
|
||
|
|
[System.IO.Stream]$Stream
|
||
|
|
)
|
||
|
|
|
||
|
|
$bitmap = New-Object System.Drawing.Bitmap(
|
||
|
|
$Width,
|
||
|
|
$Height,
|
||
|
|
[System.Drawing.Imaging.PixelFormat]::Format32bppArgb
|
||
|
|
)
|
||
|
|
$rectangle = New-Object System.Drawing.Rectangle(0, 0, $Width, $Height)
|
||
|
|
$bitmapData = $null
|
||
|
|
try {
|
||
|
|
$bitmapData = $bitmap.LockBits(
|
||
|
|
$rectangle,
|
||
|
|
[System.Drawing.Imaging.ImageLockMode]::WriteOnly,
|
||
|
|
[System.Drawing.Imaging.PixelFormat]::Format32bppArgb
|
||
|
|
)
|
||
|
|
|
||
|
|
$rowBytes = $Width * 4
|
||
|
|
if ([Math]::Abs($bitmapData.Stride) -lt $rowBytes) {
|
||
|
|
throw 'Bitmap stride is shorter than a pixel row.'
|
||
|
|
}
|
||
|
|
|
||
|
|
for ($row = 0; $row -lt $Height; $row++) {
|
||
|
|
$destination = [IntPtr]::Add($bitmapData.Scan0, $row * $bitmapData.Stride)
|
||
|
|
[System.Runtime.InteropServices.Marshal]::Copy(
|
||
|
|
$Pixels,
|
||
|
|
$row * $rowBytes,
|
||
|
|
$destination,
|
||
|
|
$rowBytes
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
$bitmap.UnlockBits($bitmapData)
|
||
|
|
$bitmapData = $null
|
||
|
|
$bitmap.Save($Stream, [System.Drawing.Imaging.ImageFormat]::Png)
|
||
|
|
}
|
||
|
|
finally {
|
||
|
|
if ($null -ne $bitmapData) {
|
||
|
|
$bitmap.UnlockBits($bitmapData)
|
||
|
|
}
|
||
|
|
$bitmap.Dispose()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-CameraExtentFromObject {
|
||
|
|
param([object]$Camera)
|
||
|
|
|
||
|
|
$camera = $Camera
|
||
|
|
if ($camera.schema -ne 'fparkan-legacy-camera-v1' -or $null -eq $camera.viewport -or $camera.viewport.Count -ne 4) {
|
||
|
|
throw 'Camera JSON must be fparkan-legacy-camera-v1 and contain a four-value viewport.'
|
||
|
|
}
|
||
|
|
|
||
|
|
$left = ConvertTo-ViewportCoordinate -Value $camera.viewport[0] -Name 'left'
|
||
|
|
$top = ConvertTo-ViewportCoordinate -Value $camera.viewport[1] -Name 'top'
|
||
|
|
$right = ConvertTo-ViewportCoordinate -Value $camera.viewport[2] -Name 'right'
|
||
|
|
$bottom = ConvertTo-ViewportCoordinate -Value $camera.viewport[3] -Name 'bottom'
|
||
|
|
$widthLong = [long]$right - [long]$left
|
||
|
|
$heightLong = [long]$bottom - [long]$top
|
||
|
|
if ($widthLong -gt [int]::MaxValue -or $heightLong -gt [int]::MaxValue) {
|
||
|
|
throw 'Camera JSON viewport extent exceeds 32-bit dimensions.'
|
||
|
|
}
|
||
|
|
$width = [int]$widthLong
|
||
|
|
$height = [int]$heightLong
|
||
|
|
if ($width -le 0 -or $height -le 0) {
|
||
|
|
throw 'Camera JSON viewport has an empty or inverted extent.'
|
||
|
|
}
|
||
|
|
|
||
|
|
$expectedBytes = [long]$width * [long]$height * 4
|
||
|
|
if ($expectedBytes -gt ([long]64 * 1024 * 1024)) {
|
||
|
|
throw "Camera JSON viewport exceeds the 64 MiB readback limit: $expectedBytes bytes."
|
||
|
|
}
|
||
|
|
if ($null -ne $camera.pixel_capture) {
|
||
|
|
$captureWidth = ConvertTo-ViewportCoordinate -Value $camera.pixel_capture.width -Name 'pixel_capture.width'
|
||
|
|
$captureHeight = ConvertTo-ViewportCoordinate -Value $camera.pixel_capture.height -Name 'pixel_capture.height'
|
||
|
|
Assert-ViewportWithinCapture -Left $left -Top $top -Right $right -Bottom $bottom -CaptureWidth $captureWidth -CaptureHeight $captureHeight
|
||
|
|
}
|
||
|
|
|
||
|
|
return [pscustomobject]@{ Width = $width; Height = $height }
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-CameraExtent {
|
||
|
|
param([string]$Path)
|
||
|
|
|
||
|
|
$camera = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||
|
|
return Get-CameraExtentFromObject -Camera $camera
|
||
|
|
}
|
||
|
|
|
||
|
|
function Invoke-SelfTest {
|
||
|
|
$rgba = [byte[]]@(
|
||
|
|
255, 0, 0, 255, 0, 255, 0, 255,
|
||
|
|
0, 0, 255, 255, 255, 255, 255, 255
|
||
|
|
)
|
||
|
|
$expectedBgra = [byte[]]@(
|
||
|
|
0, 0, 255, 255, 0, 255, 0, 255,
|
||
|
|
255, 0, 0, 255, 255, 255, 255, 255
|
||
|
|
)
|
||
|
|
|
||
|
|
$rgbaResult = Convert-ReadbackToBgra -Source $rgba -Format 37 -Width 2 -Height 2
|
||
|
|
$bgraResult = Convert-ReadbackToBgra -Source $expectedBgra -Format 44 -Width 2 -Height 2
|
||
|
|
for ($index = 0; $index -lt $expectedBgra.Length; $index++) {
|
||
|
|
if ($rgbaResult[$index] -ne $expectedBgra[$index] -or $bgraResult[$index] -ne $expectedBgra[$index]) {
|
||
|
|
throw 'Self-check failed: RGBA/BGRA channel mapping.'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$mismatchRejected = $false
|
||
|
|
try {
|
||
|
|
Assert-ReadbackByteLength -ActualLength 15 -Width 2 -Height 2
|
||
|
|
}
|
||
|
|
catch [System.IO.InvalidDataException] {
|
||
|
|
$mismatchRejected = $true
|
||
|
|
}
|
||
|
|
if (-not $mismatchRejected) {
|
||
|
|
throw 'Self-check failed: wrong byte length was accepted.'
|
||
|
|
}
|
||
|
|
|
||
|
|
Assert-ReadbackByteLength -ActualLength ([long]64 * 1024 * 1024) -Width 4096 -Height 4096
|
||
|
|
$oversizeRejected = $false
|
||
|
|
try {
|
||
|
|
Assert-ReadbackByteLength -ActualLength ([long]64 * 1024 * 1024) -Width 4097 -Height 4096
|
||
|
|
}
|
||
|
|
catch [System.IO.InvalidDataException] {
|
||
|
|
$oversizeRejected = $true
|
||
|
|
}
|
||
|
|
if (-not $oversizeRejected) {
|
||
|
|
throw 'Self-check failed: readback over the 64 MiB boundary was accepted.'
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($invalidCoordinate in @([double]1.5, [long]2147483648)) {
|
||
|
|
$coordinateRejected = $false
|
||
|
|
try {
|
||
|
|
ConvertTo-ViewportCoordinate -Value $invalidCoordinate -Name 'self-check'
|
||
|
|
}
|
||
|
|
catch {
|
||
|
|
$coordinateRejected = $true
|
||
|
|
}
|
||
|
|
if (-not $coordinateRejected) {
|
||
|
|
throw 'Self-check failed: fractional or out-of-range viewport coordinate was accepted.'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# A crop can be smaller than the full native PNG and start away from (0,0).
|
||
|
|
# Raw viewer readback is the crop only, so its dimensions stay 4x4 here.
|
||
|
|
$cropCamera = [pscustomobject]@{
|
||
|
|
schema = 'fparkan-legacy-camera-v1'
|
||
|
|
viewport = @(4, 6, 8, 10)
|
||
|
|
pixel_capture = [pscustomobject]@{ width = 12; height = 16 }
|
||
|
|
}
|
||
|
|
$cropExtent = Get-CameraExtentFromObject -Camera $cropCamera
|
||
|
|
if ($cropExtent.Width -ne 4 -or $cropExtent.Height -ne 4) {
|
||
|
|
throw 'Self-check failed: cropped readback extent was not derived from the viewport rectangle.'
|
||
|
|
}
|
||
|
|
Assert-ReadbackByteLength -ActualLength 64 -Width 4 -Height 4
|
||
|
|
$cropOutsideCaptureRejected = $false
|
||
|
|
try {
|
||
|
|
$cropCamera.viewport = @(4, 6, 13, 10)
|
||
|
|
Get-CameraExtentFromObject -Camera $cropCamera | Out-Null
|
||
|
|
}
|
||
|
|
catch [System.IO.InvalidDataException] {
|
||
|
|
$cropOutsideCaptureRejected = $true
|
||
|
|
}
|
||
|
|
if (-not $cropOutsideCaptureRejected) {
|
||
|
|
throw 'Self-check failed: viewport extending beyond the full pixel_capture image was accepted.'
|
||
|
|
}
|
||
|
|
|
||
|
|
$stream = New-Object System.IO.MemoryStream
|
||
|
|
$decoded = $null
|
||
|
|
try {
|
||
|
|
Save-BgraPngToStream -Pixels $expectedBgra -Width 2 -Height 2 -Stream $stream
|
||
|
|
$stream.Position = 0
|
||
|
|
$decoded = [System.Drawing.Bitmap]::new($stream)
|
||
|
|
$expectedColors = @(
|
||
|
|
[System.Drawing.Color]::Red,
|
||
|
|
[System.Drawing.Color]::FromArgb(255, 0, 255, 0),
|
||
|
|
[System.Drawing.Color]::Blue,
|
||
|
|
[System.Drawing.Color]::White
|
||
|
|
)
|
||
|
|
$points = @(@(0, 0), @(1, 0), @(0, 1), @(1, 1))
|
||
|
|
for ($index = 0; $index -lt $points.Count; $index++) {
|
||
|
|
$point = $points[$index]
|
||
|
|
$actualColor = $decoded.GetPixel($point[0], $point[1])
|
||
|
|
if ($actualColor.ToArgb() -ne $expectedColors[$index].ToArgb()) {
|
||
|
|
throw "Self-check failed: PNG pixel $index expected $($expectedColors[$index]) but read $actualColor."
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
finally {
|
||
|
|
if ($null -ne $decoded) { $decoded.Dispose() }
|
||
|
|
$stream.Dispose()
|
||
|
|
}
|
||
|
|
|
||
|
|
'Self-check passed (2x2 RGBA/BGRA PNG, byte-length/64 MiB limits, viewport-coordinate bounds, and cropped pixel_capture bounds).'
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($SelfTest) {
|
||
|
|
Invoke-SelfTest
|
||
|
|
exit 0
|
||
|
|
}
|
||
|
|
|
||
|
|
$repositoryRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
||
|
|
$targetRoot = [System.IO.Path]::GetFullPath((Join-Path $repositoryRoot 'target'))
|
||
|
|
$rawPath = (Resolve-Path -LiteralPath $InputRaw).Path
|
||
|
|
$cameraPath = (Resolve-Path -LiteralPath $CameraJson).Path
|
||
|
|
|
||
|
|
if (-not (Test-Path -LiteralPath $rawPath -PathType Leaf)) { throw "Raw readback is not a file: $rawPath" }
|
||
|
|
if (-not (Test-Path -LiteralPath $cameraPath -PathType Leaf)) { throw "Camera JSON is not a file: $cameraPath" }
|
||
|
|
|
||
|
|
$outputPath = [System.IO.Path]::GetFullPath($OutputPng)
|
||
|
|
$targetPrefix = $targetRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
|
||
|
|
if (-not $outputPath.StartsWith($targetPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||
|
|
throw "Output must be beneath the repository target directory: $targetRoot"
|
||
|
|
}
|
||
|
|
if ([System.IO.Path]::GetExtension($outputPath) -ine '.png') { throw 'Output filename must end in .png.' }
|
||
|
|
if (Test-Path -LiteralPath $outputPath) { throw "Refusing to overwrite existing output: $outputPath" }
|
||
|
|
$outputDirectory = [System.IO.Path]::GetDirectoryName($outputPath)
|
||
|
|
if (-not (Test-Path -LiteralPath $outputDirectory -PathType Container)) {
|
||
|
|
throw "Output directory does not exist: $outputDirectory"
|
||
|
|
}
|
||
|
|
|
||
|
|
$extent = Get-CameraExtent -Path $cameraPath
|
||
|
|
$rawInfo = Get-Item -LiteralPath $rawPath
|
||
|
|
Assert-ReadbackByteLength -ActualLength $rawInfo.Length -Width $extent.Width -Height $extent.Height
|
||
|
|
$source = [System.IO.File]::ReadAllBytes($rawPath)
|
||
|
|
$pixels = Convert-ReadbackToBgra -Source $source -Format $VkFormat -Width $extent.Width -Height $extent.Height
|
||
|
|
$stream = [System.IO.File]::Open($outputPath, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None)
|
||
|
|
try {
|
||
|
|
Save-BgraPngToStream -Pixels $pixels -Width $extent.Width -Height $extent.Height -Stream $stream
|
||
|
|
}
|
||
|
|
finally {
|
||
|
|
$stream.Dispose()
|
||
|
|
}
|
||
|
|
"Wrote $($extent.Width)x$($extent.Height) PNG to $outputPath (VkFormat $VkFormat)."
|