IconLib - 图标展开 (支持 MultiIcon 和 Windows Vista)






4.96/5 (659投票s)
用于处理图标和图标库的库,支持以 ico、icl、dll、exe、cpl 和 src 格式创建、加载、保存、导入和导出图标。(支持 Windows Vista 图标)。
引言
大约在上个月,我需要即时创建一个包含几个图像的 .ICO 文件;最好是用 C# 编写的代码。 .NET Framework 2.0 只支持 HICON,它基本上是一个只有一个图像的图标。当我四处寻找时,令我沮丧的是,我没有找到任何带有源代码的图标编辑器。我找到的只有商业闭源产品,价格从 19 美元到 39 美元不等,而且完全不暴露 API。因此,唯一的解决方案是创建一个能够创建和解析 ICO 文件的库。
我相信开源代码,并认为我可以分享这些知识来帮助开发者社区。此外,开源推动公司和商业产品不断进步。
工作完成后,我阅读了关于 ICL 文件(图标库)的内容。这些文件可以包含许多图标,我也决定支持它们。EXE/DLL 文件也是如此,最后我决定支持 Windows Vista 图标。所有这些工作都非常艰苦,让我头痛不已,因为公开的信息很少。我最终花费了大量时间进行逆向工程和网络研究。我希望它对您和我一样有用。
注意
正如任何新项目一样,可能会发生很多事情。并非所有情况都能得到测试,即使测试了,也可能无法看到很多东西。由于这是一个全新的项目,如果有什么不起作用,或者您认为它应该像现在这样工作,在您投票之前,请发帖给我一个修复它的机会。这样,我们都可以受益于更稳定的代码和更完整的库,同时如果您能提供帮助,我将不胜感激。
我还包含两个库作为示例。我从 Windows Vista 借用了一些图标,用于 256x256 版本,并添加了水印,因为图标具有版权。希望我不会因此遇到麻烦。
目标
该库的目标是创建文件格式的抽象层,并提供一个接口,允许在不了解内部文件格式的情况下修改图标。
当前支持的格式
- ICO 读取和写入具有不同尺寸和位深的图标
- ICL 读取和写入图标库内的图标
- DLL 读取 DLL 并导出到新的 DLL
- EXE 导入
- OCX 导入
- CPL 导入
- SRC 导入
MultiIcon
Iconlib
暴露了三个不同的对象。
MultiIcon
: 这是库中唯一可以实例化的对象。一旦创建了MultiIcon
对象,它就可以通过 API 以标准格式 ICO/ICL/DLL 加载和保存到文件系统或流中。SingleIcon
: 它代表MultiIcon
中的单个图标,并允许在此图标中添加/删除图像。IconImage
: 它代表SingleIcon
中的单个图像。此时,IconImage
暴露了图标的最低资源,例如XOR
(图像) 和AND
图像 (蒙版)。它还暴露了一个 Icon 属性,该属性基本上从XOR
和AND
图像构建一个 .NET Icon,您可以从中获取HICON
句柄。
如您所见,存在一个层次结构,基本上一个 MultiIcon
包含 Icons
,而一个 Icon
包含 Images
。
库对象图
该库包含许多类和结构,但只暴露了三个重要的类。开发人员需要控制库的完整行为,其余都是内部的,许多类/结构和方法对开发人员来说是不安全的。因此,我建议将 IconLib
作为一个独立的项目,因为如果开发人员将源代码集成到他们的项目中,所有内部类/结构/方法都会变得可见,并且可能不会被正确使用。
当我以不当方式使用库时,我无法提供支持。我提供源代码是出于好意,因为我坚信开源代码,并希望您在不盗用其来源的情况下充分利用它。
图标格式
在开始 IconLib
之前,我完全不知道图标是如何工作的。然而,我在网上不久就找到了这篇精彩的文章 Icons in Win32
尽管这篇文章随着 Windows Vista 图标的出现而过时,但它非常精确地解释了图标格式文件是如何组成的。
需要注意的一点是;在我最初的版本中,我遵循了图标格式的详细信息,但库无法加载我正在测试的一些图标。当我深入研究字节时,我注意到目录条目中缺失了许多信息。
我使用另一个产品测试了这些图标,可以看到,例如一个流行的产品可以毫无问题地打开这种图标,这是因为每个图标目录条目都指向一个 ICONIMAGE
结构,这个 ICONIMAGE
结构有一个 BITMAPINFOHEADER
结构,它包含的信息比图标目录条目本身还要多。因此,基本上,利用 BITMAPINFOHEADER
中的信息,我可以重建目录条目中的信息。
同样的规则不能应用于 Windows Vista 图标,因为这些图像不再包含 BITMAPINFOHEADER
结构。因此,如果目录条目中缺少某些信息,图标图像就会变得无效。
总之,重建图标目录条目是一个加分项,而丢弃未正确构造的图标图像是可以接受的,任何公司都不应提供头部信息缺失的图标。
NE 格式 (ICL)
NE 格式是存储图标库的流行格式;这种格式最初用于 16 位 Windows 版本上的可执行文件。
您可以在 Microsoft 网站上找到有关 NE 格式的更多信息,网址为 Executable-File Header Format
这是项目中更具挑战性的部分。当我开始研究 ICL 时,我完全不知道这些是 16 位 DLL。我找不到关于此扩展名的任何数据,几天后,我几乎放弃了这个项目。但我读到某些地方说 ICL 是带有资源的 16 位 DLL,于是我的任务就开始了,如何从 16 位 DLL 中恢复资源。到目前为止,我的唯一目标是尝试将 16 位 DLL 加载到内存中。当然,起初我尝试使用标准的 Win32API LoadLibrary
、LoadLibraryEx
加载库,但失败了,原因如下
193 - ERROR_BAD_EXE_FORMAT (Is not a valid application.)
我对内核内存分配不是专家,但我猜测这是因为在 Win32 中,应用程序之间的内存是受保护的,而在 16 位中则不是,因此尝试为 16 位分配内存时,操作系统会拒绝该操作。
下一步是尝试仅使用 16 位 API 将 ICL(16 位 DLL)加载到内存中。如果您阅读 MSDN WIN32 API,唯一剩下的 16 位 API 是 Loadmodule
。
当我尝试时,它加载了库,但 Windows 立即开始弹出奇怪的消息框,例如“内存不足,无法运行 16 位应用程序”之类的消息。
我在 Microsoft 论坛和其他论坛上发帖,但没有找到任何有用的信息说明我如何获取这些资源。那时,很明显我无法将 16 位 DLL 加载到内存中,我需要创建自己的 NE 解析器/链接器。
Microsoft 关于 NE 格式(New Executable)的文章是一个极好的来源,并详细描述了文件中的每个字段。
NE 格式文件以 IMAGE_DOS_HEADER
开始,此标头用于保持与 MS-DOS 操作系统的兼容性。此标头还包含一些特定字段,以指示存在新的分段文件格式。IMAGE_DOS_HEADER
通常包含一个可以在 MS-DOS 上运行的有效可执行程序。此程序称为存根程序,通常它只会在屏幕上显示消息“此程序无法在 MS-DOS 上运行”。
在读取 IMAGE_DOS_HEADER
之后,首先要做的是知道它是否是一个有效的标头。通常每个文件都包含所谓的魔数。它之所以被称为魔数,是因为该字段中存储的数据与程序无关,但它包含一个签名来描述文件类型。
您几乎可以在任何地方找到魔数。IMAGE_DOS_HEADER 的魔数是 0x5A4D
,它代表字符 'MZ',代表“Mark Zbikowski”,他是 Microsoft 的架构师,在 Microsoft 成立后不久就开始为其工作。他可能从未想到他的签名会在世界上几乎所有的个人电脑上被使用数千次。
如果魔数为 'MZ',那么我们关心的唯一额外字段是 e_lfanew
,此标头是新 exe 标头(NE 标头)的偏移量。
我们在文件中搜索此偏移量,然后在该点读取一个新的标头。此标头是 IMAGE_OS2_HEADER
,它包含有关要在内存中加载的程序的所有信息。
首先要做的就是再次加载魔数,但这次魔数必须是 0x454E
,它代表 'NE'。如果签名匹配,则我们可以继续分析其余的标头。此时,更重要的字段是 ne_rsrctab
,因为该字段包含资源表的偏移量。从该偏移量开始,我们得到从该标头开头跳转的字节数,以便能够读取资源表。
如果一切顺利,我们就可以读取资源表了。
资源表的第一个字段是对齐移位计数,通常的解释是“资源数据的对齐移位计数。当移位计数用作 2 的指数时,结果值指定一个字节的因子,用于计算可执行文件中资源的位置。”
用我自己的话来说,这个字段的工作很难理解。它是为了兼容 MS-DOS 而创建的,它将包含到达资源所需的乘数。
正如您将看到的,资源偏移量是一个 ushort
类型的变量,这意味着它只能寻址 64KB(65536)。实际上,几乎所有文件都大于此,而 '对齐移位' 字段在这里发挥作用。
对齐移位是一个 ushort
,并且“通常”在 2 到 10 的范围内。这个数字是我们必须将数字 1 左移的次数。例如
- 5 的对齐移位表示 1 << 5,等于 32。
- 10 的对齐移位表示 1 << 10 = 1024
现在,使用资源表的虚拟地址,我们乘以移位结果值,就得到了文件中资源的实际地址。
例如
资源位于虚拟地址 0x2000,对齐移位为 5,则得到
Realoffset = (1 << 5) * 0x2000 Realoffset = 32 * 0x2000 Realoffset = 0x40000
此资源的实际偏移量为 262144 (0x40000)。
哇,这很酷,对吧?因为我们只使用了一个 ushort
就可以定位任意位置的资源。现在您会想,窍门在哪里?
窍门是,例如,如果您使用 5 的移位对齐,这意味着最小可寻址空间是 32 字节 (1 << 5),这意味着如果您想使用此方法分配 10 字节,将分配 32 字节,并且只使用前 10 字节,另外 22 字节将被浪费。
现在您可能会问,好吧,那么我们将移位对齐设置为 0,这样就不会浪费空间,因为虚拟地址将匹配实际地址。并非如此简单,这仅在资源位于第一个 64KB 空间范围内的范围时才有效。
因此,为了说清楚,这个移位对齐与文件大小成正比。
下表显示了不同移位对齐下的最大文件大小
(1 << 0) * (2 ^ 16) = 64KB (1 << 1) * (2 ^ 16) = 128KB (1 << 2) * (2 ^ 16) = 256KB (1 << 3) * (2 ^ 16) = 512KB (1 << 4) * (2 ^ 16) = 1MB (1 << 5) * (2 ^ 16) = 2MB (1 << 6) * (2 ^ 16) = 4MB (1 << 7) * (2 ^ 16) = 8MB (1 << 8) * (2 ^ 16) = 16MB (1 << 9) * (2 ^ 16) = 32MB (1 << 10) * (2 ^ 16) = 64MB
计算这个值并不容易。IconLib
最初使用 9 的移位因子,因为我认为 32MB 对于一个 Icon
库来说绰绰有余。但令我惊讶的是,当我提取 Windows Vista DLL 时,IconLib
在某些文件上超出了范围。然后我将移位因子增加到 10,这使我能够将 Windows Vista DLL 的内容转储到 ICL 文件中,但这花了 63MB。
因子为 10 的情况下,我们可以创建高达 64MB 的 ICL 库,但每个资源至少需要寻址 1024 字节。如果您认为这不算太糟,因为所有资源都将大于 1024 字节,那就不那么简单了。因子为 10 意味着它以 1024 的倍数寻址,那么如果资源是 1025 字节,它将在文件系统中分配 2048 字节。
总之,使用因子 10,IconLib
平均浪费 (1024 / 2) 512 字节每个分配的资源,但同时它允许我们创建 64MB 的 Icon
库。
我的下一个版本将预测最大文件大小并动态调整移位因子;如果您想在不扫描内存以了解最大可寻址空间的情况下预测此数字,这是一项艰巨的任务,尤其是对于 PNG 图像,其值也是动态的。
希望现在对移位对齐字段的解释已经清楚,我们回到资源表。
下一个字段是 TYPEINFO
的数组
TYPEINFO
是一个结构,它提供了关于资源的信息;可以分配许多类型的资源,但 IconLib
只对两种类型感兴趣:RT_GROUP_ICON
和 RT_ICON
。
当 IconLib
读取 TYPEINFO
数组时,它会丢弃所有 rtTypeID
不是 RT_GROUP_ICON
或 RT_ICON
的结构。
RT_GROUP_ICON
类型提供图标信息。
RT_ICON
类型提供图标内单个图像的信息。
rtResourceCount
是该类型资源的数量。
rtNameInfo
是一个 TNAMEINFO
数组,其中包含有关该类型每个资源的 [信息]。该数组的长度等于 rtResourceCount
。
这里是我们拥有关于资源本身的信息;rnOffset
是物理资源所在的虚拟地址。要了解实际地址,请参见上面的移位对齐如何工作。
rnLength
是资源在虚拟地址空间中的长度。这意味着,例如,如果资源长度为 1500 字节,对齐移位为 10,那么此字段中的值将为 2。
计算长度的方法是
rnLenght = Ceiling(ralresourcesize / (1 << resource_table.rscAlignShift)); rnFlags tell us if the resource is fixed, preloaded, or shareable rnID is the ID of the resource. rnHandle is reserved. rnUsage is reserved.
回到 TYPEINFO
,如果 TYPEINFO
结构类型是 ""OLE_LINK6"">RT_GROUP_ICON
,那么我们就读取 TNAMEINFO
数组,它提供了关于资源中每个图标的信息。
每个 TNAMEINFO
中的偏移量将包含一个指向 GRPICONDIR
结构的指针,该结构将提供关于单个图标的信息,例如它包含多少个图像以及一个 GRPICONDIRENTRY
数组,其中 GRPICONDIRENTRY
包含关于图像的信息,例如宽度、高度、颜色计数等。
现在,如果 TYPEINFO
结构类型是 RT_ICON
,那么我们就读取 TNAMEINFO
数组,它提供了关于资源内每个单个图像的信息。
回到资源表,我们还有另外三个字段:rscEndTypes
、rscResourcesNames
和 rscEndNames
。
rscEndTypes
是一个 ushort
值,告诉我们何时停止读取 TYPEINFO
结构。资源表结构没有告诉我们它包含多少个 TYPEINFO 结构,所以唯一知道的方法是使用一个停止标志。这个标志是 rscEndTypes
。如果我们读取 TYPEINFO
时前两个字节是零,则表示我们已到达 TYPEINFO
数组的末尾。
rscResourceNames
是一个字节数组,包含 TYPEINFO
结构中每个资源的名称。名称(如果有)与此表中的资源相关联。每个名称存储为连续字节;第一个字节指定名称中的字符数。
例如,如果数组是 [
5,
73,
67,
79,
78,
48,
5,
73,
67,
79,
78,
49]
这可以翻译为两个字符串数组:“Icon1
”、“Icon2
”。
[5, 73, 67, 79, 78, 48, 5, 73, 67, 79, 78, 49] [73, 67, 79, 78, 48] = "Icon1" [73, 67, 79, 78, 49] = "Icon2"
如果您想知道何时停止读取数组中的字节,还有一个停止标志 rscEndNames
,值为零。在读取字节时,如果检测到空字符 ('\x0'),则必须停止读取名称,并将它们准备好翻译为 ANSI 字符串。
此时,我们已经拥有了 Icon
和其中图像的所有信息和二进制数据。
IconLib
将所有
Icon
和 Icon
图像加载到内存中,以获得良好的性能。此外,它不需要锁定文件系统上的文件。
创建 ICL 文件并不复杂。因为 IconLib
是从头开始创建 ICL 的,所以它不必关心 NE 格式中的其他段,因此过程相对简单。我们写入一个 IMAGE_DOS_HEADER
,写入 MSDOS
存根程序,写入一个 IMAGE_OS2_HEADER
,我们在其中选择正确的对齐因子,并将资源表写入 os2_header
中 ne_rsrctab
字段指定的 [位置]。
写入资源表时,必须应用与加载时相同的规则。这意味着写入一个部分资源表结构,两个 TYPEINFO
结构(
RT_GROUP_ICON
和
RT_ICON
,并在
TYPEINFO
</ode>
中,写入 TNAMEINFO
信息)。
下表显示了一个存储 2 个图标的 NE 格式,第一个图标包含一个图像,第二个图标包含 2 个图像。
这是我想提的一点。如前所述,我重新设计了核心 3 次,第一次我遵循了关于如何从图标和 DLL 读取和写入图标文件的所有已知规范。当我从 DLL 导出图标时,我保留了关于图标的所有信息,如图标名称、组 ID 和图标 ID。当我将它们保存到文件系统时,我以读取它们的方式保存它们,所以基本上我可以将图标从 DLL 导出,导出到 ICL 文件,然后加载 ICL 并导出到 DLL,并且我将保持组和图标的 ID 相同。
到目前为止,我测试了两个流行的商业产品,它们可以毫无问题地打开它们,但例如,当我将某些 DLL 或 EXE 导出到 ICL 文件时,我开始遇到问题。例如,如果您在 Visual Studio 中打开 Windows 文件夹中的 explorer.exe,您会注意到第一个图标 ID 不是连续的,它们从 ID 100、101、102、103、104 开始,然后跳到 107,并继续。
IconLib
将 explorer.exe 导出到 ICL 文件,并在 Visual Studio 中导入,没有任何问题。但令我惊讶的是,当我尝试用一个流行的图标编辑器打开它时,图标库显示图标错乱,并且图标混合在一起。我花了很多天来弄清楚原因。
基本上,经过对不同应用程序的多次测试,我注意到那些应用程序在编写 ICL 文件时会丢弃图标和组 ID,并且它们期望连续的 ID。
对于 ICL 文件,有一个要写入的标头 TNAMEINFO
,该标头包含一个字段,即 ID。此 ID 可以是 GRPICONDIRENTRY
(图标本身)或 ICONDIRENTRY
ID(图标内的单个图像 ID)。当这些应用程序编写 ICL 文件时,它们会以连续的方式进行,基本上它们在从 DLL 导入时会丢弃 ID,并按 1、2、3、4 的顺序写入组 ID,图标 ID 也是如此,它们是 1、2、3、4 等。
因此,基本上我注意到一些应用程序无法妥善处理所有情况下的 ICL 文件。另一个不太流行的应用程序通过了,它基本上可以读取 ID 不连续的 ICL 文件,但当它保存 ICL 文件时,它会丢弃源 ID 并使用自己的 ID。
所以,我面临一个艰难的抉择。我应该保留所有信息并将信息按照从 EXE/DLL 来的方式写入 ICL 文件;这将使我的 ICL 文件结构正确,但与一些外部应用程序不兼容。还是我应该在导入时丢弃原始 ID 并创建连续 ID,这意味着丢弃部分原始信息并使用我自己的 ID(我不太喜欢这个解决方案),但小鱼可以在大鱼池里游,除非它们表现得像大鱼。
所以,我别无选择,只能重新设计我的核心以产生这些结果,使用连续的 ID。重新设计后,我减少了源代码,因为我现在不需要保留在运行时生成的所有信息,但图标从 DLL 导出时,原始图标 ID 会丢失。总之,普通开发人员很少会使用这些 ID。
我仍然想知道这是否是这些产品完全支持 ICL 的错误实现,或者 NE 格式文件是否存在一个规则,规定不能存储具有“随机”ID 的资源。到目前为止,我所有的研究都得出结论,您可以在 NE 格式中使用任何 ID 来表示 ICONIMAGES
。
PE 格式 (DLL, EXE, OCX, CPL, SRC)
PE 格式表示可移植可执行文件;此格式由 Microsoft 创建,用于支持 Windows 的 32 位和 64 位版本,取代了 16 位 Windows 版本中使用的 NE 格式。
基本上,EXE、DLL、OCX、CPL、SCR 等文件格式彼此之间差别不大。例如,可以将 EXE 视为一个带入口点的 DLL。在处理资源时,所有这些文件都是相同的。这意味着如果库支持 PE 格式,那么它就支持所有上述扩展名。
由于 Win32 API 已经支持 PE 格式的资源处理,因此没有必要原生支持这种文件格式,而是 IconLib 利用 Win32 API 来访问图标资源。
唯一原生功能是读取 PE 文件中的第一组标头,以检测要加载的文件是否为 PE 格式。
如果我们只想访问资源,那么最好的方法是将库加载为 DATAFILE
。这意味着库中不会执行任何代码,而是 Win32 API 只访问资源数据。
hLib = Win32.LoadLibraryEx(fileName, IntPtr.Zero, LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE);
IconLib
核心只支持读写流。
MultiIcon
重载了一些函数,如
Load
/Save
,并从文件系统中的文件创建一个 FileStream
,然后再调用 Load(stream)
。Win32 API LoadLibrary
只能从文件系统加载库;因此,流将被保存到临时文件中,然后才能调用 Win32 API
LoadLibraryEX
。
当资源按正确的顺序访问时,访问资源是一项简单的任务。
IconLib
首先调用
Win32.EnumResourceNames
,并将
GROUP_ICONS
作为参数传递,该参数返回每个图标的 ID。此 ID 可以是数字或指向字符串的指针。如果返回值小于 65535,则为数字;如果值大于 65535,则为指向字符串的指针。
一旦我们获得所有图标的 ID,我们就会调用 Win32.FindResource
函数。对于找到的每个 ID,它会返回一个资源句柄,然后我们可以继续加载和锁定资源以访问资源条目。这些条目包含已加载/锁定的每个图标内的每个图像的 ID。现在我们重复之前的步骤,但不是使用常量代码>GROUP_ICONS,而是使用 RT_ICON
。这告诉 Win32 API 我们要访问图标内的图像。
这是需要完成的关键步骤。在 Windows XP 或更早的操作系统下,在锁定图标图像资源后,我们将获得一个指向 ICONIMAGE
的指针。此图标图像将包含 BITMAPINFOHEADER
、调色板、XOR
Image
和 AND
图像(蒙版),但在 Windows Vista 中,它返回一个指向
PNG
图像的指针,这基本上是当前图标编辑器(包括最流行的编辑器)会崩溃的主要原因。它们会分配大量内存或直接丢弃图像,因为它们会将 PNG
图像像 BMP
图像一样解析。
IconLib
通过读取图像的第一个字节并检测图像签名,在读取和解析图像之前创建适当的编码器实例来解决此问题。
当 IconLib
需要创建
DLL
时,到目前为止最好的方法是使用一个空的 DLL
作为模板,并将资源添加到其中。
Win32 API 提供了三个 API 来完成这项工作。
BeginUpdateResources
UpdateResources
EndUpdateResources
MSDN 告诉我们,您可以调用 BeginUpdateResources
,然后根据需要多次调用 UpdateResources
,文件此时不会被写入。最后调用
EndUpdateResources
,更改将被提交到
DLL
。
这种方法对于小型 DLL
文件来说效果很好。当 IconLib
创建包含超过 80 个图像的库时,一切都很好,但调用
EndUpdateResources
总是失败。经过多次不成功的尝试,我能想到的唯一一件事是,更新资源的 API 有一个内部缓冲区。当该缓冲区已满时,调用
EndUpdateResoruces
将无法将更改提交到
DLL
。
我找到的变通方法是平均每提交 70 次更新一次,这效果很好,但极大地增加了更新 DLL
的时间。因此,除非我能找到 Win32 这样做的原因,否则我将尝试提出自己的 PE 格式实现,并且根本不使用 Win32。这将大大加快进程。
您可以在 Microsoft 网站上找到有关 PE 格式的更多信息,网址为 Microsoft Portable Executable and Common Object File Format Specification
Windows Vista 图标支持
我想创建一个没有限制的图标处理库,所以支持 Windows Vista 是必须的。
在 Windows XP 中,他们引入了带 alpha 通道和 48x48 像素的图标。在 Windows Vista 中,Microsoft 引入了 256x256 像素的图标图像。图标内的此图像在未压缩格式下可能需要 256KB,蒙版另外需要 8KB。这会显著增加图标库的大小,基本上解决了以压缩格式存储图像的问题。
使用的压缩是 PNG(Portable Network Graphic),因为它没有专利,支持透明度(Alpha 通道)并采用无损数据压缩。
与未压缩的位图相比,平均而言,压缩率是原来的 3 到 5 倍。
如果您认为差别不大,那么请加载 Windows Vista 中 Windows\System32 的文件 imageres.dll(11MB),对所有图像进行循环,并将编码器设置为 BMP 而不是 PNG,然后将其保存到 DLL 或 ICL 文件。您会注意到 DLL 大约是 45MB,ICL 大约是 54MB。这就是 PNG 真正发挥作用的地方。
为了存储压缩图像,他们想出了一个保持向后兼容性的方法,那就是将 BITMAPINFOHEADER
中的 biCompression
字段设置为 BI_PNG
而不是 BI_RGB
。此标头已经从 Windows 3.1 开始支持,而 BI_PNG
字段从 Windows 95 开始支持,但他们打破了兼容性,而是单独存储图像。(请参阅“哦,Microsoft 的兼容性策略在改变?”)。
示例仅包含两个图像,但最多可以有 65535 个。
尽管在我所有的研究中,我只看到了 PNG 格式的 256x256 图像,但这并不意味着它不能将所有图像存储为 PNG。这只是一个为了与早期 Windows 版本兼容而做的决定。
我个人认为图标编辑器应该支持任何尺寸和位深度的 PNG。图标不仅被 Windows 操作系统使用,Windows 图标也允许引入非标准图像,如 128x96x24,而 Windows 永远不会使用它。
如果您正在创建一个 Windows Vista 将要使用的图标,则仅为 256x256 图像存储 PNG 压缩。
智能类/结构
更难的部分是如何构建清晰的代码和 API,能够理解不同的图标格式和图标库以及不同的图像压缩,而不会造成混乱的 switch/if/else。
在我创建库的过程中,我从头开始重新设计了核心 3 次,并且仍然需要进行 TODO 更改以避免 IconImages
对象了解不同的压缩方法。基本上,
IconImage
对象不应负责了解要读取/写入的图像的格式。相反,它应该依赖于不同的编码器来了解此信息。
现在,IconImage
有一个指向
ImageEncoder
对象(基类)的引用,但
IconImage
对象仍然负责检测图像签名,以确定是创建
BMPEncoder
还是
PNGEncoder
实例。
此外,还有一些更改可以更有效地管理内存分配,但这不会改变核心设计。
回到我称之为智能类/结构的东西:基本上,一个图标是分层结构,图标库也是如此,但包含更多一层信息。
这些智能类/结构的目的是避免在不同对象之间交换数据;相反,每个类/结构都应该能够读取和写入自身。如果一个类或结构包含更多类或结构,那么它应该要求子级读取/写入该部分信息,依此类推。
如果您打开源代码,您会立即注意到参数 'Stream stream
' 无处不在。这允许接收此参数的对象在流的当前位置读取/写入自身。
例如,当需要创建 Icon
文件时,MultiIcon
对象将打开一个
FileStream
并调用 ImageFormat.Save(stream)
,将刚刚打开的流作为参数传递。
ImageFormat
对象将仅包含写入自身逻辑,并依赖于不同的类/结构来写入其余信息。
ImageFormat.Save(stream) { ICONDIR.write(stream) { Write iconDir header } Loop for each IconImage { ImageEntry.Write(stream) { Write iconEntry header } Image.Write(stream) { BitmapInfoHeader.Write(stream) { Write bitmap info header } Write Color Palette Write XOR image Write AND image } } }
这是一个简单的例子,但更复杂的案例,如读取 ICL(图标库),也遵循相同的行为。
因此,遵循这种模型,读写不同格式变得非常容易。它还产生了一个超级、干净的代码。
图像编码器
我希望提供一个易于理解且足够灵活以适应任何图像格式的库。理想情况是创建一个具有基本功能的类,并将特定格式的实现留给其他类。
ImageEncoder
对象保存一个图标图像的所有信息,并包含图像属性、调色板、图标图像和图标蒙版等信息。
这个类是一个抽象类,无法实例化。
BMP 编码器
BMPEncoder
类包含读取和写入图标条目的逻辑,当 biCompression
为 BI_RGB
(BMP)时。
PNG 编码器
PNGEncoder
类包含读取和写入图标条目的逻辑,当图像为 PNG 格式时。
我参考了不同来源的信息来创建带 PNG 压缩的图标。到目前为止,该实现没有问题,PNG 格式的图标图像可以用所有支持 Windows Vista 的图标编辑器打开。
Icon
库是另一回事。到目前为止,我还没有找到任何开源或商业图标编辑器,包括最流行的图标编辑器,它们允许打开或编写带 PNG 压缩的图标库,如 ICL 或 DLL。在一些商业产品中,您会注意到 PNG 图标未加载,并且如果您创建 PNG 图标,它们会在保存到 DLL 或 ICL 之前被解压缩。我认为这是因为 Microsoft 尚未发布任何相关信息,公司正在等待最终的 Windows Vista 发布。
我基于在 Windows Vista RC2 上进行逆向工程和遵循 Microsoft 程序员使用的图标文件逻辑来创建压缩图标库(ICL、DLL)。
IconLib
能够加载 Windows Vista DLL/EXE 和 CPL 文件(包括 PNG 格式)中的所有图标。它还允许编写带 PNG 压缩的 ICL/DLL 图标库。
坏消息是,目前只有您可以使用 IconLib
加载它们。如果您尝试加载包含
IconLib
生成的 PNG 图像的 ICL 或 DLL,并尝试使用第三方图标编辑器打开它,您会看到 PNG 图标丢失了,并且图标也包含来自其他图标的图像。到目前为止,我所有的研究都得出结论,这些产品对 ICL 库的 PNG 格式存在误实现,与
IconLib
无关。
现在,如果您想知道我怎么能确定 IconLib
正确生成 ICL 或 DLL?
基本上,如果您尝试在任何 Visual Studio 版本(包括 Orcas,如果您询问 VS2005)中打开包含 26x256 PNG 图标的 Windows Vista 图标,它将显示一个大小约为 2573x1293 的图像,采用 XP 格式。当然,该图像不存在,您也无法编辑它,但这就是 Visual Studio 的看法。
现在,如果您加载一个由 IconLib
生成的包含 256x256 PNG 文件的 DLL,并将包含 PNG 图像的图标保存到文件系统,然后在 Visual Studio 中打开图标图像,您会注意到与 Windows Vista 的 DLL 相同的行为。
总之,所有工作都基于推测,并且在任何 Windows Vista 库图标编辑器上市或 Microsoft 发布更多相关信息之前,我无法确定。
颜色减少和调色板优化
通常有一些程序允许从位图创建图标,它们会生成带有 alpha 通道(透明度)的图标,与 Windows XP 兼容,但这些图标缺少对低分辨率图像的支持。Iconlib 允许添加低分辨率图像,并包含一个完整的命名空间来从高分辨率图像生成低分辨率图像。
IconLib 使用的技术是
- 调色板优化
- 颜色减少
- 抖动
调色板优化
调色板是 RGB 颜色数组,大多数情况下调色板的长度是支持的颜色数量,调色板可以包含任何长度,但在大多数情况下,调色板是 256 或 16 个索引。
优化的调色板是根据要处理的位图创建的;它将分析输入图像并创建一个新的调色板,其中包含输入图像中最常用的颜色,可能使用多种方法来创建优化的调色板。
为什么要在位图上使用调色板?
调色板中的每个索引都是一个 RGB 颜色,需要 3 个字节来创建颜色(1 字节用于红色,1 字节用于绿色,1 字节用于蓝色),这允许创建 1600 万种颜色的组合,因为每个通道都可以产生 256 种颜色的渐变,然后 256R * 256G * 256B = 16777216 种颜色组合。
如果在位图数据中存储 RGB 信息,那么至少需要 3 个字节来存储每个像素颜色。
相反,索引位图只存储一个指向颜色数组的索引;这意味着位图数据不包含颜色信息,而是包含指向调色板的索引。
这可以节省大量空间,但图像质量可能会严重受损,因为非索引图像中非常相似的颜色在索引图像中将被转换为相同的颜色(索引)。
位图中存储了更多数据,但仅举例说明,让我们比较 3 个位图的大小。
100x100 像素 24 bpp 图像
1 像素 = 3 字节
100x100x3 = 30000 字节用于存储颜色信息。
100x100 像素 8bpp 索引图像
1 像素 = 1 字节
1 调色板 = 256 个 RGB 颜色索引 = 256x3 = 768
100x100x1 + 768 = 10768 字节用于存储颜色信息。
100x100 像素 4bpp 索引图像
1 像素 = 1/2 字节
1 调色板 = 16 个 RGB 颜色索引 = 16x3 = 48
100x100x1/2 + 48 = 5048 字节用于存储颜色信息。
拥有低分辨率索引图像并使其看起来仍然很好,关键在于选择正确的颜色来构成调色板,可以使用不同的调色板。
系统调色板:这是默认的 Windows 调色板,包含 256 种颜色,具有广泛的颜色。IconLib 不使用此调色板,因为例如,如果颜色减少的图标具有许多渐变,当这些渐变转换为索引像素版本时,其中许多将具有相同的索引,图像的质量将大大降低。
精确:如果图像包含少于 256 种颜色,则这些颜色将直接映射到调色板。
Web:是 Windows 和 Mac OS 调色板的交集,包含 216 种在 Windows 或 Mac OS 系统上使用的安全颜色。
自适应:此调色板根据颜色频率减少位图中的颜色;例如,如果您的图像主要包含肤色,则自适应颜色调色板将主要包含肤色。
感知:此调色板着重于减少位图中的颜色,以达到我们最敏感的颜色。
选择性:选择性调色板将从位图中选择颜色到 Web 安全颜色。
自定义:可以提供自定义调色板。
IconLib 使用自适应算法和 Octtree 结构来创建优化的调色板。
颜色减少
颜色减少背后的想法是获取一个 32 位(ARGB)或 24 位(RGB)图像,其中每个像素的数据包含 RGB 颜色信息,并将其转换为索引图像,这些图像称为索引图像,因为每个像素数据不包含 RGB 颜色信息,而是包含一个指向调色板(颜色数组)的索引,此调色板存储 n 个颜色,32 位和 24 位图像可以产生 1600 万种颜色,每个像素存储为 3 字节(第 4 字节用于 alpha 通道)。因为索引只存储指向调色板的索引,所以存储需求取决于图像分辨率。
非索引 32 位(1600 万色加透明度)= 每像素 4 字节
非索引 24 位(1600 万色)= 每像素 3 字节
索引 8 位(256 色)= 每像素 1 字节
索引 4 位(16 色)= 每像素 1/2 字节或每字节 2 像素
索引 1 位(黑白)= 每像素 1/8 字节或每字节 8 像素
在 IconLib 中,颜色减少算法与调色板优化算法非常接近。
在将像素转换为索引像素之前,必须有一个调色板可用以选择正确的颜色索引。
颜色减少过程中可以使用不同的调色板。
参见上面的调色板优化。
我在颜色选择中使用的算法是欧几里得距离,基本上它在调色板中找到最近邻的颜色,它将图像中的当前颜色与调色板中的颜色进行映射,找到当前颜色与 3D 空间中邻近颜色之间的最短距离。
抖动
即使在使用优化的调色板进行颜色减少的过程中,结果图像看起来可能不佳,尤其是在输入位图包含大量渐变的情况下,为了改善图像的外观,使用了抖动。
抖动是将两种颜色的像素并列以产生第三种颜色存在的错觉的过程,基本上在此过程中添加了噪声,此噪声与像素之间的不同颜色间隙成正比。
有许多算法可以实现抖动,并且输出图像因算法而异,我个人喜欢 Floyd-Steinberg 算法,因为生成的噪声均匀分布,产生漂亮的图像。
无抖动:输出位图中不添加噪声。
有三种抖动
噪声抖动:它实际上不能作为生产方法接受,但它非常简单易懂且易于实现。对于图像中的每个值,只需生成一个随机数 1..256;如果它大于该点的图像值,则将该点绘制为白色,否则绘制为黑色。
有序抖动:有序抖动添加具有特定幅度的噪声模式,对于图像中的每个像素,使用对应位置的模式值作为阈值。不同的模式可以产生完全不同的抖动效果。
错误扩散:将量化误差扩散到相邻像素。
Floyd-Steinberg 抖动:它是一种错误扩散抖动算法,并且是 IconLib 中使用的算法,它基于误差分散。对于图像中的每个点,首先找到可用的最接近的颜色。计算图像中的值与您拥有的颜色之间的差异。现在将这些误差值分成并分布到您尚未访问的相邻像素上。当您到达这些后续像素时,只需添加从早期像素分布的误差,如果需要,将值裁剪到允许的范围内,然后继续如上所述。
在下面的示例中,它将图像从 24bpp 源图像减少到 8、4 和 1bpp。
IColorQuantizer colorReduction = new EuclideanQuantizer(new OctreeQuantizer(), new FloydSteinbergDithering());
Bitmap bmp = (Bitmap) Bitmap.FromFile("c:\\Pampero.png");
Bitmap newBmp = colorReduction.Convert(bmp, PixelFormat.Format8bppIndexed);
newBmp.Save("c:\\Pampero 8.png", ImageFormat.Png);
newBmp = colorReduction.Convert(bmp, PixelFormat.Format4bppIndexed);
newBmp.Save("c:\\Pampero 4.png", ImageFormat.Png);
newBmp = colorReduction.Convert(bmp, PixelFormat.Format1bppIndexed);
newBmp.Save("c:\\Pampero 1.png", ImageFormat.Png);
|
|
|
|
24 位 RGB 1600 万色 |
8 位 256 色 Floyd-Steinberg 抖动 |
4 位 16 色 Floyd-Steinberg 抖动 |
1 位黑白 Floyd-Steinberg 抖动 |
可扩展颜色处理命名空间
对于大多数使用 IconLib 的应用程序,ColorProcessing 命名空间提供了创建高质量图标所需的所有工具,但由于颜色减少的算法很多,因此它通过接口实现,这意味着如果需要,可以扩展库以使用不同的算法。
对于颜色减少,有一个接口 IColorQuantizer
,它由默认类 EuclideanQuantizer
实现。
对于调色板优化,有一个接口 IPaletteQuantizer
,它由默认类 OctreeQuantizer
实现。
对于抖动,有一个接口 IDithering
,它由默认类 FloydSteinbergDithering
实现。
任何这些接口都可以实现,并且可以替换默认设置。
例如,如果开发人员实现了噪声或随机抖动算法,那么颜色减少初始化可能如下所示
IColorQuantizer colorReduction = new EuclideanQuantizer(new OctreeQuantizer(), new NoiseDithering());
自动图标创建
即使使用几行代码 IconLib 也可以从单个图像创建包含多个图像的图标,但 IconLib 提供了一个特殊 API,可以从单个输入图像创建完整的图标。
MultiIcon mIcon = new MultiIcon();
SingleIcon sIcon = mIcon.Add("Icon1");
sIcon.CreateFrom("c:\\Clock.png", IconOutputFormat.FromWin95);
CreateFrom
是 SingleIcon
类上公开的一个方法,该方法将接受一个必须是 256x256 像素且必须是 32bpp(必须包含 alpha 通道)的输入图像,最适合此方法的是为 PhotoShop 或任何图像编辑软件创建的 PNG24 图像。
API 中的第二个参数是一个标志枚举,它针对我们要创建图标的操作系统,在上一个示例中,它将接受输入图像并创建以下 IconImage 格式。
256x256x32bpp (PNG 压缩)
48x48x32bpp
48x48x8bpp
48x48x4bpp
32x32x32bpp
32x32x8bpp
16x16x32bpp
16x16x8bpp
有 14 种可能的枚举定义,但它们可以组合以获得开发人员想要的任何格式。
此方法利用整个库为每种格式提供最佳的 IconImage。
哦,Microsoft 的兼容性策略在改变?
我不得不评论一下,因为我认为这是 Microsoft 通常做事方式的一个突破,从我的角度来看。
我在 Windows 平台开发了十年,从 Windows 3.1 到现在,我在 Microsoft API 中看到的一个特点是版本之间惊人的兼容性。个人认为很多 Win32 API 如此内在和复杂,是因为它们必须保持向后兼容性,而过去几年我因此遇到了很多麻烦。
例如,Windows 未来一代的巨大障碍是 GDI,它施加了一套绝不能打破的规则,GDI+ 有所帮助,但仍然遵循 GDI 规则,这就是为什么有些事情 Windows 以前一直无法做到,直到现在。
当我想实现 Windows Vista 图标支持时,就发生了这种情况。
我读到 Windows Vista 图标是 256x256,并且它们使用 PNG 压缩。
起初,我 100% 确信他们会保持向后兼容性,所以我想知道他们是怎么做到的。我首先想到的是,Microsoft 的程序员会使用 BITMAPINFOHEADER
标头中的 biCompression
字段,并将其设置为 BI_RGB
(BMP)。他们会使用 BI_PNG
(PNG),该字段已经得到了标头的支持,调色板将是空的,
XOR
和 AND
图像将包含 PNG 数据。
我惊讶地发现事实并非如此。相反,他们完全放弃了拥有 BITMAPINFOHEADER
、图像(XOR
)和蒙版(AND
)的概念。相反,图标目录指向一个 100% PNG 结构。
起初我以为,“我的天哪,他们做了什么!”
这将破坏所有现有的图标编辑器,Visual Studio 和资源编辑器也无法再打开 ICO 文件,但当我坐下来思考时,我意识到这是正确的做法。
开发人员一直抱怨某些 Win32 API 有多么复杂,而这一次 Microsoft 听到了这一点,并做对了。
如果他们能够保持兼容性,那么 ICO 和 ICON 库现在将有 3 个地方包含图像的冗余信息。
例如 ICONDIRENTRY
、BITMAPINFOHEADER
和 PNGHEADER
,通常在 Win32 API 中会发现这些奇怪的东西。
相反,现在他们有一个图标目录条目指向图像本身。这样,他们为未来实现不同的图像或压缩方式打开了大门。ICO 文件仍然受到最大 256x256 像素的限制,因为图标目录以两个字节类型字段 bWidth
和
bHeight
存储宽度和高度。可能可以使用多个平面来解决这个问题。但仍然,我们离使用大于 256x256 像素的图标还有很长的路要走。
所以这次我祝贺 Microsoft 的程序员们,“以最佳方式做事”胜过一切。
如果您想知道这是否意味着 VS2005 或任何 VS 将无法正确打开 Windows Vista 的 ICO 或 DLL,那么您是对的,它不会。我也测试了 ORCAS(VS2006),它不支持。但这可以通过 Visual Studio 的补丁轻松解决,希望很快就会出现,否则您将拥有像本库这样的产品,它将支持 Windows Vista 图标。
路线图
IconLib
是一个强大的库,用于创建和修改图标或图标库。我计划在下一个版本中支持 DLL 和 EXE 的更新,允许在其中替换/添加/删除图标。
IconLib
本身只对编程语言有用。因此,我还计划创建一个高级图标编辑器应用程序,以充分利用 IconLib
。这可能是我在未来几个月内发表的下一篇文章。
如果我能获得 .icc(图标集合)、Icns、RSC、bin(mac)等文件格式,我将支持它们。如果您知道某种文件格式并拥有其内部文件结构,请告诉我,我将尝试实现它。
如果有人有兴趣创建一个开源的图标提取器和编辑器,那么欢迎使用 IconLib
作为文件格式引擎,我可以为 IconLib
提供支持。
历史
IconLib 0.73 (2008/01/31)
- 修复了带索引的 8bpp 图像的一个小问题。
- 正确处理添加 PNG24 图像。
- 从 PNG 或 BMP32 自动创建图标,支持 Vista、XP、W95 和 Win31。
- 添加了一个新的命名空间“ColorProcessing”,支持
- 颜色减少
- 抖动
- 调色板优化
- 允许将 IconImage 保存为带透明度的 PNG 或 BMP32。
- SingleIcon.Add() 方法现在返回新创建的 IconImage 的引用。
- 一些代码和方法签名发生了变化,但向后兼容。
- 演示应用程序允许导出 XOR、AND 和透明图像,现在 IconImages 也可以导出为 PNG24 或 BMP32。
IconLib 0.72 (2006/11/02)
- 将 ICL 库的默认移位因子从 9 更改为 10。(现在支持最大 64MB 的 ICL 文件大小)。
- 使用指针和 memcopy 重新编码函数以对黑白图像进行垂直翻转(提高了性能)。
- 位图编码器和库格式的不同命名空间。
- 删除了库格式的静态类,并替换为接口,不同的格式从中实现。
- 包含
IconLib
许可证类型。
IconLib 0.71 (初始发布)
许可证
这项工作根据 Creative Commons Attribution-Share Alike 3.0 Unported License. 获得许可。