|
|
@@ -0,0 +1,171 @@
|
|
|
+# 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 = @'
|
|
|
+<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 512 512">
|
|
|
+ <g transform="translate(54.4,0) skewX(-12)">
|
|
|
+ <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>
|
|
|
+ </g>
|
|
|
+</svg>
|
|
|
+'@
|
|
|
+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'
|