Linux 图标主题 & 图标查找

图标主题概述

Linux 环境的一个乐趣或者说自由就是可以很方便的进行各种自定义配置,图标主题也是个人定制中最经常修改的项目之一。最近阅读了 freedesktop 中有关方面的标准定义,研究了 Qt、gtk 等框架对图标主题的查找策略与优化手段,在此做一个知识梳理,也方便大家了解更多有关 Linux 图标主题的相关特性。

图标主题的意义在于给你的桌面提供一个风格相似的图标集合。当用于选定了某个图标主题作为他的偏好设置,桌面环境中的所有的应用程序都将使用这个主题中的图标,以提供统一的 UI 风格。

一个图标主题的标准结构很简单,它是由一个主题描述文件(index.theme)和一系列子目录组成。一句话概括,主题描述文件是一个用来指明主题的基本信息、引导你按规则去每个子目录寻找你想要的图标的向导文件。

Icon Theme

根据标准[1]定义,图标主题描述文件是一个 ini 格式的文本文件。文件的第一个描述段应当是[Icon Theme],它保存了以下几个基本信息:

  • Name 代表主题的名称
  • Comment 代表主题的描述
  • Directories 代表本主题存放图标的子目录列表
  • Inherits [可选] 代表本主题继承的主题列表
  • ScaledDirectories [可选] 和 Directories 类似,代表了可缩放的图标所在的子目录列表
  • Hidden [可选] 控制是否在用户可选的主题列表中显示,例如像 hicolor 这样的 fallback 主题,是不希望在控制中心这样的主题列表中出现的
  • Example [可选] 本主题的标志性示例图标

标准规定,所有存放图标的目录需要在 Directories 中指明。但有些主题制作的非常不标准,甚至于没有这个字段(虽然标准规定此字段是必须的)。或许由于太多不按标准的主题的存在,在如 Qt[3] 等一些图标查找逻辑中,通过直接遍历子目录来查找,没有读取这个字段。这样的实现是不利于图标查找逻辑快速的找到最合适的图标的。

Directory

在上面的 Directories 或 ScaledDirectories 中指定的每个图标子目录,都有一个单独的配置段来说明这个目录中的图标信息,它一般具有以下几个属性:

  • Size 代表本目录的图标大小
  • Scale 图标的缩放比例
  • Context 图标的分类,如 Actions/Devices/MimeTypes 等分类
  • Type 图标目录的类型,有 Fixed、Scalable、Threshold 三种,默认为 Threshold
    • Fixed 代表使用本目录的图标时,所需要的图标大小必须和本目录的 Size 相同
    • Scalable 代表图标匹配时,满足一定大小范围的图标需求,都可以使用本目录的图标,范围由 MaxSize 和 MinSize 指定,默认值均为目录的 Size 属性
    • Threshold 代表图标匹配时,需要的图标大小与本目录图标大小的差距小于某个阈值即可,由 Threshold 属性指定,阈值默认为 2

Icon Data

标准中定义了可选的,描述图标在图标合成、加工时所需要的信息,但是目前似乎并没有多少主题使用了这些特性。

要定义一个图标的描述数据,可以创建一个与图标同名的 .icon 文件,它可以包含以下几个描述字段:

  • DisplayName 用来详细描述图标的一个名称,通常是用于用户界面显示的文本。
  • EmbeddedTextRectangle 提供一个四元组的坐标,用于确定一个矩形区域,上层应用可以在这个指定区域内绘制文字,例如,Linux 下的许多发行版的文本文件图标是一个纸张的框图,内部绘制的是此文本文件真实的文本内容。此时文本的可绘制区域就由这个属性决定。
  • AttachPoints 是一个二元组列表,用于指示本图标在进行图标合成时最佳的放置位置。常用的场景是:有一个文件夹图标,需要在它的上方覆盖一个软链接的角标,表示它是一个软链接目录;或者在可执行程序图标上合成一个禁止图标,表示当前用户无权限执行此程序等等。此时作为装饰的图标如何在基础图标上放置,就可以参考这里的描述。

一份示例的 index.theme 解读

[Icon Theme]
Name=themed
Comment=Test icon theme
Inherits=parent
Directories=apps/16,apps/32,apps/48,apps/scalable

[apps/16]
Size=16
Context=Applications
Type=Fixed

[apps/32]
Size=32
MinSize=22
MaxSize=36
Context=Applications
Type=Scalable

[apps/48]
Size=48
Context=Applications
Type=Threshold

[apps/scalable]
MinSize=1
MaxSize=256
Context=Applications
Type=Scalable

这份文件定义了一个名为themed的图标主题,它继承于parent主题(标准规定图标查找不到时会最后在hicolor主题中查找,所以主题不应该继承hicolor),它含有 4 个图标目标:

  • apps/16 目录中的图标只可以用作 16×16 大小。
  • apps/32 目录中的图标大小为 32,但可以缩放至 [22, 36] 以匹配更多需求。
  • apps/48 目录中的图标可以匹配以 48 为基准,相差阈值在 2(Threshold 的默认值)以内的图标,即 [46, 50]。
  • apps/scalable 目录中的图标可以缩放到 [1, 256]。通常图标主题中都包含一个很宽范围的 Scalable 目录以弥补其它所有目录没有覆盖到的大小范围。

扩展

标准确定了一种自定义扩展的方法,例如 KDE、Gnome 都在标准的基础上添加了自己的“私货”,这些非标准的描述都应该以X-开头,如X-KDE Icon Theme或是X-Gnome Icon Theme。它们的具体细节与用法就不在这里过多介绍。

图标查找

当应用程序需要一个图标时,会有对应图标查找逻辑在系统所选的主题中查找对应图标。其基本思想是根据图标主题的描述,从文件系统中寻找“最佳”图标供上层应用使用。

图标查找的位置

按照优先级,图标查找应该从以下几个目录开始:

  • $HOME 如果存在此环境变量,需要查找 $HOME/.local/share/icons 和 $HOME/.icons
  • $XDG_DATA_DIRS 如果存在此环境变量,需要查找每个目录的 icons 子目录
  • $XDG_DATA_HOME 如果存在此环境变量,需要查找此目录的 icons 子目录
  • /usr/local/share/icons
  • /usr/share/icons
  • /usr/share/pixmaps

Best-fit 策略及 fallback-fit 机制

从上面的示例图标主题可以看出,不同的目录之前是可能存在覆盖范围重叠的情况的,例如当请求 16px 图标时,apps/16 中的图标与 apps/scalable 中的图标都是满足需求的。或者当主题没有提供大范围的 Scalable 目录时,还有可能出现有些图标大小没有被任何一个目录所覆盖的情况。此时就需要跨越图标目录的限制,在所有可能中选择一个“最接近”的图标。标准[1]中用下面的伪代码来描述不同目录与所需图标的“距离”,根据距离的最小值来确定最佳图标,即 Best-fit 策略。

DirectorySizeDistance(subdir, iconsize, iconscale) {
  read Type and size data from subdir
  if Type is Fixed
    return abs(Size*Scale - iconsize*iconscale)
  if Type is Scaled
    if iconsize*iconscale < MinSize*Scale
        return MinSize*Scale - iconsize*iconscale
    if iconsize*iconscale > MaxSize*Scale
        return iconsize*iconscale - MaxSize*Scale
    return 0
  if Type is Threshold
    if iconsize*iconscale < (Size - Threshold)*Scale
        return MinSize*Scale - iconsize*iconscale
    if iconsize*iconsize > (Size + Threshold)*Scale
        return iconsize*iconsize - MaxSize*Scale
    return 0
}

当然,还有一种情况是此主题中根本没有所需要的图标。此时就需要根据各种规则进行 fallback 了。

基于图标继承列表的 fallback

在当前主题中没有查找到所需图标时,应该按照继承顺序,从各个父主题中查找。需要特别说明的是,在所有继承主题中都查找不到图标时,应该在hicolor主题中查找。这是标准明确规定的,也因此我们自己创建的主题不需要显示的指定继承hicolor

同时,也是基于这个原因,在应用程序的安装列表中,一般也会把自己的默认图标安装在 hicolor 图标主题下,以保证无论用户选择的图标主题中是否提供了这个应用的图标,最后都至少可以通过 fallback 机制找到一个可用的图标。

基于名称分类的 fallback 策略

根据图标命名标准[4]-字符用于指定图标的详细分类,如input-mouse-usb,代表输入设备 - 鼠标输入设备 - usb 接口鼠标输入设备。显而易见,当主题没有提供此图标时,input-mouseinput可以作为一个接近的图标名称。即:基于图标名称的 fallback 策略使用-分割图标名称,寻找当前主题中更上层分类的图标作为目标图标,以提供更加统一的 UI 风格。

例如,当 dark 主题中的input-mouse-usb图标查找不到时,使用 dark 中的input-mouse图标代替要比 light 主题中的input-mouse-usb图标更加适合。(显然,本例中 dark 是不会继承于 light 的)

优先级:基于名称的 fallback 是在当前主题、当前主题的父主题列表、hicolor 中都找不到之后,才会进行的。 这在某些图标查找策略中被错误的实现了。之前遇到过在查找 dde-introduction 的图标时,由于在 deepin 主题中不包含此图标(应用的图标被安装在了 hicolor 主题目录中),但由于 deepin 主题同时包含了一个名为 dde 的图标,错误的 fallback 顺序使得目标图标寻找到了 dde 而不是 hicolor 中的 dde-introduction。

Name-fallback 的缺陷

关于这个大坑的一切都要从“具象化图标”和“书写方向”开始说起。

  • “-symbolic” 后缀所指代的具象化图标通常是一些小分辨率的、用于控件图标或托盘、标题栏装饰图标,例如电源、声音及网络指示图标,它们一般具有简单的线条、单一的色彩等较为扁平的风格。
  • “-ltr” “-rtl” 后缀用于区分不同书写方向的图标。例如,在某些主题中,输入框的删除文字图标可能为一个从右向左的箭头,这在从右向左书写的文字中可能会不协调(因为此时删除是从左到右进行的),此时主题会用这些后缀区分以提供不同方向的两种图标。

当我们请求寻找input-mouse-usb-symbolic这个具象化的 usb 鼠标图标时,查找了各个主题都没有找到,于是进行了 name-fallback 找到了input-mouse-usb这个图标,而这个图标很有可能是一个色彩丰富的、拟物化的图标。这样的图标用在托盘或控件上是非常不协调的。

一个更好的 name-fallback 方式是保留这些特殊的后缀,即名称查找按以下顺序进行 fallback:

input-mouse-usb-symbolic
input-mouse-symbolic
input-symbolic
input

当然,这只是其中一种解决方案。由于标准中并没有说明对于这种图标需要进行何种处理,在各个实现中对这种情况的处理都有各自选择。如在 gtk 的图标查找逻辑[5]中,提供了特殊的GTK_ICON_LOOKUP_FORCE_SYMBOLICflag 以强制请求 symbolic 图标。

Linux 下的“历史遗留问题”

在上面的查找目录列表中,可以看到像$HOME/.icons或是/usr/share/pixmaps这样的目录,在图标系统中这些目录显得很不协调,甚至里面的图标也并没有像图标主题目录那样按照图标特性进行分类。这是由于之前在标准没有确立或是广泛应用时所用的目录,为了前向兼容,在进行图标查找时也要“照顾”这些目录。

这些目录中的图标不仅图标命名混乱,甚至自有一套“约定俗成”的 fallback 机制,例:

app.png
app_32x32.png
my-app.32.png
my-app.64.png

在这种目录查找图标时,大小如何确定、如何 fallback 可谓是混乱之极,例如在查找my这个图标时,现在的有些实现会返回my-app.32.png作为结果(没错,它甚至 fallback 了目录中的文件名字来进行匹配)由此也说明了标准确立的重要性。

Icon Search 实战

得注意的是,图标查找顺序及各级 fallback 策略虽然有相关标准,但由于上述一些缺陷及标准中有些细节没有明确说明,在各个图标查找策略的实现中,具体细节是不尽相同的,这也是导致了不同平台、不同框架下图标查找的结果不相同的一个很大的原因。

例如,文件系统中有下面的目录结构,其中查找的目标主题 themed 即为上面的示例主题文件。同时,把$HOME环境变量设置为fake_home目录以演示多目录的情况:

├── fake_home
│   └── .local
│       └── share
│           └── icons
│               └── themed
│                   └── apps
│                       └── 16
│                           └── just-in-another-base.png
└── icons
    ├── hicolor
    │   ├── apps
    │   │   ├── 16
    │   │   │   └── TestAppIcon.png
    │   │   ├── 48
    │   │   │   └── TestAppIcon.png
    │   │   ├── 48@2
    │   │       └── TestAppIcon.png
    │   └── index.theme
    ├── parent
    └── themed
        ├── apps
        │   ├── 16
        │   │   ├── best-app.svg
        │   │   └── name.with.dot.png
        │   ├── 32
        │   │   └── best-app.svg
        │   ├── 48
        │   │   └── best-app.svg
        │   └── scalable
        │       └── best-app.svg
        └── index.theme
Icon Name Size Scale Result Rule
best-app 16 1 themed/apps/16/best-app.svg Fixed matched
best-app 20 1 themed/apps/scalable/best-app.svg Scalable [1, 256]
best-app 24 1 themed/apps/32/best-app.svg Scalable [22, 36]
best-app 48 1 themed/apps/48/best-app.svg Fixed matched
best-app 50 1 themed/apps/48/best-app.svg Threshold [46, 50]
best-app 51 1 themed/apps/scalable/best-app.svg Scalable [1, 256]
TestAppIcon 16 1 hicolor/apps/16/TestAppIcon.png Fixed matched in parent
TestAppIcon 64 1 hicolor/apps/48/TestAppIcon.png Best-fit in parent
TestAppIcon 48 2 hicolor/apps/48@2/TestAppIcon.png Fixed matched in parent
TestAppIcon 96 1 hicolor/apps/48@2/TestAppIcon.png Best-fit in parent
best 48 1 themed/apps/48/best-app.svg Name fallback + Fixed matched
best 50 1 themed/apps/48/best-app.svg Name fallback + Threshold [46,50]
just-in 16 1 just-in-another-base.png Name fallback + Fixed matched

查找过程的优化

按上文的查找方式可以很容易发现,查找图标的重点在于需要遍历的目录很多,而且大部分都是重复的工作,因此可以从缓存目录信息、缓存图标查找结果、提高查找速度等几个方面进行优化。

gtk-icon-cache

GTK 的 icon-cache 可以说是最经典,接受度最高的缓存文件了,甚至于 Qt 在图标主题查找中,也参考了这里的缓存信息[3]。它定义了一套缓存文件生成、图标查询、验证缓存失效的标准机制[2],详细的细节甚至并不比图标的标准描述差多少。不过,在现在的实现中,无论是gtk-update-icon-cache还是 gtk 的图标查找逻辑,都只使用了标准中的一部分特性,即根据图标名查找图标可能存在的目录列表。

gtk-icon-cache 的核心思想是:一个图标主题含有大量的子目录,而查找一个特定图标时,包含有这个图标文件的目录可能只有很少几个。那么,我们可以将本主题下所有图标分别存放在几个“桶”内,同时,包含了这个桶内所有图标的一个路径列表也和这个桶相关联。这样,在查询时,我们先通过一定的方法计算出图标所存放的桶,这个桶所对应的目录列表必然是小于或等于所有可能的目录列表的,以此达到减少目录遍历的目的。显然,能减少多少目录遍历,与桶的大小及桶所装载的图标的个数是相关的。在极端情况下,例如只有一个桶,它包含了所有图标,那么此缓存信息的查找效率将退化成无缓存的直接搜索。

由此可见,如何确定并分配这些桶是十分重要的。桶的数量过少,会导致桶内数据过于集中,优化不明显;桶的数量过多,则会导致缓存文件本身过大、解析起来耗费过多资源。在gtk-update-icon-cache程序中,桶的数量为主题当中图标数量的三分之一,并且为了方便进行哈希运算,选择最接近于这个数的素数作为最终桶的数量[6]

进行哈希映射的目的是为了让各个桶内存放的图标数量大致相同,标准规定了如下的哈希算法:

static guint
icon_name_hash (gconstpointer key)
{
  const signed char *p = key;
  guint32 h = *p;

  if (h)
    for (p += 1; *p != '\0'; p++)
      h = (h << 5) - h + *p;

  return h;
}

图标查找逻辑可以通过图标名称计算出它所存放的桶,并从桶中取出一个目录子集。通过查找子集而不是直接搜索文件系统,是可以很大程度上提升图标查找的效率的。

Replacement Cache

根据桌面应用程序的特点,在一定时间内,活跃的程序及所经常用到的图标总是一个很小的集合。一般会在图标查找逻辑甚至上层应用中安排一个 LRU/LFU Cache,以加速常用图标的查找。这在 gtk、Qt 的图标查找逻辑中都有相应的实现。

Parallel Search

综上所述,我们所需要的目标图标可能隐藏在各个主题、各个目录中,而在不同的目录进行图标查找几乎是互不相干的操作,因此可以很方便的使用现代 CPU 的多核心特性实现并行查找,以提升总体查找速度。

题外话:行动

说了这么多,肯定有人想问了:既然有这么多坑,~~作为东半球最好用的 Linux 发行版~~,我们为什么不自己造更好的图标查找的轮子呢?这里为大家安利几个项目:

  • themed-icon-lookup 一个 Linux 图标查找的实现,完全按标准实现,利用 Rust 语言的并发特性实现了并行图标查找。
  • gtk-icon-cache GTK icon cache 文件解析的 Rust 实现。
  • qt5integration 利用 QPA 层插件实现的 DDE 桌面风格定制项目。在图标查找中,默认使用 XdgIcon 查找图标,同时也添加了使用themed-icon-lookup作为可选查找后端的支持(commit-eb99c24)。

Benchmark: 利用 dde-launcher 进行测试,对 140 多个应用进行图标查找,总耗时从原来的 500ms 左右降低到 300ms 左右,并解决了之前图标查找的一些 bug。当然,保持图标查找的正确性是当下最优先的任务,更多的优化手段还在路上…

最后的总结

限于个人水平和文本篇幅,Linux 图标主题及图标查找相关的内容还有很多没有介绍,Icon Data 的具体用法、Linux 前向兼容目录的图标 fallback 查找及带缩放的图标查找等也仅仅是浅尝辄止。同时,在图标主题及图标查找的标准定义之外,KDE 及 Gnome 等桌面环境也做了许多自己的扩充,例如 KFramework 有一套更加复杂的缓存逻辑。在通过以后的不断学习,对图标主题有了更多更深入的理解之后,会再从这几个方面更全面的介绍图标主题中的一些相关知识。

引用资料

  1. Icon Theme Spec
  2. GTK+ Icon Cache Spec
  3. QIconLoader
  4. Icon Naming Spec
  5. gtkicontheme.h
  6. updateiconcache.c

发表评论

电子邮件地址不会被公开。 必填项已用*标注