mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-18 13:20:02 +08:00
144 lines
5.8 KiB
PowerShell
144 lines
5.8 KiB
PowerShell
<#
|
|
Batch-crop Godot AtlasTexture .tres files into PNGs from their referenced atlas images.
|
|
Requires a FULL RECOVERY output where atlas sources are PNG/WebP. This script does not
|
|
unpack PCK files or decode .ctex; it only reads .tres/.tpsheet metadata and crops images.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ProjectRoot,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$OutputDirectory,
|
|
|
|
[string]$SpriteDirectory = 'images/atlases/compressed.sprites/card_template',
|
|
|
|
[switch]$Overwrite
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
Add-Type -AssemblyName System.Drawing
|
|
|
|
$root = [System.IO.Path]::GetFullPath($ProjectRoot).TrimEnd('\')
|
|
$outRoot = [System.IO.Path]::GetFullPath($OutputDirectory).TrimEnd('\')
|
|
$relativeSpriteDir = $SpriteDirectory.TrimStart('\', '/').Replace('/', '\')
|
|
$sourceDir = [System.IO.Path]::GetFullPath((Join-Path $root $relativeSpriteDir))
|
|
|
|
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
|
|
throw "ProjectRoot not found: $root"
|
|
}
|
|
if (-not (Test-Path -LiteralPath $sourceDir -PathType Container)) {
|
|
throw "SpriteDirectory not found: $sourceDir"
|
|
}
|
|
if ($outRoot.Equals($root, [System.StringComparison]::OrdinalIgnoreCase) -or
|
|
$outRoot.StartsWith($root + '\', [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
throw 'OutputDirectory must be outside ProjectRoot so recovered source files stay untouched.'
|
|
}
|
|
|
|
$invariant = [System.Globalization.CultureInfo]::InvariantCulture
|
|
$files = @(Get-ChildItem -LiteralPath $sourceDir -Filter '*.tres' -File -Recurse)
|
|
$written = 0
|
|
$skipped = 0
|
|
$errors = [System.Collections.Generic.List[string]]::new()
|
|
|
|
foreach ($tres in $files) {
|
|
$sourceImage = $null
|
|
$crop = $null
|
|
try {
|
|
$text = [System.IO.File]::ReadAllText($tres.FullName)
|
|
if ($text -notmatch 'type="AtlasTexture"') {
|
|
$skipped++
|
|
continue
|
|
}
|
|
|
|
$atlasIdMatch = [regex]::Match($text, '(?m)^\s*atlas\s*=\s*ExtResource\("([^"]+)"\)')
|
|
$regionMatch = [regex]::Match($text, '(?m)^\s*region\s*=\s*Rect2i?\(\s*(-?[0-9.]+)\s*,\s*(-?[0-9.]+)\s*,\s*(-?[0-9.]+)\s*,\s*(-?[0-9.]+)\s*\)')
|
|
if (-not $atlasIdMatch.Success -or -not $regionMatch.Success) {
|
|
$skipped++
|
|
$errors.Add("Missing atlas/region fields: $($tres.FullName)")
|
|
continue
|
|
}
|
|
|
|
$atlasPath = $null
|
|
foreach ($blockMatch in [regex]::Matches($text, '(?m)^\s*\[ext_resource[^\]]*\]')) {
|
|
$block = $blockMatch.Value
|
|
$idMatch = [regex]::Match($block, '\bid="([^"]+)"')
|
|
$pathMatch = [regex]::Match($block, '\bpath="([^"]+)"')
|
|
if ($idMatch.Success -and $pathMatch.Success -and
|
|
$idMatch.Groups[1].Value -eq $atlasIdMatch.Groups[1].Value) {
|
|
$atlasPath = $pathMatch.Groups[1].Value
|
|
break
|
|
}
|
|
}
|
|
if (-not $atlasPath) {
|
|
$skipped++
|
|
$errors.Add("Could not resolve atlas ExtResource: $($tres.FullName)")
|
|
continue
|
|
}
|
|
|
|
if ($atlasPath.StartsWith('res://', [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
$atlasRelative = $atlasPath.Substring(6).Replace('/', '\')
|
|
$atlasFile = Join-Path $root $atlasRelative
|
|
} else {
|
|
$atlasFile = Join-Path $tres.DirectoryName $atlasPath.Replace('/', '\')
|
|
}
|
|
if (-not (Test-Path -LiteralPath $atlasFile -PathType Leaf)) {
|
|
$skipped++
|
|
$errors.Add("Atlas image missing (Full Recovery should restore it as PNG): $atlasFile ; referenced by $($tres.FullName)")
|
|
continue
|
|
}
|
|
|
|
$values = @(
|
|
[double]::Parse($regionMatch.Groups[1].Value, $invariant),
|
|
[double]::Parse($regionMatch.Groups[2].Value, $invariant),
|
|
[double]::Parse($regionMatch.Groups[3].Value, $invariant),
|
|
[double]::Parse($regionMatch.Groups[4].Value, $invariant)
|
|
)
|
|
$rounded = @($values | ForEach-Object { [int][Math]::Round($_) })
|
|
for ($i = 0; $i -lt 4; $i++) {
|
|
if ([Math]::Abs($values[$i] - $rounded[$i]) -gt 0.001) {
|
|
throw "Non-integer pixel region in $($tres.FullName)"
|
|
}
|
|
}
|
|
$x = $rounded[0]; $y = $rounded[1]; $width = $rounded[2]; $height = $rounded[3]
|
|
if ($x -lt 0 -or $y -lt 0 -or $width -le 0 -or $height -le 0) {
|
|
throw "Invalid crop region Rect2($x, $y, $width, $height) in $($tres.FullName)"
|
|
}
|
|
|
|
$sourceImage = [System.Drawing.Bitmap]::FromFile($atlasFile)
|
|
if (($x + $width) -gt $sourceImage.Width -or ($y + $height) -gt $sourceImage.Height) {
|
|
throw "Crop exceeds atlas bounds ($($sourceImage.Width)x$($sourceImage.Height)): $($tres.FullName)"
|
|
}
|
|
$rect = [System.Drawing.Rectangle]::new($x, $y, $width, $height)
|
|
$crop = $sourceImage.Clone($rect, $sourceImage.PixelFormat)
|
|
|
|
$relativeTres = $tres.FullName.Substring($root.Length).TrimStart('\')
|
|
$relativeOutput = [System.IO.Path]::ChangeExtension($relativeTres, '.png')
|
|
$target = Join-Path $outRoot $relativeOutput
|
|
$targetDir = Split-Path -Parent $target
|
|
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
|
if ((Test-Path -LiteralPath $target) -and -not $Overwrite) {
|
|
$skipped++
|
|
continue
|
|
}
|
|
if (Test-Path -LiteralPath $target) {
|
|
Remove-Item -LiteralPath $target -Force
|
|
}
|
|
$crop.Save($target, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
$written++
|
|
}
|
|
catch {
|
|
$skipped++
|
|
$errors.Add("$($tres.FullName): $($_.Exception.Message)")
|
|
}
|
|
finally {
|
|
if ($null -ne $crop) { $crop.Dispose() }
|
|
if ($null -ne $sourceImage) { $sourceImage.Dispose() }
|
|
}
|
|
}
|
|
|
|
Write-Output "TRES scanned: $($files.Count)"
|
|
Write-Output "PNG written: $written"
|
|
Write-Output "Skipped: $skipped"
|
|
foreach ($message in $errors) { Write-Warning $message }
|