从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
相关推荐
游客52014 分钟前
图像处理|闭运算
图像处理·人工智能·python·opencv·计算机视觉
wit_@15 分钟前
深入了解卷积神经网络(CNN):图像处理与深度学习的革命性技术
python·深度学习·机器学习·cnn·scikit-learn
迷雾漫步者1 小时前
React封装倒计时按钮
前端·react.js·前端框架
Channing Lewis1 小时前
python创建pdf水印,希望根据文本长度调整水印字体大小,避免超出页面
python·pdf
m0_672449604 小时前
基础vue3前端登陆注册界面以及主页面设计
前端·vue.js·elementui
匹马夕阳4 小时前
Vue3中使用组合式API通过路由传值详解
前端·javascript·vue.js
zpjing~.~4 小时前
VUE中css样式scope和deep
前端·css·vue.js
fxshy4 小时前
Vue3父子组件双向绑定值用例
前端·javascript·vue.js
风茫4 小时前
如何在vue中渲染markdown内容?
前端·javascript·vue.js