# Generate Android launcher icons & splash from resources/icon.svg # Requires: Edge (headless SVG rendering) + .NET System.Drawing (scale/clip) # Usage: powershell -ExecutionPolicy Bypass -File generate-android-icons.ps1 # Re-run this script after modifying icon.svg to refresh all Android assets. $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Drawing $root = Split-Path -Parent $MyInvocation.MyCommand.Path # admin-h5/resources $android = Join-Path $root '..\android\app\src\main\res' $edge = 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe' $tmp = Join-Path $env:TEMP 'yeswash_icons' New-Item -ItemType Directory -Force $tmp | Out-Null # Text-only master (same layout as icon.svg, no background) # Rendered as BLACK text on TRANSPARENT background; converted to white-on-transparent below $textSvg = @' Yeswash '@ Set-Content -Path (Join-Path $tmp 'text.svg') -Value $textSvg -Encoding ASCII Copy-Item (Join-Path $root 'icon.svg') (Join-Path $tmp 'icon.svg') -Force function Wait-File($path, $sec = 20) { $deadline = (Get-Date).AddSeconds($sec) while ((Get-Date) -lt $deadline) { if ((Test-Path $path) -and ((Get-Item $path).Length -gt 0)) { return $true } Start-Sleep -Milliseconds 500 } return $false } Remove-Item "$tmp\icon.png", "$tmp\text.png" -Force -ErrorAction SilentlyContinue & $edge --headless=new --disable-gpu --hide-scrollbars --default-background-color=00000000 --user-data-dir="$tmp\edge1" --window-size=512,512 --screenshot="$tmp\icon.png" "file:///$(($tmp -replace '\\','/'))/icon.svg" 2>$null if (-not (Wait-File "$tmp\icon.png")) { throw 'icon master render failed' } & $edge --headless=new --disable-gpu --hide-scrollbars --default-background-color=00000000 --user-data-dir="$tmp\edge2" --window-size=1024,1024 --screenshot="$tmp\text.png" "file:///$(($tmp -replace '\\','/'))/text.svg" 2>$null if (-not (Wait-File "$tmp\text.png")) { throw 'text master render failed' } Start-Sleep -Milliseconds 500 $iconBmp = [System.Drawing.Bitmap]::FromFile("$tmp\icon.png") # 512 full logo on red $textBmp = [System.Drawing.Bitmap]::FromFile("$tmp\text.png") # 1024 black text on white (text width 856/1024) # Sanity check: master must contain black text pixels (guards against stale/blank renders) $probe = New-Object System.Drawing.Bitmap($textBmp, 128, 128) $hasInk = $false for ($y = 0; $y -lt 128 -and -not $hasInk; $y++) { for ($x = 0; $x -lt 128; $x++) { if ($probe.GetPixel($x, $y).R -lt 100) { $hasInk = $true; break } } } $probe.Dispose() if (-not $hasInk) { throw 'text master is blank - headless render failed' } # Convert black-on-white master to white-on-transparent (alpha = 255 - luminance) # Pixel-level conversion via inline C# (LockBits) - deterministic, no ColorMatrix quirks Add-Type -TypeDefinition @" using System; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; public static class MaskUtil { public static Bitmap WhiteMask(Bitmap src) { Bitmap outBmp = new Bitmap(src.Width, src.Height, PixelFormat.Format32bppArgb); Rectangle rect = new Rectangle(0, 0, src.Width, src.Height); BitmapData data = src.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); BitmapData odata = outBmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); int len = data.Stride * src.Height; byte[] buf = new byte[len]; Marshal.Copy(data.Scan0, buf, 0, len); for (int i = 0; i < len; i += 4) { // BGRA little-endian int lum = (buf[i+2]*299 + buf[i+1]*587 + buf[i]*114) / 1000; int a = buf[i+3] * (255 - lum) / 255; // source alpha * (1 - luminance) buf[i] = 255; buf[i+1] = 255; buf[i+2] = 255; buf[i+3] = (byte)a; // text -> opaque white, any background -> transparent } Marshal.Copy(buf, 0, odata.Scan0, len); src.UnlockBits(data); outBmp.UnlockBits(odata); return outBmp; } } "@ -ReferencedAssemblies System.Drawing $textMask = [MaskUtil]::WhiteMask($textBmp) $textBmp.Dispose() $textBmp = $textMask # 1024 white text on transparent (text width 856/1024) function New-Canvas([int]$w, [int]$h) { $bmp = New-Object System.Drawing.Bitmap($w, $h) $g = [System.Drawing.Graphics]::FromImage($bmp) $g.InterpolationMode = 'HighQualityBicubic' $g.SmoothingMode = 'HighQuality' $g.PixelOffsetMode = 'HighQuality' return @($bmp, $g) } function Save-Png($bmp, $path) { $dir = Split-Path -Parent $path if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force $dir | Out-Null } $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) } # 1) Square launcher icons (full logo) $launcher = @{ 'mipmap-mdpi'=48; 'mipmap-hdpi'=72; 'mipmap-xhdpi'=96; 'mipmap-xxhdpi'=144; 'mipmap-xxxhdpi'=192 } foreach ($k in $launcher.Keys) { $s = $launcher[$k] $bmp, $g = New-Canvas $s $s $g.DrawImage($iconBmp, 0, 0, $s, $s) $g.Dispose() Save-Png $bmp (Join-Path $android "$k\ic_launcher.png") $bmp.Dispose() Write-Output "ic_launcher: $k ${s}x${s}" } # 2) Round launcher icons (ellipse brush clip, anti-aliased edge) foreach ($k in $launcher.Keys) { $s = $launcher[$k] $bmp, $g = New-Canvas $s $s $g.Clear([System.Drawing.Color]::Transparent) $mid = New-Object System.Drawing.Bitmap($s, $s) $mg = [System.Drawing.Graphics]::FromImage($mid) $mg.InterpolationMode = 'HighQualityBicubic' $mg.DrawImage($iconBmp, 0, 0, $s, $s) $mg.Dispose() $brush = New-Object System.Drawing.TextureBrush($mid) $brush.WrapMode = 'Clamp' $g.SmoothingMode = 'HighQuality' $g.FillEllipse($brush, 0, 0, $s, $s) $brush.Dispose(); $mid.Dispose(); $g.Dispose() Save-Png $bmp (Join-Path $android "$k\ic_launcher_round.png") $bmp.Dispose() Write-Output "ic_launcher_round: $k ${s}x${s}" } # 3) Adaptive icon foreground (transparent, text scaled into safe zone ~60% of canvas) $fg = @{ 'mipmap-mdpi'=108; 'mipmap-hdpi'=162; 'mipmap-xhdpi'=216; 'mipmap-xxhdpi'=324; 'mipmap-xxxhdpi'=432 } foreach ($k in $fg.Keys) { $s = $fg[$k] $sf = (0.60 * $s) / 856.0 $d = [int][Math]::Round(1024 * $sf) $off = [int][Math]::Round(($s - $d) / 2.0) $bmp, $g = New-Canvas $s $s $g.DrawImage($textBmp, $off, $off, $d, $d) $g.Dispose() Save-Png $bmp (Join-Path $android "$k\ic_launcher_foreground.png") $bmp.Dispose() Write-Output "ic_launcher_foreground: $k ${s}x${s}" } # 4) Splash screens (red background + white text ~58% of short side, keep original sizes) $splashDirs = Get-ChildItem $android -Directory | Where-Object { $_.Name -eq 'drawable' -or $_.Name -like 'drawable-land-*' -or $_.Name -like 'drawable-port-*' } foreach ($dir in $splashDirs) { $f = Join-Path $dir.FullName 'splash.png' if (-not (Test-Path $f)) { continue } $probe = [System.Drawing.Image]::FromFile($f); $w = $probe.Width; $h = $probe.Height; $probe.Dispose() $bmp, $g = New-Canvas $w $h $g.Clear([System.Drawing.Color]::FromArgb(0xF3, 0x1C, 0x17)) $sf = (0.58 * [Math]::Min($w, $h)) / 856.0 $d = [int][Math]::Round(1024 * $sf) $offX = [int][Math]::Round(($w - $d) / 2.0) $offY = [int][Math]::Round(($h - $d) / 2.0) $g.DrawImage($textBmp, $offX, $offY, $d, $d) $g.Dispose() Save-Png $bmp $f $bmp.Dispose() Write-Output "splash: $($dir.Name) ${w}x${h}" } $iconBmp.Dispose(); $textBmp.Dispose() Write-Output 'DONE'