Skip to content

Instantly share code, notes, and snippets.

@scriptingstudio
Last active July 28, 2026 10:42
Show Gist options
  • Select an option

  • Save scriptingstudio/cf863c8a02f8d1d41b4ae3f3a72eac80 to your computer and use it in GitHub Desktop.

Select an option

Save scriptingstudio/cf863c8a02f8d1d41b4ae3f3a72eac80 to your computer and use it in GitHub Desktop.
Simple SVG to ICO converter
<#
.SYNOPSIS
SVG to Windows ICO converter.
.DESCRIPTION
The script uses Inkscape app to convert SVG to PNG and then ensembles PNGs to a single ICO using .NET.
Key features:
- Multisize ICO
- Just one Inkscape instance per multiple size conversions, i.e. a conversion time is independent on amount of sizes
- Control over SVG exported area
- Ability to replace colors in SVG
- Ability to fetch SVG from internet
- Ability to model an output file name
- There are input and output integrity checks
- Animated progress bar for Inkscape runtime
- The image quality of the output ICO is notably better than any online converter can provide
There are two editions:
- .NET (preferred) : *minimal* ICO size, external assemblies (System.Drawing)
- File (demo) : larger ICO size, no external assemblies, few lines of code less
.PARAMETER Path
Mandatory. Position is 0. Specifies a filepath to the SVG. Path can be a web-URL. SVG from internet is saved in the user's downloads.
.PARAMETER Output
Specifies a directory where the exports should be stored. If no directory is specified, ICO will be exported to the user's TEMP directory. To select the source's directory specify "*".
.PARAMETER Rename
Specifies an alternative name for the output file. If a new name starts with "+" it will join the input file base name.
.PARAMETER Sizes
Specifies a pixel size of the exported icon. The icon will be square, so if you want a 16x16 export, it would be -Sizes 16.
Standard values are 16, 20, 24, 32, 40, 48, 64, 72, 96, 128, 192, and 256. To select the standard size range specify -1. To disable this filter specify 0 or leave it behind.
.PARAMETER NoCrop
Indicates that exported area is the *page*. Otherwise exported area is the *drawing* (not page), i.e. the bounding box of all objects of the SVG, and the exported PNG image will display all the visible objects of the SVG without margins or cropping. This is an Inkscape's option.
.PARAMETER Margin
Reserved. Extension of the extraction area; currently has no effect for PNG format. The size of the margin is specified in SVG units. 1 unit equals 1 bitmap pixel, at the default export resolution of 96 dpi. This is an Inkscape's option.
.PARAMETER ReColor
Specifies an array of substitution color pairs in RGB notation. For example, "aaaaaa=555555","ff22cc=eeeeef","029c0f", where the instance without "=" would replace the rest of colors in SVG. The character "#" is added automatically.
As SVG is regular XML all colors found (in "fill" and "stroke" attributes) are replaced with the specified. A copy of the source SVG as resource for ICO is created. The source SVG stays intact.
Note, that this way of recoloring is more preferred for flat and monochrome palettes.
.PARAMETER Keep
This parameter specifies not delete png files, but then open the user's temp folder in Windows Explorer.
.PARAMETER NoProgress
Indicates to disable showing a progress bar.
.PARAMETER BarType
Specifies the type of the progress bar. Valid values are growth, fill, bounce, tide. If not specified, random is selected.
.PARAMETER BarColor
Specifies the color of the progress bar. Default is none, i.e. current. Valid values are standard console colors. Run and see standards: [ConsoleColor]::GetValues([ConsoleColor]).
.NOTES
Requires : PowerShell 5+, Inkscape
Version : 1.8.0
LastUpdate : 2026-July-28
.LINK
https://gist.github.com/scriptingstudio/cf863c8a02f8d1d41b4ae3f3a72eac80
https://inkscape.org/doc/inkscape-man.html
https://wiki.inkscape.org/wiki/Action
#>
# File edition
# This version inspired by https://gist.github.com/takuyatsuchida/989423177c7991f20ea6191e6ababaff
function Convert-SvgToIco {
[CmdletBinding()]
[Alias('ConvertTo-Ico','s2i')]
param (
# file options
[Parameter(Position=0,Mandatory)]
[Alias('FilePath','LiteralPath','Fullname')]
[string] $Path,
[string] $Output, # output folder
[alias('alter')][string] $Rename, # alternative name for output file
# image options
[int[]] $Sizes = (16,20,24,32,40,48,64,72,96,128,192,256),
[Alias('notrim','noclip','nc')][switch] $NoCrop, # extraction area
[int] $Margin, # reserved; extraction area, currently has no effect for PNG format
[string[]] $ReColor, # replace colors in SVG
[switch] $Keep, # don't delete png files and open temp folder in explorer
# runtime options
[switch] $Quiet, # experimental; mute diagnostic (and error?) messages
[alias('np','nostatus')][switch] $NoProgress,
[alias('type')][string] $BarType,
[alias('color')][ConsoleColor] $BarColor
)
function Measure-RunTime ([scriptblock]$command, [timespan]$timespan) {
$diff = if ($timespan) {$timespan}
elseif ($command) {Measure-Command -Expression $command -ErrorAction 0}
else {return}
$ms = $diff.TotalMilliseconds - [Math]::Truncate($diff.TotalMilliseconds) + $diff.Milliseconds
if ($diff.Minutes) {'{0}m:{1}s:{2:N2}ms' -f $diff.Minutes,$diff.Seconds,$ms}
elseif ($diff.Seconds) {'{0}s:{1:N2}ms' -f $diff.Seconds,$ms}
else {'{0:N2}ms' -f $diff.TotalMilliseconds}
} # END Measure-RunTime
function Show-Progress {
param (
[scriptblock] $scriptblock,
[int] $width,
[consolecolor] $color,
[string] $type, # growth | fill | bounce | tide
[switch] $passthru
)
if (-not $scriptblock) {return}
if ($width -lt 1) {$width = 10}
if ([Console]::CursorLeft + $width -ge [Console]::WindowWidth) {
return Measure-Command -Expression $scriptblock -ErrorAction 0
}
if ($type -notmatch 'growth|fill|bounce|tide') {$type = 'growth','fill','bounce','tide' | Get-Random}
$pipeUI = [hashtable]::Synchronized(@{})
$pipeUI.x,$pipeUI.y = [Console]::CursorLeft,[Console]::CursorTop
$pipeUI.width = $width
if ($null -ne $color) {$pipeUI.color = $color}
$pipeUI.type = $type
$rsjob = [runspacefactory]::CreateRunspace()
$rsjob.ApartmentState = "STA"
$rsjob.ThreadOptions = "ReuseThread"
$rsjob.Open()
$rsjob.SessionStateProxy.SetVariable("pipeUI", $pipeUI)
$pipeUI.psThread = [powershell]::Create().AddScript({
$start = [datetime]::now
$pipeUI.running = $true
$width = $pipeUI.width
$count = if ($pipeUI.type -eq 'tide') {0} else {1}
$dir = 1 # from left to rigth
$fill = '░'
$cursor = '▓'
$format = "{0,$(-$width-1)}"
$sleep = if ($pipeUI.type -eq 'bounce') {100} else {120}
[Console]::CursorVisible = $false
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$fg = [Console]::ForegroundColor
if ($pipeUI.color) {[Console]::ForegroundColor = $pipeUI.color}
while ($pipeUI.running) {
Start-Sleep -Milliseconds $sleep
if ($pipeUI.type -eq 'bounce') {
if ($dir -eq 1) {
[Console]::Write($fill*($count-1))
[Console]::Write($cursor)
[Console]::Write($fill*($width-$count))
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$count++
if ($count -gt $width) {$count--; $dir = 0}
} else {
[Console]::Write($fill*($count-1))
[Console]::Write($cursor)
[Console]::Write($fill*($width-$count))
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$count--
if ($count -eq 0) {$count++; $dir = 1}
}
} elseif ($pipeUI.type -eq 'tide') {
if ($dir -eq 1) {
[Console]::Write($cursor*$count)
[Console]::Write($fill*($width-$count))
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$count++
if ($count -gt $width) {$count--; $dir = 0}
} else {
[Console]::Write($cursor*$count)
[Console]::Write($fill*($width-$count))
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$count--
if ($count -eq -1) {$count++; $dir = 1}
}
} elseif ($pipeUI.type -eq 'growth') {
[Console]::Write($format,$cursor*$count)
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$count++
if ($count -gt $width) {$count = 1}
} else { # fill
[Console]::Write($cursor*$count)
[Console]::Write($fill*($width-$count))
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$count++
if ($count -gt $width) {$count = 1}
}
} # while
$pipeUI.runtime = [datetime]::now - $start
# cleanup
if ($pipeUI.type -eq 'growth') {
[Console]::Write($format,'⁠ '*$width)
} else {
[Console]::Write('⁠ '*$width)
}
if ([Console]::ForegroundColor -ne $fg) {[Console]::ForegroundColor = $fg}
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
[Console]::CursorVisible = $true
})
$pipeUI.psThread.Runspace = $rsjob
$pipeUI.handle = $pipeUI.psThread.BeginInvoke()
$null = & $scriptblock # try {} catch {}
$pipeUI.running = $false
while (-not $pipeUI.handle.IsCompleted) {[System.Threading.Thread]::Sleep(40)}
$null = $pipeUI.psThread.EndInvoke($pipeUI.handle)
$pipeUI.psThread.Runspace.Close()
$pipeUI.psThread.Runspace.Dispose()
$pipeUI.psThread.Dispose()
$pipeUI.psThread = $null
if ($passthru) {$pipeUI.runtime}
} # END Show-Progress
function Replace-Color ($path,$recolor) {
if ($null -eq $recolor) {return $path}
$subst = @{}
$recolor | Where-Object {$_} | ForEach-Object {
$item = $_.replace('#','').trim()
if ($item -match '=') {
$old,$new = $item.split('=').trim()
if ($old -ne '' -and $new -ne '') {$subst["#$old"] = "#$new"}
} elseif ($item) {
$subst['*'] = "#$item"
}
}
if (-not $subst.count) {return $path}
$dirty = $false
$newsvg = Get-Content $path | ForEach-Object {
$m = [regex]::Matches($_,'(stroke|fill)="(#?[^"no]+)"',[System.Text.RegularExpressions.RegexOptions]'Compiled,IgnoreCase')
if ($m.count) {
$l = $_
$m.groups.where({$_.name -eq 2 -and $_.success}).value |
Sort-Object -Unique | ForEach-Object {
$c = $subst[$_]
if (-not $c) {$c = $subst['*']}
if ($c) {
$dirty = $true
$l = $l.replace($_,$c)
}
}
$l
} else {$_}
}
if ($dirty) {
$fn = [IO.Path]::GetFileNameWithoutExtension($path), 'edit.svg' -join '-'
$newpath = [IO.Path]::GetDirectoryName($path),$fn -join '\'
try {$newsvg | Out-File -FilePath $newpath -Encoding utf8 -Force} catch {
Write-Host "ERROR: Failed to edit SVG. Check your write access to the source directory." -ForegroundColor Red
return
}
$path = $newpath
}
$path
} # END Replace-Color
$error.Clear()
if ($IsLinux -or $IsMacOS) {return}
$Sizes = @($Sizes | Sort-Object -Descending -Unique | Where-Object {$_ -gt 7 -or $_ -lt 257})
if ($Sizes.length -eq 0) {
Write-Host 'ERROR: Invalid sizes specified.' -ForegroundColor Red
return
}
# SVG engine
$inkscape = 'C:\Program Files\Inkscape\bin\inkscape.com'
if (-not (Test-Path -LiteralPath $inkscape)) {
Write-Warning "Trying to find Microsoft Store installation. Note, that there are problems to run MS Store Inkscape from command line."
# check for Microsoft Store installation
$inkscapeReg = Get-ChildItem -Path 'HKLM:\SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\PackageRepository\Packages\*inkscape*' -ErrorAction 0
###$inkscape = if ($inkscapeReg) {$inkscapeReg.GetValue('path'), 'VFS\ProgramFilesX64\Inkscape\bin\inkscape.exe' -join '\'}
###if (-not $inkscape -or -not (Test-Path -LiteralPath $inkscape)) {
$inkscape = if ($inkscapeReg) {(Get-ChildItem -Path $inkscapeReg.GetValue('path') -Filter inkscape.exe -Recurse -ErrorAction 0).FullName}
if (-not $inkscape) {
Write-Host "ERROR: Could not found Inkscape app.`nInstall it with winget (winget install Inkscape.Inkscape) or Microsoft Store" -ForegroundColor Red
return
} else {
try {Start-Process -FilePath $inkscape -Wait -NoNewWindow -ArgumentList '--version'}
catch {
Write-Host "ERROR: Unable to start Microsoft Store version of Inkscape app via command line.`nUninstall the current version and install it with winget: winget install Inkscape.Inkscape" -ForegroundColor Red
return
}
}
} # Inkscape
# Resolve/validate input file
if (([uri]$Path).Scheme -match '^http') {
if ($Path -match '\.svg$') {
$fn = $home,'downloads',[IO.Path]::GetFileName($Path) -join '\'
if (Test-Path $fn) {Remove-Item $fn -Force -ErrorAction 0}
$ProgressPreference = 'SilentlyContinue'
$null = Invoke-WebRequest -Uri $Path -UseBasicParsing -OutFile $fn -ErrorAction 0
$ProgressPreference = 'Continue'
$Path = $fn
if (-not $output) {$output = "$home\downloads"}
} else {
Write-Warning "The file requested doesn't look like SVG. It must have .svg extension. Check URL and try again."
return
}
}
$Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
if (-not (Test-Path $Path -PathType Leaf -ErrorAction 0)) {
Write-Host 'ERROR: Input file not found. Check the file path and try again.' -ForegroundColor Red
return
}
try {[xml]$xml = [System.IO.File]::ReadAllText($Path)} catch {
Write-Host 'ERROR: Input file is not SVG or is corrupted. Check file name, content integrity and try again.' -ForegroundColor Red
return
}
# Edit SVG
$Path = Replace-Color $Path $ReColor
if (-not $Path) {return}
# Resolve file paths
$svgFile = Get-Item -LiteralPath $Path -ErrorAction Stop
$tempDirectory = Join-Path -Path $env:TEMP -ChildPath '_svgworkshop_'
if (-not (Test-Path $tempDirectory)) {
$null = New-Item -ItemType Directory -Path $tempDirectory -ErrorAction Stop
} else { # cleanup svg desktop
Remove-Item -Path $tempDirectory\*.* -Force -ErrorAction 0
}
if ($Output) {
$Output = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Output.TrimEnd('\'))
}
if (-not $Output -or -not (Test-Path $Output -PathType Container -ErrorAction 0)) {$Output = $svgFile.DirectoryName}
$basename = if ($Rename) { # output file name
if ($Rename -match '^+') {
$svgFile.BaseName + $Rename.Substring(1)
} else {$Rename}
} else {$svgFile.BaseName}
# Start converter, prepare data for ICO
Write-Host "Preparing intermediate PNG ($($Sizes.length))... " -NoNewline
$fnTemplate = '{0}\{1}_%sz%.png' -f $tempDirectory,$svgFile.BaseName
$area = if ($NoCrop) {'export-area-page'} else {'export-area-drawing'}
$imgmargin = if ($margin -gt 0) {"export-margin:$margin"}
$szActions = $Sizes | ForEach-Object {
$pngPath = $fnTemplate.replace('%sz%',$_)
"export-width:$_",
"export-height:$_",
"export-filename:$pngPath",
"export-do"
}
$actions = "export-overwrite","export-type:png","export-dpi:300",$area,$imgmargin + $szActions | Where-Object {$_}
$iparam = ('--actions="{0}"' -f ($actions -join ';')), $svgFile
$icmd = {Start-Process -FilePath $inkscape -Wait -NoNewWindow -ArgumentList $iparam -ErrorAction 0}
if ($NoProgress) {
Write-Host (Measure-RunTime -command $icmd)
} else {
$param = if ($BarColor) {@{Color=$BarColor}} else {@{}}
Write-Host (Measure-RunTime -timespan (Show-Progress $icmd -type $BarType @param -passthru))
}
# Import PNG as bytestream
[array]$images = $Sizes | ForEach-Object {
$pngPath = $fnTemplate.replace('%sz%',$_)
# file bitmap
[byte[]]$bitmap = try {[System.IO.File]::ReadAllBytes($pngPath)} catch {return}
@{
Size = if ($_ -eq 256) {0} else {$_} # 0, trigger for a hack
Bitmap = $bitmap
}
} # images
if ($images.length -eq 0) {return} # something went wrong: invalid input or Inkscape's failure
if ($images.length -ne $sizes.Length) {Write-Warning "Range of sizes was cut ($($images.length) of $($sizes.Length))."}
# Normalize ico sizes for writing
# 256 alone doesn't work, so we apply a pinpoint hack, 255
if ($images.length -eq 1 -and $images[0].size -eq 0) {$images[0].size = 255}
Write-Host 'Writing ICO...'
$icoPath = '{0}\{1}.ico' -f $Output,$basename
$icoStream = try {[System.IO.File]::Open($icoPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write)} catch {}
if ($null -eq $icoStream) {
Write-Host "ERROR: Failed to create output file. Check your write access to the output directory and try again." -ForegroundColor Red
return
}
$icoWriter = [System.IO.BinaryWriter]::new($icoStream)
# 0-1 reserved, 0
$icoWriter.Write([byte]0)
$icoWriter.Write([byte]0)
# 2-3 image type: 1 = icon, 2 = cursor
$icoWriter.Write([int16]1)
# 4-5 number of images
$icoWriter.Write([int16]$images.length)
$offset = 6 + (16 * $images.length)
foreach ($item in $images) {
# image entry 1
# 0 image width
$icoWriter.Write([byte]$item.Size)
# 1 image height
$icoWriter.Write([byte]$item.Size)
# 2 number of colors
$icoWriter.Write([byte]0)
# 3 reserved
$icoWriter.Write([byte]0)
# 4-5 color planes
$icoWriter.Write([int16]0)
# 6-7 bits per pixel
$icoWriter.Write([int16]32)
# 8-11 size of image data
$icoWriter.Write([int]$item.Bitmap.Length)
# 12-15 offset of image data
$icoWriter.Write([int]$offset)
$offset += $item.Bitmap.Length
}
# write image data
# png data must contain the whole png data file
foreach ($item in $images) {
$icoWriter.Write($item.Bitmap)
}
$icoWriter.Flush()
# Cleanup, finalizing
$icoStream.Close()
if ($icoWriter) {
$icoWriter.Close()
$icoWriter.Dispose()
}
if (-not $Keep -and (Test-Path -LiteralPath $tempDirectory)) {
Remove-Item -Path $tempDirectory -Recurse -Force -ErrorAction 0
} elseif ($Keep) {Invoke-Item $tempDirectory}
if ($ReColor) {
Get-ChildItem ([IO.Path]::GetDirectoryName($Path)) -Filter '*-edit.svg' -ErrorAction 0 | Remove-Item -Force -ErrorAction 0
}
# Validate ICO file
if (Test-Path $icoPath) {
$param = @{TotalCount = 4}
if ($PSVersionTable.PSVersion.Major -lt 6) {$param['Encoding'] = 'Byte'}
else {$param['AsByteStream'] = $true}
$signature = Get-Content -LiteralPath $icoPath @param -ErrorAction 0
if ($signature -and ([System.BitConverter]::ToInt32($signature, 0)) -eq 0x10000) {
Write-Host "ICO file successfully created in '$Output'" -ForegroundColor Green
} else {
Write-Host "ERROR: '$([System.IO.Path]::GetFileName($icoPath))' is not a valid ICO file." -ForegroundColor Red
}
} else {
Write-Host "ERROR: ICO file is not created." -ForegroundColor Red
}
} # END Convert-SvgToIco
# .NET edition
function Convert-SvgToIco {
[CmdletBinding()]
[Alias('ConvertTo-Ico','s2i')]
param (
# file options
[Parameter(Position=0,Mandatory)]
[Alias('FilePath','LiteralPath','Fullname')]
[string] $Path,
[string] $Output, # output folder
[alias('alter')][string] $Rename, # alternative name for output file
# image options
[int[]] $Sizes = (16,20,24,32,40,48,64,72,96,128,192,256),
[Alias('notrim','noclip','nc')][switch] $NoCrop, # extraction area
[int] $Margin, # reserved; extraction area, currently has no effect for PNG format
[string[]] $ReColor, # replace colors in SVG
[switch] $Keep, # don't delete png files and open temp folder in explorer
# runtime options
[switch] $Quiet, # experimental; mute diagnostic (and error?) messages
[alias('type')][string] $BarType,
[alias('color')][ConsoleColor] $BarColor,
[alias('np','nostatus')][switch] $NoProgress
)
function Measure-RunTime ([scriptblock]$command, [timespan]$timespan) {
$diff = if ($timespan) {$timespan}
elseif ($command) {Measure-Command -Expression $command -ErrorAction 0}
else {return}
$ms = $diff.TotalMilliseconds - [Math]::Truncate($diff.TotalMilliseconds) + $diff.Milliseconds
if ($diff.Minutes) {'{0}m:{1}s:{2:N2}ms' -f $diff.Minutes,$diff.Seconds,$ms}
elseif ($diff.Seconds) {'{0}s:{1:N2}ms' -f $diff.Seconds,$ms}
else {'{0:N2}ms' -f $diff.TotalMilliseconds}
} # END Measure-RunTime
function Show-Progress {
param (
[scriptblock] $scriptblock,
[int] $width,
[consolecolor] $color,
[string] $type, # growth | fill | bounce | tide
[switch] $passthru
)
if (-not $scriptblock) {return}
if ($width -lt 1) {$width = 10}
if ([Console]::CursorLeft + $width -ge [Console]::WindowWidth) {
return Measure-Command -Expression $scriptblock -ErrorAction 0
}
if ($type -notmatch 'growth|fill|bounce|tide') {$type = 'growth','fill','bounce','tide' | Get-Random}
$pipeUI = [hashtable]::Synchronized(@{})
$pipeUI.x,$pipeUI.y = [Console]::CursorLeft,[Console]::CursorTop
$pipeUI.width = $width
if ($null -ne $color) {$pipeUI.color = $color}
$pipeUI.type = $type
$rsjob = [runspacefactory]::CreateRunspace()
$rsjob.ApartmentState = "STA"
$rsjob.ThreadOptions = "ReuseThread"
$rsjob.Open()
$rsjob.SessionStateProxy.SetVariable("pipeUI", $pipeUI)
$pipeUI.psThread = [powershell]::Create().AddScript({
$start = [datetime]::now
$pipeUI.running = $true
$width = $pipeUI.width
$count = if ($pipeUI.type -eq 'tide') {0} else {1}
$dir = 1 # from left to rigth
$fill = '░'
$cursor = '▓'
$format = "{0,$(-$width-1)}"
$sleep = if ($pipeUI.type -eq 'bounce') {100} else {120}
[Console]::CursorVisible = $false
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$fg = [Console]::ForegroundColor
if ($pipeUI.color) {[Console]::ForegroundColor = $pipeUI.color}
while ($pipeUI.running) {
Start-Sleep -Milliseconds $sleep
if ($pipeUI.type -eq 'bounce') {
if ($dir -eq 1) {
[Console]::Write($fill*($count-1))
[Console]::Write($cursor)
[Console]::Write($fill*($width-$count))
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$count++
if ($count -gt $width) {$count--; $dir = 0}
} else {
[Console]::Write($fill*($count-1))
[Console]::Write($cursor)
[Console]::Write($fill*($width-$count))
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$count--
if ($count -eq 0) {$count++; $dir = 1}
}
} elseif ($pipeUI.type -eq 'tide') {
if ($dir -eq 1) {
[Console]::Write($cursor*$count)
[Console]::Write($fill*($width-$count))
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$count++
if ($count -gt $width) {$count--; $dir = 0}
} else {
[Console]::Write($cursor*$count)
[Console]::Write($fill*($width-$count))
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$count--
if ($count -eq -1) {$count++; $dir = 1}
}
} elseif ($pipeUI.type -eq 'growth') {
[Console]::Write($format,$cursor*$count)
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$count++
if ($count -gt $width) {$count = 1}
} else { # fill
[Console]::Write($cursor*$count)
[Console]::Write($fill*($width-$count))
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
$count++
if ($count -gt $width) {$count = 1}
}
} # while
$pipeUI.runtime = [datetime]::now - $start
# cleanup
if ($pipeUI.type -eq 'growth') {
[Console]::Write($format,'⁠ '*$width)
} else {
[Console]::Write('⁠ '*$width)
}
if ([Console]::ForegroundColor -ne $fg) {[Console]::ForegroundColor = $fg}
[Console]::SetCursorPosition($pipeUI.x,$pipeUI.y)
[Console]::CursorVisible = $true
})
$pipeUI.psThread.Runspace = $rsjob
$pipeUI.handle = $pipeUI.psThread.BeginInvoke()
$null = & $scriptblock # try {} catch {}
$pipeUI.running = $false
while (-not $pipeUI.handle.IsCompleted) {[System.Threading.Thread]::Sleep(40)}
$null = $pipeUI.psThread.EndInvoke($pipeUI.handle)
$pipeUI.psThread.Runspace.Close()
$pipeUI.psThread.Runspace.Dispose()
$pipeUI.psThread.Dispose()
$pipeUI.psThread = $null
if ($passthru) {$pipeUI.runtime}
} # END Show-Progress
function Replace-Color ($path,$recolor) {
if ($null -eq $recolor) {return $path}
$subst = @{}
$recolor | Where-Object {$_} | ForEach-Object {
$item = $_.replace('#','').trim()
if ($item -match '=') {
$old,$new = $item.split('=').trim()
if ($old -ne '' -and $new -ne '') {$subst["#$old"] = "#$new"}
} elseif ($item) {
$subst['*'] = "#$item"
}
}
if (-not $subst.count) {return $path}
$dirty = $false
$newsvg = Get-Content $path | ForEach-Object {
$m = [regex]::Matches($_,'(stroke|fill)="(#?[^"no]+)"',[System.Text.RegularExpressions.RegexOptions]'Compiled,IgnoreCase')
if ($m.count) {
$l = $_
$m.groups.where({$_.name -eq 2 -and $_.success}).value |
Sort-Object -Unique | ForEach-Object {
$c = $subst[$_]
if (-not $c) {$c = $subst['*']}
if ($c) {
$dirty = $true
$l = $l.replace($_,$c)
}
}
$l
} else {$_}
}
if ($dirty) {
$fn = [IO.Path]::GetFileNameWithoutExtension($path), 'edit.svg' -join '-'
$newpath = [IO.Path]::GetDirectoryName($path),$fn -join '\'
try {$newsvg | Out-File -FilePath $newpath -Encoding utf8 -Force} catch {
Write-Host "ERROR: Failed to edit SVG. Check your write access to the source directory." -ForegroundColor Red
return
}
$path = $newpath
}
$path
} # END Replace-Color
$error.Clear()
if ($IsLinux -or $IsMacOS) {return}
$Sizes = @($Sizes | Sort-Object -Descending -Unique | Where-Object {$_ -gt 7 -or $_ -lt 257})
if ($Sizes.length -eq 0) {
Write-Host 'ERROR: Invalid sizes specified.' -ForegroundColor Red
return
}
try {$null = [System.Drawing.Icon]}
catch {Add-Type -AssemblyName System.Drawing}
# SVG engine
$inkscape = 'C:\Program Files\Inkscape\bin\inkscape.com'
if (-not (Test-Path -LiteralPath $inkscape)) {
Write-Warning "Trying to find Microsoft Store installation. Note, that there are problems to run MS Store Inkscape from command line."
# check for Microsoft Store installation
$inkscapeReg = Get-ChildItem -Path 'HKLM:\SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\PackageRepository\Packages\*inkscape*' -ErrorAction 0
###$inkscape = if ($inkscapeReg) {$inkscapeReg.GetValue('path'), 'VFS\ProgramFilesX64\Inkscape\bin\inkscape.exe' -join '\'}
###if (-not $inkscape -or -not (Test-Path -LiteralPath $inkscape)) {
$inkscape = if ($inkscapeReg) {(Get-ChildItem -Path $inkscapeReg.GetValue('path') -Filter inkscape.exe -Recurse -ErrorAction 0).FullName}
if (-not $inkscape) {
Write-Host "ERROR: Could not found Inkscape app.`nInstall it with winget (winget install Inkscape.Inkscape) or Microsoft Store" -ForegroundColor Red
return
}
} # Inkscape
# Resolve/validate input file
if (([uri]$Path).Scheme -match '^http') {
if ($Path -match '\.svg$') {
$fn = $home,'downloads',[IO.Path]::GetFileName($Path) -join '\'
if (Test-Path $fn) {Remove-Item $fn -Force -ErrorAction 0}
$ProgressPreference = 'SilentlyContinue'
$null = Invoke-WebRequest -Uri $Path -UseBasicParsing -OutFile $fn -ErrorAction 0
$ProgressPreference = 'Continue'
$Path = $fn
if (-not $output) {$output = "$home\downloads"}
} else {
Write-Warning "The file requested doesn't look like SVG. It must have .svg extension. Check URL and try again."
return
}
}
$Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
if (-not (Test-Path $Path -PathType Leaf -ErrorAction 0)) {
Write-Host 'ERROR: Input file not found. Check the file path and try again.' -ForegroundColor Red
return
}
try {[xml]$xml = [System.IO.File]::ReadAllText($Path)} catch {
Write-Host 'ERROR: Input file is not SVG or is corrupted. Check file name, content integrity and try again.' -ForegroundColor Red
return
}
# Edit SVG
$Path = Replace-Color $Path $ReColor
if (-not $Path) {return}
# Resolve file paths
$svgFile = Get-Item -LiteralPath $Path -ErrorAction Stop
$tempDirectory = Join-Path -Path $env:TEMP -ChildPath '_svgworkshop_'
if (-not (Test-Path $tempDirectory)) {
$null = New-Item -ItemType Directory -Path $tempDirectory -ErrorAction Stop
} else { # cleanup svg desktop
Remove-Item -Path $tempDirectory\*.* -Force -ErrorAction 0
}
if ($Output) {
$Output = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Output.TrimEnd('\'))
}
if (-not $Output -or -not (Test-Path $Output -PathType Container -ErrorAction 0)) {$Output = $svgFile.DirectoryName}
$basename = if ($Rename) { # output file name
if ($Rename -match '^+') {
$svgFile.BaseName + $Rename.Substring(1)
} else {$Rename}
} else {$svgFile.BaseName}
# Start converter, prepare data for ICO ⟳ ⏳ org.inkscape.color.replace-color
Write-Host "Preparing intermediate PNG ($($Sizes.length))... " -NoNewline
$fnTemplate = '{0}\{1}_%sz%.png' -f $tempDirectory,$svgFile.BaseName
$area = if ($NoCrop) {'export-area-page'} else {'export-area-drawing'}
$imgmargin = if ($margin -gt 0) {"export-margin:$margin"}
$szActions = $Sizes | ForEach-Object {
$pngPath = $fnTemplate.replace('%sz%',$_)
"export-width:$_",
"export-height:$_",
"export-filename:$pngPath",
"export-do"
}
$actions = "export-overwrite","export-type:png","export-dpi:300",$area,$imgmargin + $szActions | Where-Object {$_}
$iparam = ('--actions="{0}"' -f ($actions -join ';')), $svgFile
$icmd = {Start-Process -FilePath $inkscape -Wait -NoNewWindow -ArgumentList $iparam -ErrorAction 0}
if ($NoProgress) {
Write-Host (Measure-RunTime -command $icmd)
} else {
$param = if ($BarColor) {@{Color=$BarColor}} else {@{}}
Write-Host (Measure-RunTime -timespan (Show-Progress $icmd -type $BarType @param -passthru))
}
# Convert PNG to image object
[array]$images = $Sizes | ForEach-Object {
$pngPath = $fnTemplate.replace('%sz%',$_)
# create output bitmap; .NET bitmap
$inputBitmap = try{[System.Drawing.Image]::FromFile($pngPath)} catch {return}
if ($null -eq $inputBitmap) {return} # system failure
$size = [System.Drawing.Size]::new($inputBitmap.Width, $inputBitmap.Height)
$outputBitmap = [System.Drawing.Bitmap]::new($inputBitmap, $size)
$memoryStream = [System.IO.MemoryStream]::new()
$outputBitmap.Save($memoryStream, [System.Drawing.Imaging.ImageFormat]::Png)
@{
Size = if ($_ -eq 256) {0} else {$_} # 0, trigger for a hack
Bitmap = $memoryStream
}
# cleanup
$outputBitmap.Dispose()
$inputBitmap.Dispose()
} # images
if ($images.length -eq 0) {return} # something went wrong: invalid input or Inkscape's failure
if ($images.length -ne $sizes.Length) {Write-Warning "Range of sizes was cut ($($images.length) of $($sizes.Length))."}
# Normalize ico sizes for writing
# 256 alone doesn't work, so we apply a pinpoint hack, 255
if ($images.length -eq 1 -and $images[0].size -eq 0) {$images[0].size = 255}
Write-Host 'Writing ICO...'
$icoPath = '{0}\{1}.ico' -f $Output,$basename
$icoStream = try {[System.IO.File]::Open($icoPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write)} catch {}
if ($null -eq $icoStream) {
Write-Host "ERROR: Failed to create output file. Check your write access to the output directory and try again." -ForegroundColor Red
return
}
$icoWriter = [System.IO.BinaryWriter]::new($icoStream)
# 0-1 reserved, 0
$icoWriter.Write([byte]0)
$icoWriter.Write([byte]0)
# 2-3 image type: 1 = icon, 2 = cursor
$icoWriter.Write([int16]1)
# 4-5 number of images
$icoWriter.Write([int16]$images.length)
$offset = 6 + (16 * $images.length)
foreach ($item in $images) {
# image entry 1
# 0 image width
$icoWriter.Write([byte]$item.Size)
# 1 image height
$icoWriter.Write([byte]$item.Size)
# 2 number of colors
$icoWriter.Write([byte]0)
# 3 reserved
$icoWriter.Write([byte]0)
# 4-5 color planes
$icoWriter.Write([int16]0)
# 6-7 bits per pixel
$icoWriter.Write([int16]32)
# 8-11 size of image data
$icoWriter.Write([int]$item.Bitmap.Length)
# 12-15 offset of image data
$icoWriter.Write([int]$offset)
$offset += $item.Bitmap.Length
}
# write image data
# png data must contain the whole png data file
foreach ($item in $images) {
$icoWriter.Write($item.Bitmap.ToArray())
$item.Bitmap.Dispose()
}
$icoWriter.Flush()
# Cleanup, finalizing
$icoStream.Close()
if ($icoWriter) {
$icoWriter.Close()
$icoWriter.Dispose()
}
if (-not $Keep -and (Test-Path -LiteralPath $tempDirectory)) {
Remove-Item -Path $tempDirectory -Recurse -Force -ErrorAction 0
} elseif ($Keep) {Invoke-Item $tempDirectory}
if ($ReColor) {
Get-ChildItem ([IO.Path]::GetDirectoryName($Path)) -Filter '*-edit.svg' -ErrorAction 0 | Remove-Item -Force -ErrorAction 0
}
# Validate ICO file
if (Test-Path $icoPath) {
$param = @{TotalCount = 4}
if ($PSVersionTable.PSVersion.Major -lt 6) {$param['Encoding'] = 'Byte'}
else {$param['AsByteStream'] = $true}
$signature = Get-Content -LiteralPath $icoPath @param -ErrorAction 0
if ($signature -and ([System.BitConverter]::ToInt32($signature, 0)) -eq 0x10000) {
Write-Host "ICO file successfully created in '$Output'" -ForegroundColor Green
} else {
Write-Host "ERROR: '$([System.IO.Path]::GetFileName($icoPath))' is not a valid ICO file." -ForegroundColor Red
}
} else {
Write-Host "ERROR: ICO file is not created." -ForegroundColor Red
}
} # END Convert-SvgToIco
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment