文章目录
- 环境
- 背景
- 基础用法
- 使用图片URL:
- 自定义图标
- 问题
- 分析
- 步骤
- 步骤1:解码
- 步骤2:提取图标
- 步骤3:批量提取图标
- 完整代码和用法
- 总结
- 参考
环境
- Windows 11
- Python 3.13.1
- Vant 4.9.15
- NPM 11.0.0
背景
我需要一些图标文件,在网上搜来搜去,后来找到了Vant组件( https://vant-ui.github.io/vant/#/zh-CN/
)。
Vant组件的图标: https://vant-ui.github.io/vant/#/zh-CN/icon
注:关于如何安装和使用Vant,可以参考我另一篇文档( https://blog.csdn.net/duke_ding2/article/details/136131991
)。写的时间有点久了,不过还是能work的。
Vant图标大致有3种用法:
- 基础用法
- 使用图片URL
- 自定义图标
基础用法
直接通过 name
来指定图标,例如:
<van-icon name="chat-o" />
本例中, chat-o
是Vant图标的名称。Vant通过名字来找到图标。
使用图片URL:
仍然是使用 name
来指定图标,只不过指向的是一个图标文件,例如:
<van-icon name="https://fastly.jsdelivr.net/npm/@vant/assets/icon-demo.png" />
自定义图标
引入第三方iconfont对应的字体文件和CSS文件,之后就可以在Icon组件中直接使用。例如下面的CSS文件:
css">/* 引入第三方或自定义的字体图标样式 */
@font-face {font-family: 'my-icon';src: url('./my-icon.ttf') format('truetype');
}.my-icon {font-family: 'my-icon';
}.my-icon-extra::before {content: '\e626';
}
注: .ttf
(TrueType Font)是一种字体格式。可以通过 @font-face
规则将 .ttf
字体文件嵌入到网页中,使网页能够显示自定义字体。
一种常见用法是利用 .ttf
文件来存储图标:将图标转换为字体的字形,作为字体的字符存储在 .ttf
文件中,并为每个图标分配一个唯一的字符编码(通常使用Unicode编码)。例如:第一个图标放在 \e001
位置,第二个图标放在 \e002
位置,以此类推。
::before
是一个CSS伪元素,它允许你在一个元素的内容之前插入生成的内容。这个生成的内容是虚拟的,不会出现在HTML结构中,而是通过CSS动态添加。本例中, content: '\e626';
表示在 .my-icon-extra
类的元素内容的前面插入一个字符,其Unicode编码为 \e626
。
接下来,就可以在页面上使用图标了:
<!-- 通过 class-prefix 指定类名为 my-icon -->
<van-icon class-prefix="my-icon" name="extra" />
这样,就会在该处插入指定的自定义字符,其编码为 \e626
(实际上是一个图标)。
事实上,Vant图标也是一种自定义字符。它的工作原理与上述做法类似。
问题
前面讲的都是Vant图标的用法和原理。但是问题在于,使用Vant组件,虽然能在页面上显示各种图标,还能添加一些效果(比如颜色,右上角显示小红点,等等,本质是对字体的装饰),但是貌似没有简单的方法能获取图标文件(比如 xxx.png
)。
例如,在Vant网站上,只有 icon-demo
这个图标可以通过点击右键,保存为png文件:
这是因为它使用了图片URL,它本来就是一个图标文件。而别的图标是字体,貌似没法直接下载成图标文件。
页面上无法下载图片,那在浏览器里看看页面的源码吧。如下:
上图中,页面中的 chat-o
图标对应的源码如下:
<i class="van-badge__wrapper van-icon van-icon-chat-o"><!----><!----><!----></i>
正如前面所述,它的原理是CSS找到类,然后通过 ::before
伪元素在此处添加指定的自定义字符(即图标)。所以从页面的源码也看不到图标的具体信息。
我试着自己搭了一个Vant页面环境,效果也是一样的:
- 页面上无法下载图标文件
- HTML源码里没有图标的链接或其它信息
分析
既然页面上没办法,那就从Vant的源码着手吧。
安装Vant组件:
npm i vant
注:安装的是 vant
,不是 @vant/weapp
,后者是微信小程序版。
安装好以后,可以发现 node_modules
目录下多了 vant
目录(以及其它几个目录)。其中, node_modules\vant\es\icon\index.css
(以及 node_modules\vant\lib\icon\index.css
)这个文件大小为45KB:
一个CSS文件这么大,显然是包含了图标的内容在里面。于是就从这里想办法,把图标搞出来。
打开这个CSS文件,其内容有两个关键点:
.van-icon-arrow-double-left:before{content:"\e653"}
、.van-icon-arrow-double-right:before{content:"\e654"}
……:前面提到了,这些是指定了类和要添加的字符编码。换句话说,每处就代表了一个图标base64,d09GMg......0AAA==
:这是一串很长的编码,代表图标的实际内容,使用了base64
编码。
所以,我们要做的就是,从这一长串编码里,把每个图标的内容提取出来,保存成图标文件。
过程中经历了不少调试和反复的过程,略过不表,只说最后总结。
步骤
步骤1:解码
把base64编码的内容解码,并保存为相应的字体文件格式(本例中为 woff2
)。
注: .woff2
(Web Open Font Format 2.0)也是一种字体文件格式,此处我们不做深究,认为它和 .ttf
文件类似就行了。
有很多方法可以对base64编码做解码。例如,使用Python实现:
python">import base64def main():# 这里是一个示例的 base64 编码的字体数据,你可以从 CSS 文件中提取相应的 base64 字符串替换它base64_font_data = "d09GMg......0AAA==" # 该编码非常长# 解码 base64 数据font_data = base64.b64decode(base64_font_data)# 将解码后的数据保存为 woff2 文件with open('font.woff2', 'wb') as f:f.write(font_data)if __name__ == "__main__":main()
运行代码,生成 font.woff2
文件。
步骤2:提取图标
接下来就要从该文件中提取出字符,也就是图标,然后保存成文件。
我使用了Python来实现。在做之前,需要先安装几个Python包:
pip install fontToolspip install pillowpip install brotli
以 \e653
为例,代码如下:
python">from fontTools.ttLib import TTFont
from PIL import Image, ImageFont, ImageDraw
import iodef main():# 打开字体文件,这里假设你已经将 base64 编码的字体文件解码为 woff2 并保存为 font.woff2font = TTFont('font.woff2')# 获取字体对象cmap = font.getBestCmap()# 获取 \uE653 对应的 glyph 名称glyph_name = cmap.get(0xE653)if glyph_name:# 获取字形信息glyph = font['glyf'][glyph_name]# 创建一个新的图像image = Image.new('RGBA', (200, 200), (255, 255, 255, 0))draw = ImageDraw.Draw(image)font_obj = ImageFont.truetype('font.woff2', 150)# 绘制字符draw.text((0, 0), chr(0xE653), font=font_obj, fill=(0, 0, 0))# 保存图像image.save('icon_e653.png', 'PNG')if __name__ == "__main__":main()
运行代码,就把 \e653
所指向的图标保存为 icon_e653.png
了。
同理,把 \e653
换成 \e654
,就提取了另一个图标:
步骤3:批量提取图标
要想批量提取图标,只需通过正则表达式来解析 index.css
文件,获取所有图标的名称与字符编号:
python">import re# 读取 CSS 文件
with open('index.css', 'r', encoding='utf-8') as file:css_data = file.read()# 去除前面的无用信息
css_data = re.findall(r'(.*){display:inline-block}(.*)', css_data)[0][1]# 使用正则表达式查找所有图标的 Unicode 编码
unicode_icons = re.findall(r'\.(.*?):before{content:\s*"\\(.*?)"', css_data)# 打印所有找到的 Unicode 编码
for icon in unicode_icons:print(icon)
注:简单分析一下正则表达式。
css_data = re.findall(r'(.*){display:inline-block}(.*)', css_data)[0][1]
re.findall()
函数:第1个参数是正则表达式,第2个参数是要处理的字符串。该函数在字符串中查找所有匹配正则表达式的子串,并将结果以列表的形式返回。(.*){display:inline-block}(.*)
:这是一个正则表达式,其中圆括号括起来的部分是要提取的,本例中有两处圆括号,所以列表中的每个元素会包含两个元素。- 本例中,列表只包含一个元素,而该元素(也是个列表)中,我们只需要第二个元素,所以使用了
[0][1]
。本质上,是以{display:inline-block}
为分隔符,保留分隔符之后的内容。
re.findall(r'\.(.*?):before{content:\s*"\\(.*?)"', css_data)
\.
:.
是特殊字符,需要通过\
来转义.*?
:与.*
的区别在于,它是非贪婪的\s*
:\s
表示任何空白字符(如空格、Tab、回车等),后面的*
表示0次或者多次\\
:\
是特殊字符,需要通过\
来转义
运行结果如下:
('van-icon-arrow-double-left', 'e653')
('van-icon-arrow-double-right', 'e654')
('van-icon-contact', 'e753')
......
('van-icon-discount-o', 'e6ab')
('van-icon-ecard-pay', 'e6ac')
('van-icon-envelop-o', 'e6ae')
这样,我们就得到了每个图标的名称和编号,接下来,只需和之前的代码结合起来,就可以批量提取图标文件了。
完整代码和用法
把上面的步骤结合起来,形成完整的代码:
python">import base64
import os
from pathlib import Path
import re
from fontTools.ttLib import TTFont
from PIL import Image, ImageFont, ImageDraw
import io# 读取 CSS 文件
css_file = 'index.css'
with open(css_file, 'r', encoding='utf-8') as file:css_data = file.read()# 去除前面的无用信息
css_data = re.findall(r'(.*){display:inline-block}(.*)', css_data)[0][1]# 使用正则表达式查找所有图标的 Unicode 编码
unicode_icons = re.findall(r'\.(.*?):before{content:\s*"\\(.*?)"', css_data)# base64编码
base64_font_data = re.findall(r'(.*?)base64,(.*?)\)(.*)', css_data)[0][1]
# print(base64_font_data)# 解码 base64 数据
font_data = base64.b64decode(base64_font_data)# 将解码后的数据保存为 woff2 文件
woff2_file = 'font.woff2'
with open(woff2_file, 'wb') as f:f.write(font_data)# 打开字体文件,这里假设你已经将 base64 编码的字体文件解码为 woff2 并保存为 font.woff2
font = TTFont(woff2_file)# 获取字体对象
cmap = font.getBestCmap()# 创建目标文件夹
output_dir = 'output'
if not os.path.exists(output_dir):os.makedirs(output_dir)# 批量提取图标
for icon in unicode_icons:# print(icon)# 获取 icon 对应的 glyph 名称glyph_name = cmap.get(int(icon[1], 16))if glyph_name:# 获取字形信息# glyph = font['glyf'][glyph_name]# 创建一个新的图像image = Image.new('RGBA', (200, 200), (255, 255, 255, 0))draw = ImageDraw.Draw(image)font_obj = ImageFont.truetype(woff2_file, 150)# 绘制字符draw.text((0, 0), chr(int(icon[1], 16)), font=font_obj, fill=(0, 0, 0)) # 通过fill参数指定颜色# 保存图像image.save(Path(output_dir) / (icon[0] + '.png'), 'PNG')
用法:保存代码为 test1.py
文件,需要同目录下有 index.css
文件。
运行代码,就会在当前目录下生成 font.woff2
文件和 output
目录。
打开 output
目录,里面存放了所有提取出的图标文件。
注意:在绘制图像( draw.text()
函数)时,使用的颜色是黑色( fill=(0, 0, 0)
)。如果希望改变图标的颜色,只需改变颜色的RGB值。
例如,想要生成蓝色图标,则需传入 fill=(0, 0, 255)
,效果如下:
其它颜色也同理。
总结
- Vant图标是一种自定义字体,每个图标对应一个字符编号。
- 图标的显示方式为,在HTML结构里指定类,在CSS文件里针对类,通过伪元素
::before
在页面插入指定字符(即图标)。 - 使用这种方法,在页面上以及页面源码里无法直接提取图标文件。
- 每个字符(即图标)的编号以及全部内容(base64编码)可以从
index.css
获取。 - 可以自己写代码,从
index.css
里提取图标,具体步骤为:- 获取所有图标的名称(如
van-icon-arrow-double-left
,其实是CSS的类名称)和编号(如e653
) - 解码base64数据
- 根据以上信息,将每个字符(即图标)绘制出来
- 绘制图像时,可以指定颜色
- 将绘制的图像保存为图标文件
- 获取所有图标的名称(如
具体实现,参见上面的Python代码。