generate-android-icons.ps1 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. # Generate Android launcher icons & splash from resources/icon.svg
  2. # Requires: Edge (headless SVG rendering) + .NET System.Drawing (scale/clip)
  3. # Usage: powershell -ExecutionPolicy Bypass -File generate-android-icons.ps1
  4. # Re-run this script after modifying icon.svg to refresh all Android assets.
  5. $ErrorActionPreference = 'Stop'
  6. Add-Type -AssemblyName System.Drawing
  7. $root = Split-Path -Parent $MyInvocation.MyCommand.Path # admin-h5/resources
  8. $android = Join-Path $root '..\android\app\src\main\res'
  9. $edge = 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
  10. $tmp = Join-Path $env:TEMP 'yeswash_icons'
  11. New-Item -ItemType Directory -Force $tmp | Out-Null
  12. # Text-only master (same layout as icon.svg, no background)
  13. # Rendered as BLACK text on TRANSPARENT background; converted to white-on-transparent below
  14. $textSvg = @'
  15. <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 512 512">
  16. <g transform="translate(54.4,0) skewX(-12)">
  17. <text x="256" y="300" text-anchor="middle" font-family="'Arial Black',Arial,sans-serif" font-weight="900" font-size="123" fill="#000000" textLength="428" lengthAdjust="spacingAndGlyphs">Yeswash</text>
  18. </g>
  19. </svg>
  20. '@
  21. Set-Content -Path (Join-Path $tmp 'text.svg') -Value $textSvg -Encoding ASCII
  22. Copy-Item (Join-Path $root 'icon.svg') (Join-Path $tmp 'icon.svg') -Force
  23. function Wait-File($path, $sec = 20) {
  24. $deadline = (Get-Date).AddSeconds($sec)
  25. while ((Get-Date) -lt $deadline) {
  26. if ((Test-Path $path) -and ((Get-Item $path).Length -gt 0)) { return $true }
  27. Start-Sleep -Milliseconds 500
  28. }
  29. return $false
  30. }
  31. Remove-Item "$tmp\icon.png", "$tmp\text.png" -Force -ErrorAction SilentlyContinue
  32. & $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
  33. if (-not (Wait-File "$tmp\icon.png")) { throw 'icon master render failed' }
  34. & $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
  35. if (-not (Wait-File "$tmp\text.png")) { throw 'text master render failed' }
  36. Start-Sleep -Milliseconds 500
  37. $iconBmp = [System.Drawing.Bitmap]::FromFile("$tmp\icon.png") # 512 full logo on red
  38. $textBmp = [System.Drawing.Bitmap]::FromFile("$tmp\text.png") # 1024 black text on white (text width 856/1024)
  39. # Sanity check: master must contain black text pixels (guards against stale/blank renders)
  40. $probe = New-Object System.Drawing.Bitmap($textBmp, 128, 128)
  41. $hasInk = $false
  42. for ($y = 0; $y -lt 128 -and -not $hasInk; $y++) {
  43. for ($x = 0; $x -lt 128; $x++) {
  44. if ($probe.GetPixel($x, $y).R -lt 100) { $hasInk = $true; break }
  45. }
  46. }
  47. $probe.Dispose()
  48. if (-not $hasInk) { throw 'text master is blank - headless render failed' }
  49. # Convert black-on-white master to white-on-transparent (alpha = 255 - luminance)
  50. # Pixel-level conversion via inline C# (LockBits) - deterministic, no ColorMatrix quirks
  51. Add-Type -TypeDefinition @"
  52. using System;
  53. using System.Drawing;
  54. using System.Drawing.Imaging;
  55. using System.Runtime.InteropServices;
  56. public static class MaskUtil {
  57. public static Bitmap WhiteMask(Bitmap src) {
  58. Bitmap outBmp = new Bitmap(src.Width, src.Height, PixelFormat.Format32bppArgb);
  59. Rectangle rect = new Rectangle(0, 0, src.Width, src.Height);
  60. BitmapData data = src.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
  61. BitmapData odata = outBmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
  62. int len = data.Stride * src.Height;
  63. byte[] buf = new byte[len];
  64. Marshal.Copy(data.Scan0, buf, 0, len);
  65. for (int i = 0; i < len; i += 4) { // BGRA little-endian
  66. int lum = (buf[i+2]*299 + buf[i+1]*587 + buf[i]*114) / 1000;
  67. int a = buf[i+3] * (255 - lum) / 255; // source alpha * (1 - luminance)
  68. buf[i] = 255; buf[i+1] = 255; buf[i+2] = 255;
  69. buf[i+3] = (byte)a; // text -> opaque white, any background -> transparent
  70. }
  71. Marshal.Copy(buf, 0, odata.Scan0, len);
  72. src.UnlockBits(data);
  73. outBmp.UnlockBits(odata);
  74. return outBmp;
  75. }
  76. }
  77. "@ -ReferencedAssemblies System.Drawing
  78. $textMask = [MaskUtil]::WhiteMask($textBmp)
  79. $textBmp.Dispose()
  80. $textBmp = $textMask # 1024 white text on transparent (text width 856/1024)
  81. function New-Canvas([int]$w, [int]$h) {
  82. $bmp = New-Object System.Drawing.Bitmap($w, $h)
  83. $g = [System.Drawing.Graphics]::FromImage($bmp)
  84. $g.InterpolationMode = 'HighQualityBicubic'
  85. $g.SmoothingMode = 'HighQuality'
  86. $g.PixelOffsetMode = 'HighQuality'
  87. return @($bmp, $g)
  88. }
  89. function Save-Png($bmp, $path) {
  90. $dir = Split-Path -Parent $path
  91. if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force $dir | Out-Null }
  92. $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
  93. }
  94. # 1) Square launcher icons (full logo)
  95. $launcher = @{ 'mipmap-mdpi'=48; 'mipmap-hdpi'=72; 'mipmap-xhdpi'=96; 'mipmap-xxhdpi'=144; 'mipmap-xxxhdpi'=192 }
  96. foreach ($k in $launcher.Keys) {
  97. $s = $launcher[$k]
  98. $bmp, $g = New-Canvas $s $s
  99. $g.DrawImage($iconBmp, 0, 0, $s, $s)
  100. $g.Dispose()
  101. Save-Png $bmp (Join-Path $android "$k\ic_launcher.png")
  102. $bmp.Dispose()
  103. Write-Output "ic_launcher: $k ${s}x${s}"
  104. }
  105. # 2) Round launcher icons (ellipse brush clip, anti-aliased edge)
  106. foreach ($k in $launcher.Keys) {
  107. $s = $launcher[$k]
  108. $bmp, $g = New-Canvas $s $s
  109. $g.Clear([System.Drawing.Color]::Transparent)
  110. $mid = New-Object System.Drawing.Bitmap($s, $s)
  111. $mg = [System.Drawing.Graphics]::FromImage($mid)
  112. $mg.InterpolationMode = 'HighQualityBicubic'
  113. $mg.DrawImage($iconBmp, 0, 0, $s, $s)
  114. $mg.Dispose()
  115. $brush = New-Object System.Drawing.TextureBrush($mid)
  116. $brush.WrapMode = 'Clamp'
  117. $g.SmoothingMode = 'HighQuality'
  118. $g.FillEllipse($brush, 0, 0, $s, $s)
  119. $brush.Dispose(); $mid.Dispose(); $g.Dispose()
  120. Save-Png $bmp (Join-Path $android "$k\ic_launcher_round.png")
  121. $bmp.Dispose()
  122. Write-Output "ic_launcher_round: $k ${s}x${s}"
  123. }
  124. # 3) Adaptive icon foreground (transparent, text scaled into safe zone ~60% of canvas)
  125. $fg = @{ 'mipmap-mdpi'=108; 'mipmap-hdpi'=162; 'mipmap-xhdpi'=216; 'mipmap-xxhdpi'=324; 'mipmap-xxxhdpi'=432 }
  126. foreach ($k in $fg.Keys) {
  127. $s = $fg[$k]
  128. $sf = (0.60 * $s) / 856.0
  129. $d = [int][Math]::Round(1024 * $sf)
  130. $off = [int][Math]::Round(($s - $d) / 2.0)
  131. $bmp, $g = New-Canvas $s $s
  132. $g.DrawImage($textBmp, $off, $off, $d, $d)
  133. $g.Dispose()
  134. Save-Png $bmp (Join-Path $android "$k\ic_launcher_foreground.png")
  135. $bmp.Dispose()
  136. Write-Output "ic_launcher_foreground: $k ${s}x${s}"
  137. }
  138. # 4) Splash screens (red background + white text ~58% of short side, keep original sizes)
  139. $splashDirs = Get-ChildItem $android -Directory | Where-Object { $_.Name -eq 'drawable' -or $_.Name -like 'drawable-land-*' -or $_.Name -like 'drawable-port-*' }
  140. foreach ($dir in $splashDirs) {
  141. $f = Join-Path $dir.FullName 'splash.png'
  142. if (-not (Test-Path $f)) { continue }
  143. $probe = [System.Drawing.Image]::FromFile($f); $w = $probe.Width; $h = $probe.Height; $probe.Dispose()
  144. $bmp, $g = New-Canvas $w $h
  145. $g.Clear([System.Drawing.Color]::FromArgb(0xF3, 0x1C, 0x17))
  146. $sf = (0.58 * [Math]::Min($w, $h)) / 856.0
  147. $d = [int][Math]::Round(1024 * $sf)
  148. $offX = [int][Math]::Round(($w - $d) / 2.0)
  149. $offY = [int][Math]::Round(($h - $d) / 2.0)
  150. $g.DrawImage($textBmp, $offX, $offY, $d, $d)
  151. $g.Dispose()
  152. Save-Png $bmp $f
  153. $bmp.Dispose()
  154. Write-Output "splash: $($dir.Name) ${w}x${h}"
  155. }
  156. $iconBmp.Dispose(); $textBmp.Dispose()
  157. Write-Output 'DONE'