引言
最近在做 H5 小游戏的开发,与 App 不同,由于 H5 所有的资源都是通过 CDN 获取的,考虑到网络资源加载速度的问题,优化资源显得格外重要。因此,图片资源的压缩也是必不可少的。
起源
起初,我们在 windows 下是通过一个叫做 PNGoo 的 GUI 工具来实现图片资源批量压缩的。但考虑压缩资源还需要启动一个应用,将图片资源拖进去再开始压缩,显然不够智能,希望通过 python 脚本自动完成。
后来,找到了这个工具的 Github 源码 pngoo ,才发现这个工具是基于 pngquant 这个开源库实现的,类似的基于此压缩算法库工具还有 Pngyu (Github 源码:Pngyu)和网页版压缩工具的 TinyPng 。
重点是这个开源的压缩库压缩比达到 60%-80% ,也是相当可观了。
源码资源
python 实现
直接下载 win 和 mac 平台的命令行工具包:
-
Binary for Windows (v2.12.0)
-
Binary for macOS (v2.12.0)
具体实现步骤如下:
-
遍历指定目录下所有的 .png 后缀的文件;
-
根据是否覆盖源文件进行压缩处理。
核心的方法有:
-
getImages
:这个方法用来获取需要进行压缩的图片
# 获取文件列表 def getImages(): print u"========= 开始遍历图片" global file_list files = os.listdir(PngSrcRoot) for file in files: # 过滤出 png 图片 if os.path.isdir(file): print u"过滤掉文件目录:"+file else: endStr = os.path.splitext(file)[1] if endStr == file_end: if isBack(file): print u"过滤掉黑名单中的文件:"+file else: file_list.append(file) # print u"文件 " + file + u" 添加到压缩列表"
假如需要获取子目录下的图片资源,可以改写成递归调用的方式,改成如下即可很简单:
def getImages(path, recursion): global file_list files = os.listdir(path) for file in files: # 过滤出 png 图片 if os.path.isdir(file): getImages(path+'/'+file, recursion) else: endStr = os.path.splitext(file)[1] if endStr == file_end: if isBack(file): print u"过滤掉黑名单中的文件:"+file else: file_list.append(path+'/'+file)
使用递归遍历的方式需要保存完整的文件路径(绝对路径或相对路径),非递归可直接保存文件名即可。
-
compress
:这是压缩文件的方法,当然要根据是否覆盖源文件做区分处理:
# 压缩一个图片 def compress(fileName): srcPath = PngSrcRoot + '/' + fileName outPath = SaveRoot + '/' + fileName if SaveToOriginalDir: # 使用 .png 后缀,且通过 -f 覆盖源文件 cmd = PngquantExe + " -f --ext "+ file_end + " " + srcPath + " --quality " + compress_quality os.system(cmd) return else: # 默认压缩到当前目录下,并加上 '-fs8.png' 后缀 cmd = PngquantExe + " --ext "+ file_temp_end + " " + srcPath + " --quality " + compress_quality os.system(cmd) # 复制到文件夹 fileOriginalName = os.path.splitext(fileName)[0] compressed_srcpath = PngSrcRoot + '/'+fileOriginalName + file_temp_end if os.path.exists(compressed_srcpath): if os.path.exists(outPath): os.remove(outPath) shutil.move(compressed_srcpath, outPath) #移动文件
覆盖源文件的直接使用命令参数
-f
或--force
即可,保存到另外目录下的使用-fs8.png
做后缀,移动到目标地址时再改名即可。当然也可以直接使用-o
参数,达到一样的效果,省去了移动文件的操作。
完整的脚本如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import os.path
import shutil
import sys
# 压缩结果是否覆盖源文件
SaveToOriginalDir=True
SelfPath = sys.path[0]
# 压缩工具
PngquantExe=SelfPath+".\pngquant\pngquant" # 参考 https://pngquant.org/ 工具来实现的
# 工程根目录
PathWorkspaceRoot = os.path.abspath(
os.path.join(os.path.dirname(__file__), ".."))
print u"当前工作目录: "+PathWorkspaceRoot
# 压缩资源目录
PngSrcRoot=PathWorkspaceRoot+"../../resource/@ui"
# 压缩后存放的目录
SaveRoot=PathWorkspaceRoot+"../../resource/@ui_pressed"
# 压缩过的图片列表
CompressFilesRecord=PngSrcRoot+'/compress_record.txt'
# 黑名单(不需要压缩的图片)
Backlits=[
'NetworkTips_atlas0.png',
'Common_atlas0.png',
'BldgUpgrade_atlas0.png'
]
# 文件后缀名
file_end='.png'
file_temp_end='-fs8.png'
# 压缩品质范围
compress_quality='75-80'
# 文件列表
file_list=[]
# 清理旧文件
def initDir():
global SaveRoot
if(SaveToOriginalDir):
if os.path.exists(CompressFilesRecord):
print u"图片已经压缩过了!"
return
SaveRoot = PngSrcRoot
else:
if os.path.exists(SaveRoot):
print u"压缩文件存放目录清空"
shutil.rmtree(SaveRoot)
print u"创建压缩文件存放目录:"+SaveRoot
os.makedirs(SaveRoot)
# 获取文件列表
def getImages():
print u"========= 开始遍历图片"
global file_list
files = os.listdir(PngSrcRoot)
for file in files:
# 过滤出 png 图片
if os.path.isdir(file):
print u"过滤掉文件目录:"+file
else:
endStr = os.path.splitext(file)[1]
if endStr == file_end:
if isBack(file):
print u"过滤掉黑名单中的文件:"+file
else:
file_list.append(file)
# print u"文件 " + file + u" 添加到压缩列表"
# 开始图片压缩任务
def startCompress():
print u"========= 开始压缩图片"
record_file = open(CompressFilesRecord,'w')
if not os.path.exists(CompressFilesRecord):
print u"创建压缩文件日志文件"
for file in file_list:
print u"压缩图片:"+file
compress(file)
record_file.write(file+'\n')
record_file.close()
def main():
initDir()
getImages()
startCompress()
print u"========= 图片压缩完成"
# 判断是否在黑名单中
def isBack(filePath):
for i in Backlits:
if(filePath.find(i) != -1):
return True
return False
# 压缩一个图片
def compress(fileName):
srcPath = PngSrcRoot + '/' + fileName
outPath = SaveRoot + '/' + fileName
if SaveToOriginalDir: # 使用 .png 后缀,且通过 -f 覆盖源文件
cmd = PngquantExe + " -f --ext "+ file_end + " " + srcPath + " --quality " + compress_quality
os.system(cmd)
return
else: # 默认压缩到当前目录下,并加上 '-fs8.png' 后缀
cmd = PngquantExe + " --ext "+ file_temp_end + " " + srcPath + " --quality " + compress_quality
os.system(cmd)
# 复制到文件夹
fileOriginalName = os.path.splitext(fileName)[0]
compressed_srcpath = PngSrcRoot + '/'+fileOriginalName + file_temp_end
if os.path.exists(compressed_srcpath):
if os.path.exists(outPath):
os.remove(outPath)
shutil.move(compressed_srcpath, outPath) #移动文件
if __name__ == '__main__':
main()
sys.exit(0)
执行方式可以在脚本目录下执行:
$ python 脚本名称.py
假如使用 Visual Studio Code 的话,可以直接添加一个任务:
-
在
task.json
中的"tasks"
添加一个任务:{ "label": "压缩 UI 图片", // 压缩 ui 图片 "type": "shell", "presentation": { "echo": true, "reveal": "always", "focus": true, "panel": "shared" }, "command": "python", "args": [ "${workspaceRoot}/subproj/png图片压缩工具/ImgCompress.py" // 上面压缩脚本的相对路径 ], "group": "build", "problemMatcher": [] }
-
执行时在 VS Code 使用快捷键
Ctrl+Shift+P
换出任务列表,选择压缩 UI 图片
即可开始执行上面的压缩脚本。
pngquant
相关参数:
-
--quality min-max
:min 和 max 是从 0-100 的数值,用于设置压缩后图片的品质,品质越高压缩率越低;如果转换后的图片比最低品质还低,就不保存,并返回错误码99
-
--ext new.png
:设置输出图片的后缀名,默认使用
-fs8.png
做后缀(防止与源文件重名),假如设置-ext=.png
则需要带上--force
参数,否则会提示输出文件与输入文件重名无法覆盖; -
-o out.png
或--output out.png
:压缩后图片的输出路径设置参数,不设置则默认输出当源文件相同路径下;
-
--skip-if-larger
:假如压缩后的图片文件比源文件还大,则放弃压缩结果;
-
--speed N
:转换速度与品质的比例。1(最佳品质),10(速度最快),默认是3;
-
--nofs
:禁用
Floyd–Steinberg dithering
(即基于错误扩散的抖动算法)效果。而另外一个参数
--floyd=0.5
则用于控制抖动的等级,取值范围0-1
,0表示无抖动(等价于--nofs
),1表示满级,这里=
符号是必须的; -
--posterize bits
:按位数减少调色板的精度。当图像在低深度屏幕上显示时使用(例如,16位显示或压缩的纹理在ARBB44格式);
-
--strip
:不要复制可选的 PNG 块。在MAC(使用Cocoa reader)时,元数据总是被删除。