├── MANIFEST.in ├── fast2webp ├── __init__.py └── __main__.py ├── .travis.yml ├── LICENSE ├── .gitignore ├── setup.py └── README.rst /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include MANIFEST.in 3 | include LICENSE -------------------------------------------------------------------------------- /fast2webp/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # __init__.py 5 | __version__ = "1.1.11" 6 | 7 | from fast2webp import * 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.6' 4 | install: 5 | - pip install setuptools 6 | script: true 7 | deploy: 8 | provider: pypi 9 | user: Mogeko 10 | password: ${PyPI_PASSWORD} 11 | on: 12 | python: 3.6 13 | tags: true 14 | branch: master 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 郑钧益 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | from setuptools import setup, find_packages 4 | 5 | # 只支持 Python 3.5+ 6 | if sys.version_info < (3, 5): 7 | sys.exit('Python 3.5 or greater is required.') 8 | 9 | 10 | PACKAGE = "fast2webp" 11 | NAME = "fast2webp" 12 | DESCRIPTION = "A Python script that converts image files in PNG, JPG, JPEG, BMP, GIF batch (recursively) to webp format" 13 | KEYWORDS = "webp" 14 | AUTHOR = "Mogeko" 15 | AUTHOR_EMAIL = "zhengjunyi@live.com" 16 | LICENSE = "MIT" 17 | PLATFORMS = "Linux" 18 | URL = "https://github.com/Mogeko/fast2webp" 19 | VERSION = __import__(PACKAGE).__version__ 20 | 21 | 22 | def readme(): 23 | with open('README.rst') as f: 24 | return f.read() 25 | 26 | setup( 27 | name=NAME, 28 | version=VERSION, 29 | description=DESCRIPTION, 30 | long_description=readme(), 31 | license=LICENSE, 32 | url=URL, 33 | author=AUTHOR, 34 | author_email=AUTHOR_EMAIL, 35 | keywords=KEYWORDS, 36 | 37 | packages=['fast2webp'], 38 | include_package_data=True, 39 | zip_safe=True, 40 | 41 | platforms=PLATFORMS, 42 | 43 | entry_points={ 44 | 'console_scripts': ['fast2webp=fast2webp.__main__:main'], 45 | }, 46 | 47 | classifiers=[ 48 | 'Development Status :: 3 - Alpha', 49 | # 3 - Alpha 50 | # 4 - Beta 51 | # 5 - Production/Stable 52 | 53 | 'Operating System :: POSIX :: Linux', 54 | 'Intended Audience :: Developers', 55 | 'Topic :: Software Development', 56 | 'Natural Language :: Chinese (Simplified)', 57 | 'License :: OSI Approved :: MIT License', 58 | 'Programming Language :: Python :: 3 :: Only', 59 | # 'Programming Language :: Python :: 3.4', 60 | 'Programming Language :: Python :: 3.5', 61 | 'Programming Language :: Python :: 3.6', 62 | 'Programming Language :: Python :: 3.7', 63 | ], 64 | project_urls={ 65 | 'GitHub': 'https://github.com/Mogeko/fast2webp', 66 | }, 67 | ) 68 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | fast2webp 3 | ========== 4 | 5 | .. Travis build - https://github.com/Mogeko/fast2webp 6 | 7 | .. image:: https://travis-ci.org/Mogeko/fast2webp.svg?branch=master 8 | :target: https://github.com/Mogeko/fast2webp 9 | :alt: Travis build 10 | 11 | .. PyPI version — https://pypi.org/project/fast2webp 12 | 13 | .. image:: https://img.shields.io/pypi/v/fast2webp.svg 14 | :target: https://pypi.org/project/fast2webp 15 | :alt: PyPI version 16 | 17 | .. Python version — https://pypi.org/project/fast2webp 18 | 19 | .. image:: https://img.shields.io/pypi/pyversions/fast2webp.svg 20 | :target: https://pypi.org/project/fast2webp 21 | :alt: Python version 22 | 23 | 24 | 25 | 一个将 PNG、JPG、JPEG、BMP、GIF 等格式的图像文件批量 (递归) 转换为 webp 格式的 Python 脚本 26 | 27 | 可以设定压缩级别 (0 ~ 100),数字越大品质越好。支持无损压缩。支持多线程。 28 | 29 | 可以在 GNU/Linux 上运行,Mac OS 估计也可以 (没测试过),Windows 上可能要修改一下才能用 (主要是目录地址中 ``/`` 与 ``\`` 的区别) 30 | 31 | 突然发现 ``webp`` 的工具包中有一个叫 ``img2webp`` 的工具 (没用过所以不知道QAQ),为了避免冲突现将项目改名为 ``fast2webp`` _(:з」∠)_ 32 | 33 | --------- 34 | 依赖 35 | --------- 36 | 37 | - Python3 38 | - webp 39 | 40 | --------- 41 | 安装 42 | --------- 43 | 44 | :: 45 | 46 | sudo pip3 install fast2webp 47 | 48 | --------- 49 | 使用 50 | --------- 51 | 52 | :: 53 | 54 | fast2webp [options] 55 | 56 | --------- 57 | 截图 58 | --------- 59 | 60 | .. image:: https://f.cangg.cn:82/data/201812132140465668.gif 61 | 62 | -------------- 63 | 参数 (options) 64 | -------------- 65 | 66 | :: 67 | 68 | usage: fast2webp [-h] [-i ] [-o ] [-q | -lossless][-t ] 69 | [-only {png,jpg,bmp,gif}] [-enable_gif] [-uncopy] 70 | 71 | -i .......... 需要转换的文件所在的目录,会递归进行转换 (默认:.[当前目录]) 72 | -o .......... *.webp 文件的输出目录,输出目录结构与输入目录保持一致 (默认:./output) 73 | 如果输出目录不存则会按照给定的路径新建文件夹 74 | 如果缺省,则会在当前目录中新建并输出到目录 'output' 75 | -q .......... 与 'cwebp' 中相同;表示压缩的程度 (0 ~ 100),数字越大品质越好 (默认:80) 76 | -t .......... 线程池中线程的个数;数字越大转换速度越快。当然,也更吃资源 (默认:10) 77 | -lossless ......... 与 'cwebp' 中相同;无损压缩 78 | -only ..... 只执行某单一任务 (可接受的值有:png、jpg、bmp、gif) 79 | 例如:-only png 表示只转换输入目录中的 png 文件为到输出目录 80 | -enable_gif ....... 将 gif 转为 webp (默认情况下会跳过 gif 文件) 81 | 转换 gif 文件比较吃资源,慎用 82 | 转换 gif 文件不支持无损压缩,此时 -lossless = -q 100 83 | -uncopy ........... 默认情况下会将非图片文件复制到输出目录中,使用参数 -uncopy 关闭这一特性 84 | (*.webp 和 *.gif 仍会被复制) 85 | -h ................ 展示帮助信息 86 | 87 | --------- 88 | TODO 89 | --------- 90 | 91 | - 支持 Windows 92 | - 支持更多的参数 93 | -------------------------------------------------------------------------------- /fast2webp/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | import shutil 7 | import time 8 | import types 9 | import argparse 10 | import signal 11 | from threading import Thread 12 | from queue import Queue 13 | 14 | class ArgManger(): 15 | '''处理参数列表''' 16 | 17 | def __init__(self): 18 | self.parser = argparse.ArgumentParser( 19 | description="一个将 PNG、JPG、JPEG、BMP、GIF 等格式的图像文件批量 (递归) 转换为 webp 格式的 Python 脚本", 20 | formatter_class=argparse.RawTextHelpFormatter 21 | ) 22 | self.set_args() 23 | ArgManger.INPUT_PATH = self.args.input_path # 输入路径 24 | ArgManger.OUTPUT_PATH = self.args.output_path # 输出路径 25 | ArgManger.T_NUM = self.args.t_num # 线程池中的线程个数 26 | ArgManger.ONLY = self.args.only # 只转换某一类型的文件 27 | ArgManger.ENABLE_GIF = self.args.enable_gif # 是否转换 gif 图 28 | ArgManger.UNCOPY = self.args.uncopy # 不将非图片文件复制到输出目录 29 | if isinstance(self.args.quality, str): 30 | ArgManger.QUALITY = "-q " + self.args.quality # 压缩程度 31 | else: 32 | ArgManger.QUALITY = "-lossless" # 压缩程度 33 | 34 | def set_args(self): 35 | '''处理各个参数''' 36 | 37 | self.parser.add_argument( 38 | "-i", type=str, dest="input_path", default=".", 39 | help="需要转换的文件所在的目录,会递归进行转换" 40 | ) 41 | self.parser.add_argument( 42 | "-o", type=str, dest="output_path", default="./output", 43 | help="*.webp 文件的输出目录,输出目录结构与输入目录保持一致\n" 44 | "如果输出目录不存则会按照给定的路径新建文件夹" 45 | ) 46 | self.quality_group = self.parser.add_mutually_exclusive_group() 47 | self.quality_group.add_argument( 48 | "-q", type=str, dest="quality", default="80", 49 | help="与 'cwebp' 中相同。表示压缩的程度 (0 ~ 100),数字越大品质越好" 50 | ) 51 | self.parser.add_argument( 52 | "-t", type=int, dest="t_num", default=10, 53 | help="线程池中线程的个数。数字越大转换速度越快,当然,也更吃资源" 54 | ) 55 | self.parser.add_argument( 56 | "-only", dest="only",choices=["png", "jpg", "bmp", "gif"],default="off", help="只执行某单一任务\n" 57 | "例如:-only png 表示只转换输入目录中的 png 文件为到输出目录" 58 | ) 59 | self.quality_group.add_argument( 60 | "-lossless", "--lossless", dest="quality", action="store_true", 61 | help="与 'cwebp' 中相同。无损压缩" 62 | ) 63 | self.parser.add_argument( 64 | "-enable_gif", "--enable_gif", dest="enable_gif",action="store_true",help="将 gif 转为 webp (默认情况下会跳过 gif文件)\n" 65 | "转换 gif 文件比较吃资源,慎用\n" 66 | "转换 gif 文件不支持无损压缩,此时 -lossless = -q 100" 67 | ) 68 | self.parser.add_argument( 69 | "-uncopy", "--uncopy", dest="uncopy", action="store_true", 70 | help="默认情况下会将非图片文件复制到输出目录中,使用参数 -uncopy 关闭这一特性\n" 71 | "(*.webp 和 *.gif 仍会被复制)" 72 | ) 73 | self.args = self.parser.parse_args() 74 | 75 | 76 | class ThreadPoolManger(): 77 | """线程池管理器""" 78 | 79 | def __init__(self, thread_num): 80 | '''初始化参数''' 81 | self.work_queue = Queue() 82 | self.thread_num = thread_num 83 | self.__init_threading_pool(self.thread_num) 84 | 85 | def __init_threading_pool(self, thread_num): 86 | '''初始化线程池,创建指定数量的线程池''' 87 | for i in range(thread_num): 88 | thread = ThreadManger(self.work_queue) 89 | thread.start() 90 | 91 | def add_job(self, func, *args): 92 | # 将任务放入队列,等待线程池阻塞读取,参数是被执行的函数和函数的参数 93 | self.work_queue.put((func, args)) 94 | 95 | 96 | class ThreadManger(Thread): 97 | '''定义线程类,继承 threading.Thread''' 98 | 99 | def __init__(self, work_queue): 100 | Thread.__init__(self) 101 | self.work_queue = work_queue 102 | self.daemon = True 103 | 104 | def run(self): 105 | '''启动线程''' 106 | while True: 107 | target, args = self.work_queue.get() 108 | target(*args) 109 | self.work_queue.task_done() 110 | 111 | 112 | class Coversion(): 113 | """将图像文件转换为 webp 格式""" 114 | 115 | def __init__(self, thread_pool): 116 | self.thread_pool = thread_pool 117 | 118 | def run(self, input_dir, output_dir): 119 | '''读取目录信息''' 120 | # 读取文件列表 121 | files = os.listdir(input_dir) 122 | for file in files: 123 | # 判断读取的文件是否是文件夹 124 | if os.path.isdir(input_dir + "/" + file): 125 | self.run(input_dir + "/" + file, output_dir + "/" + file) 126 | else: 127 | # 判断输出路径是否存在 128 | # 按照输入目录的目录结构在输出的目录中创建文件夹 129 | if not os.path.exists(output_dir): 130 | os.makedirs(output_dir) 131 | # 将任务加入队列 132 | self.thread_pool.add_job(self.fast2webp, input_dir, output_dir, file) 133 | OutManger.total_num += 1 134 | 135 | def fast2webp(self, input_dir, output_dir, file): 136 | '''处理文件''' 137 | # 初始化 webp 返回值 138 | status = 0 139 | # 优化输入与输出文件路径 140 | input_file = input_dir + "/" + file 141 | output_file = output_dir + "/" + os.path.splitext(file)[0] + ".webp" 142 | if self.is_img(file): 143 | # cwebp 144 | status = os.system( 145 | "cwebp " + ArgManger.QUALITY + " \"" + input_file + "\" -o \"" + output_file + "\" -quiet" 146 | ) 147 | OutManger.cover_num += 1 148 | elif self.is_gif(file): 149 | # gif2webp 150 | status = os.system( 151 | "gif2webp " + ArgManger.QUALITY + " \"" + input_file + "\" -o \"" + output_file + "\" -quiet" 152 | ) 153 | OutManger.cover_num += 1 154 | elif self.is_copy(file): 155 | # 复制多余的文件 156 | output_file = output_dir + "/" + file 157 | shutil.copy(input_file, output_file) 158 | OutManger.copy_num += 1 159 | # 处理 webp 返回值 160 | if status != 0: 161 | OutManger.fail_num += 1 162 | OutManger.fail_list.append(input_file) 163 | 164 | def is_img(self, file): 165 | '''判断读取的文件是否是(静态) 图片''' 166 | suffix = os.path.splitext(file)[1] 167 | only = ArgManger.ONLY 168 | if (suffix == ".jpg") & ((only == "off") | (only == "jpg")): 169 | return True 170 | elif (suffix == ".jpeg") & ((only == "off") | (only == "jpg")): 171 | return True 172 | elif (suffix == ".png") & ((only == "off") | (only == "png")): 173 | return True 174 | elif (suffix == ".bmp") & ((only == "off") | (only == "bmp")): 175 | return True 176 | else: 177 | return False 178 | 179 | def is_gif(self, file): 180 | '''判断读取的文件是否是 gif 图''' 181 | suffix = os.path.splitext(file)[1] 182 | quality = ArgManger.QUALITY 183 | enable_gif = ArgManger.ENABLE_GIF 184 | only = ArgManger.ONLY 185 | if (suffix==".gif")&((enable_gif&(only=="off"))|(only=="gif")): 186 | # gif2webp任务 不支持无损压缩 187 | if ArgManger.QUALITY == "-lossless": 188 | ArgManger.QUALITY = "-q 100" 189 | return True 190 | else: 191 | return False 192 | 193 | def is_copy(self, file): 194 | '''判断文件是否需要被复制''' 195 | suffix = os.path.splitext(file)[1] 196 | only = ArgManger.ONLY 197 | if only == "off": 198 | if suffix == ".webp": 199 | return True 200 | elif suffix == ".gif": 201 | return True 202 | elif not ArgManger.UNCOPY: 203 | return True 204 | else: 205 | return False 206 | else: 207 | OutManger.pass_num += 1 208 | return False 209 | 210 | 211 | class OutManger(): 212 | """控制输出信息""" 213 | 214 | total_num = 0 # 总文件数 215 | cover_num = 0 # 转换的文件数 (包括失败的) 216 | copy_num = 0 # 复制的文件数 217 | fail_num = 0 # 失败的文件数 218 | pass_num = 0 219 | fail_list = [] # 失败的文件的地址 220 | start_time = float(round(time.time() * 100)) # 程序开始时间 221 | 222 | def spinning_cursor(self): 223 | '''这是一个会动的光标''' 224 | while True: 225 | for cursor in ["|", "/", "-", "\"]: 226 | yield cursor 227 | 228 | def get_status(self, cursor): 229 | '''输出状态信息''' 230 | output = { 231 | "cursor": cursor, 232 | "runtime_num": self.cover_num + self.copy_num, 233 | "total": self.total_num, 234 | "use_time": round( 235 | float(round(time.time() * 100)) - self.start_time 236 | ) / 100 237 | } 238 | 239 | print( 240 | "{cursor} Coverting {runtime_num}/{total} in {use_time}s ...".format(**output), end='\r' 241 | ) 242 | 243 | def final_status(self, sleep): 244 | '''输出最终状态信息''' 245 | time.sleep(sleep) 246 | only = ArgManger.ONLY 247 | output = { 248 | "total": self.total_num, 249 | "coversion": self.cover_num-self.fail_num, 250 | "copy_pass": "copy:"+str(self.copy_num) if only == "off" else "pass:"+str(self.pass_num), 251 | "fail": self.fail_num, 252 | "use_time": round( 253 | float(round((time.time() - sleep) * 100)) - self.start_time 254 | ) / 100 255 | } 256 | 257 | print("---------------------------------------------------------------") 258 | print( 259 | 'Processed {total} files (coversion:{coversion}|{copy_pass}) in {use_time}s'.format( 260 | **output) 261 | ) 262 | if len(self.fail_list) != 0: 263 | print() 264 | print("Some error occurred while processing these files") 265 | for i in self.fail_list: 266 | print(" File: " + i) 267 | else: 268 | print() 269 | print("FAILED (errors={0})".format(output["fail"])) 270 | else: 271 | print() 272 | print("OK") 273 | 274 | def shutdown(signalnum, space, frsme): 275 | """强制退出""" 276 | print("", end='\r') 277 | print("---------------------------------------------------------------") 278 | print( 279 | "Part of the task is not completed, and we don\'t know what they are.\n" 280 | "\n" 281 | "please check the directory {0}".format(ArgManger.OUTPUT_PATH) 282 | ) 283 | sys.exit(130) 284 | 285 | 286 | def main(): 287 | arg_manger = ArgManger() 288 | thread_pool = ThreadPoolManger(ArgManger.T_NUM) 289 | thread = ThreadManger(thread_pool.work_queue) 290 | fast2webp = Coversion(thread_pool) 291 | output = OutManger() 292 | 293 | fast2webp.run(ArgManger.INPUT_PATH, ArgManger.OUTPUT_PATH) 294 | 295 | cursor = output.spinning_cursor() 296 | # 阻塞主线程,直到子线程中的任务执行完毕 297 | while thread.work_queue.unfinished_tasks > 0: 298 | time.sleep(0.1) 299 | output.get_status(cursor.__next__()) 300 | # 捕捉强制退出信号 301 | for sig in [signal.SIGINT, signal.SIGHUP, signal.SIGTERM]: 302 | signal.signal(sig, output.shutdown) 303 | else: 304 | 305 | output.final_status(0.5) 306 | 307 | 308 | if __name__ == "__main__": 309 | # 只支持 Python 3.5+ 310 | if sys.version_info < (3, 5): 311 | sys.exit('Python 3.5 or greater is required.') 312 | main() 313 | --------------------------------------------------------------------------------