从Vant图标的CSS文件提取图标文件

文章目录

环境

  • 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 来指定图标,例如:

html 复制代码
<van-icon name="chat-o" />

本例中, chat-o 是Vant图标的名称。Vant通过名字来找到图标。

使用图片URL:

仍然是使用 name 来指定图标,只不过指向的是一个图标文件,例如:

html 复制代码
<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

接下来,就可以在页面上使用图标了:

html 复制代码
<!-- 通过 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 图标对应的源码如下:

html 复制代码
<i class="van-badge__wrapper van-icon van-icon-chat-o"><!----><!----><!----></i>

正如前面所述,它的原理是CSS找到类,然后通过 ::before 伪元素在此处添加指定的自定义字符(即图标)。所以从页面的源码也看不到图标的具体信息。

我试着自己搭了一个Vant页面环境,效果也是一样的:

  • 页面上无法下载图标文件
  • HTML源码里没有图标的链接或其它信息

分析

既然页面上没办法,那就从Vant的源码着手吧。

安装Vant组件:

powershell 复制代码
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 base64

def 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包:

powershell 复制代码
pip install fontTools

pip install pillow

pip install brotli

\e653 为例,代码如下:

python 复制代码
from fontTools.ttLib import TTFont
from PIL import Image, ImageFont, ImageDraw
import io

def main():
    # 打开字体文件,这里假设你已经将 base64 编码的字体文件解码为 woff2 并保存为 font.woff2
    font = 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次或者多次
    • \\\ 是特殊字符,需要通过 \ 来转义

运行结果如下:

powershell 复制代码
('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 里提取图标,具体步骤为:
    1. 获取所有图标的名称(如 van-icon-arrow-double-left ,其实是CSS的类名称)和编号(如 e653
    2. 解码base64数据
    3. 根据以上信息,将每个字符(即图标)绘制出来
    4. 绘制图像时,可以指定颜色
    5. 将绘制的图像保存为图标文件

具体实现,参见上面的Python代码。

参考

  • https://vant-ui.github.io/vant/#/zh-CN/ / https://vant-ui.github.io/vant-weapp/
  • https://vant-ui.github.io/vant/#/zh-CN/icon / https://vant-ui.github.io/vant-weapp/#/icon
  • https://github.com/vant-ui/vant-demo
  • https://github.com/youzan/vant
相关推荐
曲幽8 小时前
我用FastAPI接ollama大模型,差点被asyncio整崩溃(附对话窗口实战)
python·fastapi·web·async·httpx·asyncio·ollama
代码匠心9 小时前
AI 自动编程:一句话设计高颜值博客
前端·ai·ai编程·claude
_AaronWong10 小时前
Electron 实现仿豆包划词取词功能:从 AI 生成到落地踩坑记
前端·javascript·vue.js
cxxcode10 小时前
I/O 多路复用:从浏览器到 Linux 内核
前端
用户54330814419410 小时前
AI 时代,前端逆向的门槛已经低到离谱 — 以 Upwork 为例
前端
JarvanMo10 小时前
Flutter 版本的 material_ui 已经上架 pub.dev 啦!快来抢先体验吧。
前端
恋猫de小郭11 小时前
AI 可以让 WIFI 实现监控室内人体位置和姿态,无需摄像头?
前端·人工智能·ai编程
哀木11 小时前
给自己整一个 claude code,解锁编程新姿势
前端
程序员鱼皮11 小时前
GitHub 关注突破 2w,我总结了 10 个涨星涨粉技巧!
前端·后端·github
UrbanJazzerati11 小时前
Vue3 父子组件通信完全指南
前端·面试