图标文件规范 
警告!此规范内容是不稳定版本,可能会发生破坏兼容性的更新。当无法保障向下兼容时,将会升级此文档的主版本号,如从“1.0”更新到“2.0”。反之,普通更新只会升级次版本号,如“1.0”更新到“1.1”,其对“1.0”版本向下兼容。请在使用前确认此文档的版本号,并为将来可能发生的兼容性变化做好准备。
- 维护者:zccrs zhangjide@uniontech.com
- 版本:1.1
- 修改日期:2022.11.21
- 议题:#5
引言 
本规范约定了各类图标文件的内部数据格式要求,以及此类文件的使用规范。
归档打包 
dci 文件(以下简称为 dci)为一种归档打包格式,其规定了如何将多个图标文件合并为一个文件。
格式 
- 格式名称:dci,为 DSG combined icons 的缩写
- mimetype:image/dci,继承自:application/octet-stream
- magic:DCI,类型为 string,offset 为 0
- 此类型对应的图标名:image-dci
- 后缀名:dci
对应的 mimetype 描述文件内容:关于 mimetype 规范的详细设计请查看 http://www.freedesktop.org/standards/shared-mime-info
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
   <mime-type type="image/dci">
      <comment>DSG Icon file format</comment>
      <comment xml:lang="zh_CN">深度图标文件格式</comment>
      <glob pattern="*.dci"/>
      <icon name="image-dci"/>
      <magic priority="80">
        <match value="DCI" type="string" offset="0"/>
      </magic>
   </mime-type> 
</mime-info><?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
   <mime-type type="image/dci">
      <comment>DSG Icon file format</comment>
      <comment xml:lang="zh_CN">深度图标文件格式</comment>
      <glob pattern="*.dci"/>
      <icon name="image-dci"/>
      <magic priority="80">
        <match value="DCI" type="string" offset="0"/>
      </magic>
   </mime-type> 
</mime-info>文件头 
dci 使用小端序,文件的元数据位于文件开头。内容如下:
| 起始地址:结束地址 | 名称 | 大小 | 缺省值 | 
|---|---|---|---|
| 0x0000:0x0003 | magic | 4B | DCI | 
| 0x0004:0x0004 | 版本 | 1B | - | 
| 0x0005:0x0007 | 文件数量 | 3B | - | 
| 0x0008:0x0057 | 文件1元数据 | 72B | 格式 | 
| ... | 文件元数据 | - | - | 
这里的文件数量指根文件,不包含目录内的子文件。
文件顺序需按照其名称升序排列,此排列不区分文件类型。另外,需遵循自然排序(如“a2”在“a11”之前)规则。
文件的元数据 
| 相对起始地址:相对结束地址 | 名称 | 大小 | 缺省值 | 
|---|---|---|---|
| 0x0000:0x0000 | 文件类型 | 1B | 可选值 | 
| 0x0001:0x003F | 文件名 | 63B | - | 
| 0x0040:0x0048 | 内容的大小 | 8B | - | 
| 0x0049:0x???? | 内容 | - | - | 
文件名为 UTF-8 编码,必须以 '\0' 结尾,且不可包含 '/' 字符。
目录的内容是:此目标内子目录以及文件的元数据和数据。
| 值 | 类型 | 
|---|---|
| 0 | 保留 | 
| 1 | 文件 | 
| 2 | 目录 | 
| 3 | 链接 | 
目录文件也属于文件类型的一种,可在目录下嵌套子目录。
目录内的文件顺序需按照其名称升序排列,此排列不区分文件类型。另外,需遵循英文环境(无国家要求)下的自然排序(如“a2”在“a11”之前)规则。
链接文件 
链接的目标可以是“文件”或其它(不可链接自身)“链接”,否则认为此链接目标无效。创建链接时不检查链接的目标是否存在,对链接文件内容的读写操作等价于读写链接的目标,如若链接目标无效或不存在,则读写失败。获取链接的目标路径时,如果链接文件无效,需给出一个空路径。此外,需要提供方法以允许获取链接路径的原始数据。
链接文件的内容格式:
| 相对起始地址:相对结束地址 | 名称 | 大小 | 缺省值 | 
|---|---|---|---|
| 0x0000:0x???? | 目标路径 | 取决于元数据中指定的内容大小 | - | 
内容编码为 UTF-8,路径需符合 UNIX 标准,可使用相对路径。相对路径的 "." 和 ".." 仅可用于路径的开头,当"."、".."出现在路径中间时,如 "/./test.txt"、"../a/../",此处的"."和".."需当作文件名称处理。
图标文件规范 
在归档打包中描述了 dci 文件的归档规范,本章节将描述 dci 格式的文件目录结构和命名规范。
图标状态 
图标适应环境变化时所需要展现的不同形态,分为以下四种状态:
- normal:缺省状态
- disabled :被禁用时的状态,一般表现为不再响应鼠标、键盘等输入事件
- hover:鼠标悬浮在图标上的状态
- pressed:点击后且未释放时的状态
当图标只提供
normal状态的文件时,可使用一些固定规则对normal状态进行加工而得到其它状态,此规范不约束加工后的效果。
图标动画 
如果对应状态的图片文件具有动画效果,如嵌入了动态的 webp 图片,则应当在图标状态变化时播放动画。动画播放需满足以下规则:
- 初始化状态不自动播放动画,初始为 normal 状态时显示动画第一帧,其它状态显示动画的最后一帧;
- normal 状态动态图的第一帧和最后一帧要等价,即内容完全相同;
- 当从 normal 状态变为 hover 状态时,使用 1.0 倍速正序播放 hover 图片的动画,且停留到最后一帧;
- 当从 hover 状态变为 pressed 状态时,使用 1.0 倍速正序播放 pressed 图片的动画,且停留到最后一帧;
- 当从 normal 状态变为 pressed 状态时,如果存在 hover 状态,则界面效果(仅影响UI,不实际改变图标的当前状态)先变化为 hover 状态,再由 hover 状态变化为 pressed 状态,在此期间的动画使用 2.0(如果 hover 状态是静态图片,则使用 1.0 倍速)倍速正序播放,不同动画图片切换时中间无停顿。否则,如果 hover 状态不存在,则直接使用 1.0 倍速正序播放 pressed 图片的动画。动画播放完毕后停留到最后一帧;
- 当从 hover 状态变为 normal 状态时,使用 1.0 倍速倒序播放 hover 图片动画,不停留到最后一帧,动画播放完毕后当作初始化状态停留到 normal 状态的图片;
- 当从 pressed 状态变为 hover 状态时,使用 1.0 倍速倒序播放 pressed 图片动画,不停留到最后一帧,动画播放完毕后当作初始化状态停留到 hover 状态的图片;
- 当从 pressed 状态变为 normal 状态时,如果存在 hover 状态,则界面效果(仅影响UI,不实际改变图标的当前状态)先变化为 hover 状态,再由 hover 状态变化为 normal,在此期间的动画使用 2.0(如果 hover 状态是静态图片,则使用 1.0 倍速)倍速倒序播放,不同动画图片切换时中间无停顿。否则,如果 hover 状态不存在,则直接使用 1.0 倍速倒序播放 pressed 图片的动画。动画播放完毕后当作初始化状态停留到 normal 状态的图片;
- 当从任意状态变为 disabled 状态时,界面效果(仅影响UI,不实际改变图标的当前状态)先变为(直接变化,不播放任何动画)初始化时的 normal 状态,再使用 1.0 倍速正序播放 disabled 图片的动画,动画播放完毕后停留到最后一帧;
- 当从 disabled 状态变为任意状态时,使用 1.0 倍速倒序播放 disabled 图片的动画,最后停留在 normal 状态(仅影响UI,不实际改变图标的当前状态)对应的图片;
- 只有最后一个播放的动画允许循环播放,如从 normal 状态变为 pressed 状态,顺序播放 hover 和 pressed 对应的动画时,hover 的动画不允许循环播放;
- 当需要播放动画时,如果当前已经有动画正在播放,如果待播放的动画是当前动画的逆播放,则暂停当前动画,且从暂停的帧开始倒序播放。否则如果待播放的动画是当前动画的延续(即可以衔接到一起,如从 normal 状态变为 hover 状态,在播放动画期间又变为了 pressed 状态),则等到当前动画播放完毕后接着播放新动画,且当前动画禁止重复播放。除此之外的情况则停止当前动画,立即开始播放新动画;
- normal 状态的动画用于需要突显此图标的情况,由使用方决定在合适的时机播放。一些使用场景:某个图标刚被新增到某个地方显示时可以播放 normal 的动画,比如新安装一个应用,这个应用的图标出现在 Launcher 上时,或者是把一个应用图标发送到 Dock 或 Desktop 上面时;当一个应用最小化到任务栏时,任务栏上可以播放 normal 状态的动画。
伪代码实现:
enum Flags {
    Continue = 1,
    InvertedOrder = 2,
    IgnoreLastImageLoop = 4,
    AllowNonLastImageLoop = 8
};
fn playImages(images, speed, flags)
{
...
}
fn playAnimation(oldMode, newMode)
{
    if (oldMode == Normal) {
        if (newMode == Normal) {
            showImage(Normal);
        } else if (newMode == Hover) {
            if (hover.supportsAnimation()) {
                playImages({hover}, 1.0);
            } else {
                showImage(Hover);
            }
        } else if (newMode == Pressed) {
            if (pressed.supportsAnimation()) {
                if (hover.supportsAnimation()) {
                    playImages({hover, pressed}, 2.0);
                } else {
                    playImages({pressed}, 1.0);
                }
            } else {
                showImage(Pressed);
            }
        } else if (newMode == Disabled) {
            if (disabled.supportsAnimation()) {
                playImages({disabled}, 1.0, Flags::IgnoreLastImageLoop);
            } else {
                showImage(Disabled);
            }
        }
    } else if (oldMode == Hover) {
        if (newMode == Normal) {
            if (hover.supportsAnimation()) {
                setFinishedImage(Normal);
                playImages({hover}, 1.0, Flags::InvertedOrder | Flags::IgnoreLastImageLoop);
            } else {
                showImage(Normal);
            }
        } else if (newMode == Pressed) {
            if (pressed.isNull()) {
                if (hover.supportsAnimation()) {
                    playImages({hover}, 1.0, Flags::InvertedOrder | Flags::IgnoreLastImageLoop);
                } else {
                    showImage(Normal);
                }
                return;
            }
            if (pressed.supportsAnimation()) {
                playImages({pressed}, 1.0);
            } else {
                showImage(Pressed);
            }
        } else if (newMode == Disabled) {
            if (disabled.isNull()) {
                showImage(Normal);
                return;
            }
            if (disabled.supportsAnimation()) {
                playImages({disabled}, 1.0, Flags::IgnoreLastImageLoop);
            } else {
                showImage(Disabled);
            }
        }
    } else if (oldMode == Pressed) {
        if (newMode == Normal) {
            if (!pressed.supportsAnimation()) {
                showImage(Normal);
                return false;
            }
            setFinishedImage(Normal);
            if (hover.supportsAnimation()) {
                playImages({hover, pressed}, 2.0, Flags::InvertedOrder | Flags::IgnoreLastImageLoop);
            } else {
                playImages({pressed}, 1.0, Flags::InvertedOrder | Flags::IgnoreLastImageLoop);
            }
        } else if (newMode == Hover) {
            if (pressed.isNull()) {
                if (hover.supportsAnimation()) {
                    playImages({hover}, 1.0);
                } else {
                    showImage(Hover);
                }
                return;
            }
            if (pressed.supportsAnimation()) {
                ensureHoverModeLastImage();
                finishedImage = hoverModeLastImage;
                playImages({pressed}, 1.0, Flags::InvertedOrder | Flags::IgnoreLastImageLoop);
            } else {
                showImage(Hover);
            }
        } else if (newMode == Disabled) {
            if (disabled.isNull()) {
                showImage(Normal);
                return;
            }
            if (disabled.supportsAnimation()) {
                playImages({disabled}, 1.0, Flags::IgnoreLastImageLoop);
            } else {
                showImage(Disabled);
            }
        }
    } else if (oldMode == Disabled) {
        if (disabled.supportsAnimation()) {
            setFinishedImage(Normal);
            playImages({disabled}, 1.0, Flags::InvertedOrder | Flags::IgnoreLastImageLoop);
        } else {
            showImage(Normal);
        }
    }
    return;
}enum Flags {
    Continue = 1,
    InvertedOrder = 2,
    IgnoreLastImageLoop = 4,
    AllowNonLastImageLoop = 8
};
fn playImages(images, speed, flags)
{
...
}
fn playAnimation(oldMode, newMode)
{
    if (oldMode == Normal) {
        if (newMode == Normal) {
            showImage(Normal);
        } else if (newMode == Hover) {
            if (hover.supportsAnimation()) {
                playImages({hover}, 1.0);
            } else {
                showImage(Hover);
            }
        } else if (newMode == Pressed) {
            if (pressed.supportsAnimation()) {
                if (hover.supportsAnimation()) {
                    playImages({hover, pressed}, 2.0);
                } else {
                    playImages({pressed}, 1.0);
                }
            } else {
                showImage(Pressed);
            }
        } else if (newMode == Disabled) {
            if (disabled.supportsAnimation()) {
                playImages({disabled}, 1.0, Flags::IgnoreLastImageLoop);
            } else {
                showImage(Disabled);
            }
        }
    } else if (oldMode == Hover) {
        if (newMode == Normal) {
            if (hover.supportsAnimation()) {
                setFinishedImage(Normal);
                playImages({hover}, 1.0, Flags::InvertedOrder | Flags::IgnoreLastImageLoop);
            } else {
                showImage(Normal);
            }
        } else if (newMode == Pressed) {
            if (pressed.isNull()) {
                if (hover.supportsAnimation()) {
                    playImages({hover}, 1.0, Flags::InvertedOrder | Flags::IgnoreLastImageLoop);
                } else {
                    showImage(Normal);
                }
                return;
            }
            if (pressed.supportsAnimation()) {
                playImages({pressed}, 1.0);
            } else {
                showImage(Pressed);
            }
        } else if (newMode == Disabled) {
            if (disabled.isNull()) {
                showImage(Normal);
                return;
            }
            if (disabled.supportsAnimation()) {
                playImages({disabled}, 1.0, Flags::IgnoreLastImageLoop);
            } else {
                showImage(Disabled);
            }
        }
    } else if (oldMode == Pressed) {
        if (newMode == Normal) {
            if (!pressed.supportsAnimation()) {
                showImage(Normal);
                return false;
            }
            setFinishedImage(Normal);
            if (hover.supportsAnimation()) {
                playImages({hover, pressed}, 2.0, Flags::InvertedOrder | Flags::IgnoreLastImageLoop);
            } else {
                playImages({pressed}, 1.0, Flags::InvertedOrder | Flags::IgnoreLastImageLoop);
            }
        } else if (newMode == Hover) {
            if (pressed.isNull()) {
                if (hover.supportsAnimation()) {
                    playImages({hover}, 1.0);
                } else {
                    showImage(Hover);
                }
                return;
            }
            if (pressed.supportsAnimation()) {
                ensureHoverModeLastImage();
                finishedImage = hoverModeLastImage;
                playImages({pressed}, 1.0, Flags::InvertedOrder | Flags::IgnoreLastImageLoop);
            } else {
                showImage(Hover);
            }
        } else if (newMode == Disabled) {
            if (disabled.isNull()) {
                showImage(Normal);
                return;
            }
            if (disabled.supportsAnimation()) {
                playImages({disabled}, 1.0, Flags::IgnoreLastImageLoop);
            } else {
                showImage(Disabled);
            }
        }
    } else if (oldMode == Disabled) {
        if (disabled.supportsAnimation()) {
            setFinishedImage(Normal);
            playImages({disabled}, 1.0, Flags::InvertedOrder | Flags::IgnoreLastImageLoop);
        } else {
            showImage(Normal);
        }
    }
    return;
}缩放倍数 
为适应不同的 DPI,图标大小的缩放倍数分类以下三类:
- 1:无缩放(1倍)
- 2:放大2倍
- 3:放大3倍
色调类型 
为适应不同的明暗色,图标按色调可分为以下两类:
- light:用于亮色环境
- dark:用于暗色环境
一个图标可能包含 图标大小 x 图标状态 x 色调类型 x 缩放倍数 x 图层数 个图片文件,以名称为 dde 的图标举例,封装为 dci 格式后的目录结构如下:
├── 16
|    ├── disabled.light
|    │   ├── 1
|    │   │   └── 1.0.png
|    │   ├── 2   
|    │   │   └── 2.0.jpg
|    │   └── 3
|    │       └── 3.0.webp
|    │
|    └── normal.light
│           ├── 1
│           │   ├── 1.0.png
│           │   └── 2.1.png
│           ├── 2   
│           │   ├── 1.0.png
│           │   └── 2.1.png
│           └── 3
│               ├── 1.0.png
│               └── 2.1.png
│   
├── 32
│    └── hover.light
│          ├── 1
│          │   └── 1.0.png
│          └── 1
│              ├── 1.0.png 
│              └── 2.1.png 
└── 512
     └── pressed.dark
            ├── 1
            │   └── 1.0.png
            ├── 2   
            │   └── 2.0.png 
            └── 3
                └── 3.0.png├── 16
|    ├── disabled.light
|    │   ├── 1
|    │   │   └── 1.0.png
|    │   ├── 2   
|    │   │   └── 2.0.jpg
|    │   └── 3
|    │       └── 3.0.webp
|    │
|    └── normal.light
│           ├── 1
│           │   ├── 1.0.png
│           │   └── 2.1.png
│           ├── 2   
│           │   ├── 1.0.png
│           │   └── 2.1.png
│           └── 3
│               ├── 1.0.png
│               └── 2.1.png
│   
├── 32
│    └── hover.light
│          ├── 1
│          │   └── 1.0.png
│          └── 1
│              ├── 1.0.png 
│              └── 2.1.png 
└── 512
     └── pressed.dark
            ├── 1
            │   └── 1.0.png
            ├── 2   
            │   └── 2.0.png 
            └── 3
                └── 3.0.png- 一级目录按 图标大小创建
- 二级目录按 图标状态.色调类型创建
- 三级目录的名称格式为:缩放倍数,,可以存放多个缩放比系数来适配系统的缩放比。例如上述示例:缩放比系数在1和2之间时,选择 2,大于 2 时,选择 2 ,默认情况下选择 1。目前提供的图标中,为了保证尽量减少图标文件的大小,仅保存缩放比为 3 的文件。在软件层面进行缩放。
- 四级目录中是图片文件,描述图标图片文件(图层)所携带的信息。这些信息中,包含:图层优先级、外边框数值、调色板格式、颜色调整数值、图层格式。可以通过下述文件表示:

目录/文件名中各项变量的可选值:
| 变量 | 可选值 | 备注 | 
|---|---|---|
| 图标大小 | 整数 | 假设查找的图标大小为 16,则优先查找大于等于 16 的图标,无法找到时,可使用最大的图标资源,且可以忽略缩放倍数,以使用大小最匹配的图标资源 | 
| 图标状态 | normal、disabled、hover、pressed | normal状态的light图标必须存在,其它状态为可选,查找图标时,如果对应状态不存在,则允许使用normal.light状态代替(fallback) | 
| 色调类型 | light、dark | 查找图标时色调类型必须完全匹配 | 
| 缩放倍数 | 1、2、3 | 至少要提供此三类缩放比例的图标,其它更高的缩放为可选,如目标缩放倍数的图片不存在,则可以选用更高倍数的图,如无更高倍数的图,则可以使用低倍数中较高的图片 | 
| 图层 | 整数 | 优先级代表图层的绘制顺序,从 1-n 进行绘制,最底层的图层会被上层图层覆盖。 | 
| 外边框 | 整数 | 外边框(padding):外边框在有阴影效果的图标中充分利用。padding 代表图层外围不被控件大小覆盖的区域。 | 
| 调色板 | 整数: -1,0,1,2,3 | 当无调色板时,该部分的数值为空。调色板格式分为5种,无(无需指定)、前景色(0)、背景色(1)、高亮前景色(2)、高亮色(3) | 
| 色调/饱和度/亮度/rgba调整参数 | 整数: -100 ~ 100 | 用于调节图片填充颜色的色调/饱和度/亮度/rgba等参数的百分比,可以通过 -100~100的值调整0~255的颜色取值范围(如:调色板中颜色的亮度为50,这里亮度调整参数为20时,调整后亮度为50 + (255 - 50) * 20% = 91。 亮度调整参数为-20时调整后亮度为50 - 50x20% = 40)。当三原色调整参数为-100时,颜色为 黑色,参数为100时,颜色为 白色 。主题填充颜色来自调色板中的基础颜色。 | 
| 后缀名 | png、jpg、webp | 图片格式后缀,如果图层存在调色板格式,就意味着可以优化其大小,转化为 alpha8格式,使用.alpha8作为后缀,进行标识,在软件渲染时进行进行复原。注意webp格式的图片显示需要安装对应格式的插件( 如qt5-image-formats-plugins) | 
|  | ||
| dci-demo.zip |