PowerShell PNG 转 ICO

使用 PowerShell 将 PNG 文件转换为 ICO 文件有多种方法,但是都不太方便,且不易理解。鉴于我们现在的操作系统版本大多已经在 Windows Vista 以上,无需考虑以前的 BMP 多分辨率多位数模式,可以直接使用 PNG 加上头信息构造出 ICO 文件。这样就可以非常方便的将 PNG 转换为 ICO,同时可以无损保留 PNG。

结构

这里我们不引入任何依赖,手动合成,因此首先需要了解一下 ICO 文件的结构。

参考:

简言之,ICO 格式由头文件和图片数据组成,其中头文件的十六进制数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ICONDIR 0x00 - 0x06 : 0x06
00 00 # 保留
01 00 # 图像类型,ico 为 1
01 00 # 图像数量,1 张

# ICONDIRENTRY 如有多张,每张图像来一个 0x06 - 0x16 : 0x10 (16 bytes)
00 # 宽度
00 # 高度
00 # 颜色,大于8bpp为0(无调色板)
00 # 保留
01 00 # 颜色平面
20 00 # 每像素位数(32bpp)
6F 48 00 00 # 图像文件大小(需要修改) 0x0e - 0x12 : 0x04
16 00 00 00 # 图像数据偏移量

# 总共: 22 bytes

ICO 格式的字节序为 Little-Endian(如上方的 6F 48 00 00 为从右往左读作 48 6F),Windows 下默认表现为 LE,通常编写 PowerShell 时无需在意这个问题,自动生成出来就是 LE 的。

由于我们只有一张图,且图片为 PNG 格式,因此我们只需要修改 ICONDIRENTRY 中的图像文件大小即可。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function Convert-PngToIco {
param (
[string]$PngPath,
[string]$IcoPath
)
# 读取文件
$png = [System.IO.File]::ReadAllBytes($pngPath)

# 生成头信息
$ico = [System.IO.MemoryStream]::new()
$bin = [System.IO.BinaryWriter]::new($ico)

# 写入ICONDIR结构
$bin.Write([uint16]0) # 保留
$bin.Write([uint16]1) # 图像类型,ico 为 1
$bin.Write([uint16]1) # 图像数量,1 张

# 写入ICONDIRENTRY结构
$bin.Write([sbyte]0) # 宽度
$bin.Write([sbyte]0) # 高度
$bin.Write([sbyte]0) # 颜色
$bin.Write([sbyte]0) # 保留
$bin.Write([uint16]1) # 颜色平面
$bin.Write([uint16]32) # 每像素位数(32bpp)
$bin.Write([uint32]$png.Length) # 图像文件大小
$bin.Write([uint32]22) # 图像数据偏移量

# 写入图像数据
$bin.Write($png)

[System.IO.File]::WriteAllBytes($icoPath, $ico.ToArray())

# 清理资源
$bin.Dispose()
$ico.Dispose()
}

# 使用示例
$pngPath = ".\image.png"
$icoPath = ".\image.ico"
Convert-PngToIco -PngPath $pngPath -IcoPath $icoPath

技巧

PNG 图片可以先使用 TinyPNG 压缩,这样制作出来的图标体积更小。