Files
2026-08-11 15:00:52 +05:00

233 lines
9.1 KiB
PowerShell

<#
.SYNOPSIS
Draws the application icons: the logos for MSIX and the .ico for the exe.
.DESCRIPTION
The icons are drawn by code rather than kept as pictures so that the whole
set of sizes is rebuilt by a single command whenever another drawing is
wanted.
The current drawing is a placeholder: a dark rounded square with a white
"Aя". Before publishing to the Store it is worth replacing with a real one:
rewriting New-Logo is enough, the rest of the script does not depend on the
drawing.
.EXAMPLE
pwsh -File Packaging\New-Assets.ps1
#>
[CmdletBinding()]
param(
# Where to put the MSIX logos
[string] $AssetsPath,
# Where to put the .ico for the exe
[string] $IconPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# The defaults are worked out here rather than in the parameter declarations:
# in Windows PowerShell $PSScriptRoot is still empty inside param()
$root = $PSScriptRoot
if (-not $AssetsPath) { $AssetsPath = Join-Path $root 'Assets' }
if (-not $IconPath) { $IconPath = Join-Path $root '..\CursorLang\Resources\CursorLang.ico' }
Add-Type -AssemblyName System.Drawing
$Background = [System.Drawing.Color]::FromArgb(255, 0x20, 0x20, 0x20)
$Foreground = [System.Drawing.Color]::FromArgb(255, 0xFF, 0xFF, 0xFF)
$Glyph = 'Aя'
function New-Logo {
param(
[int] $Width,
[int] $Height,
# The plate under the glyph. The Windows taskbar draws its shortcut
# without one, so for those sizes only the white glyph remains
[bool] $WithPlate = $true
)
$bitmap = New-Object System.Drawing.Bitmap($Width, $Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
try {
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
$graphics.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAliasGridFit
$graphics.Clear([System.Drawing.Color]::Transparent)
$side = [Math]::Min($Width, $Height)
if ($WithPlate) {
# The corner radius is taken as a share of the side so that small
# sizes do not turn into a circle and large ones do not look like a
# plain square
$radius = [Math]::Max(2, [int]($side * 0.18))
$plate = New-Object System.Drawing.Drawing2D.GraphicsPath
$plate.AddArc(0, 0, $radius * 2, $radius * 2, 180, 90)
$plate.AddArc($Width - $radius * 2 - 1, 0, $radius * 2, $radius * 2, 270, 90)
$plate.AddArc($Width - $radius * 2 - 1, $Height - $radius * 2 - 1, $radius * 2, $radius * 2, 0, 90)
$plate.AddArc(0, $Height - $radius * 2 - 1, $radius * 2, $radius * 2, 90, 90)
$plate.CloseFigure()
$brush = New-Object System.Drawing.SolidBrush($Background)
try { $graphics.FillPath($brush, $plate) } finally { $brush.Dispose(); $plate.Dispose() }
}
# Typographic metrics leave out the side bearings MeasureString reports by
# default. Measured with those bearings the glyph looks wider than it is,
# the fitted type comes out too small, and at 16 px the strokes wash out
[System.Drawing.StringFormat] $format = [System.Drawing.StringFormat]::GenericTypographic.Clone()
try {
# The type size is found by trying: with different tile sizes the same
# share of the side gives text that is either cramped or too small
$target = $side * 0.80
$ceiling = $side * 0.86
$fontSize = $side * 0.9
$font = $null
$measured = $null
for ($attempt = 0; $attempt -lt 20; $attempt++) {
if ($font) { $font.Dispose() }
$font = New-Object System.Drawing.Font('Segoe UI', $fontSize, [System.Drawing.FontStyle]::Bold, [System.Drawing.GraphicsUnit]::Pixel)
$measured = $graphics.MeasureString($Glyph, $font, ([System.Drawing.PointF]::new(0, 0)), $format)
if ($measured.Width -le $target -and $measured.Height -le $ceiling) { break }
# Both dimensions are pulled in at once, otherwise a tall glyph
# keeps overflowing while the width already fits
$scale = [Math]::Min($target / $measured.Width, $ceiling / $measured.Height)
$fontSize = $fontSize * $scale * 0.98
}
try {
# AntiAliasGridFit snaps the stems onto the pixel grid, so the
# glyph is placed at a whole pixel: a fractional origin spreads
# every stem across two pixels, which is what blurred the small
# sizes when the text was centred inside a RectangleF
$x = [Math]::Round(($Width - $measured.Width) / 2.0)
$y = [Math]::Round(($Height - $measured.Height) / 2.0)
$brush = New-Object System.Drawing.SolidBrush($Foreground)
$origin = New-Object System.Drawing.PointF($x, $y)
try { $graphics.DrawString($Glyph, $font, $brush, $origin, $format) }
finally { $brush.Dispose() }
}
finally {
$font.Dispose()
}
}
finally {
$format.Dispose()
}
}
finally {
$graphics.Dispose()
}
return $bitmap
}
function Save-Logo {
param(
[string] $Path,
[int] $Width,
[int] $Height,
[bool] $WithPlate = $true
)
$bitmap = New-Logo -Width $Width -Height $Height -WithPlate $WithPlate
try { $bitmap.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png) } finally { $bitmap.Dispose() }
Write-Host " $([System.IO.Path]::GetFileName($Path))"
}
function Save-Icon {
param(
[string] $Path,
[int[]] $Sizes
)
# The .ico is put together by hand: System.Drawing can only save a single
# size to that format, and the icon of the exe is needed in several at once
$images = @()
foreach ($size in $Sizes) {
$bitmap = New-Logo -Width $size -Height $size
try {
$stream = New-Object System.IO.MemoryStream
$bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
$images += , @{ Size = $size; Bytes = $stream.ToArray() }
$stream.Dispose()
}
finally {
$bitmap.Dispose()
}
}
$output = New-Object System.IO.MemoryStream
$writer = New-Object System.IO.BinaryWriter($output)
try {
$writer.Write([uint16]0) # reserved
$writer.Write([uint16]1) # type: icon
$writer.Write([uint16]$images.Count)
# The directory of entries comes before the pictures themselves, so the
# offsets are worked out beforehand: the header plus 16 bytes per entry
$offset = 6 + 16 * $images.Count
foreach ($image in $images) {
# 256 does not fit in a byte and is written as zero — the format
# means it that way
$dimension = if ($image.Size -ge 256) { 0 } else { $image.Size }
$writer.Write([byte]$dimension) # width
$writer.Write([byte]$dimension) # height
$writer.Write([byte]0) # palette colours: no palette
$writer.Write([byte]0) # reserved
$writer.Write([uint16]1) # planes
$writer.Write([uint16]32) # bits per pixel
$writer.Write([uint32]$image.Bytes.Length)
$writer.Write([uint32]$offset)
$offset += $image.Bytes.Length
}
foreach ($image in $images) {
$writer.Write($image.Bytes)
}
$writer.Flush()
[System.IO.File]::WriteAllBytes($Path, $output.ToArray())
}
finally {
$writer.Dispose()
$output.Dispose()
}
Write-Host " $([System.IO.Path]::GetFileName($Path))"
}
New-Item -ItemType Directory -Path $AssetsPath -Force | Out-Null
New-Item -ItemType Directory -Path (Split-Path -Parent $IconPath) -Force | Out-Null
Write-Host 'MSIX logos:'
Save-Logo -Path (Join-Path $AssetsPath 'StoreLogo.png') -Width 50 -Height 50
Save-Logo -Path (Join-Path $AssetsPath 'Square44x44Logo.png') -Width 44 -Height 44
Save-Logo -Path (Join-Path $AssetsPath 'Square71x71Logo.png') -Width 71 -Height 71
Save-Logo -Path (Join-Path $AssetsPath 'Square150x150Logo.png') -Width 150 -Height 150
Save-Logo -Path (Join-Path $AssetsPath 'Square310x310Logo.png') -Width 310 -Height 310
Save-Logo -Path (Join-Path $AssetsPath 'Wide310x150Logo.png') -Width 310 -Height 150
# The taskbar and the application list take the shortcut without a plate
Save-Logo -Path (Join-Path $AssetsPath 'Square44x44Logo.targetsize-24_altform-unplated.png') -Width 24 -Height 24 -WithPlate $false
Save-Logo -Path (Join-Path $AssetsPath 'Square44x44Logo.targetsize-32_altform-unplated.png') -Width 32 -Height 32 -WithPlate $false
Save-Logo -Path (Join-Path $AssetsPath 'Square44x44Logo.targetsize-48_altform-unplated.png') -Width 48 -Height 48 -WithPlate $false
Write-Host 'The exe icon:'
Save-Icon -Path ([System.IO.Path]::GetFullPath($IconPath)) -Sizes @(16, 24, 32, 48, 64, 128, 256)