├── tests ├── __init__.py └── test_worker.py ├── requirements.txt ├── setup.cfg ├── thumb_gen ├── version.py ├── __init__.py ├── fonts │ └── RobotoCondensed-Regular.ttf ├── utils.py ├── __main__.py ├── worker.py ├── config.py ├── viewer.py └── application.py ├── pyproject.toml ├── .github └── workflows │ ├── python-publish.yml │ └── thumb-gen.yml ├── LICENSE ├── setup.py ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opencv-python 2 | infomedia 3 | Pillow 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = 3 | tag_date = 0 4 | 5 | -------------------------------------------------------------------------------- /thumb_gen/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "4.0.1" 2 | config_version = "2021.04.03" -------------------------------------------------------------------------------- /thumb_gen/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | from .worker import Generator 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /thumb_gen/fonts/RobotoCondensed-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truethari/thumb-gen/HEAD/thumb_gen/fonts/RobotoCondensed-Regular.ttf -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine Pillow infomedia opencv-python 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: __token__ 28 | TWINE_PASSWORD: ${{ secrets.TWINE_TOKEN }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.github/workflows/thumb-gen.yml: -------------------------------------------------------------------------------- 1 | name: thumb-gen 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - beta 8 | paths-ignore: 9 | - "**/README.md" 10 | - "thumb_gen/version.py" 11 | pull_request: 12 | branches: [master] 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python-version: [3.7, 3.8, 3.9] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install pytest Pillow infomedia opencv-python 31 | pip install -r requirements.txt 32 | python setup.py install 33 | sudo wget -P /home/ "https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4" 34 | - name: Test with pytest 35 | run: | 36 | pytest tests 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 tharindu.dev 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. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from setuptools import setup, find_packages 4 | from thumb_gen import __version__ 5 | 6 | here = pathlib.Path(__file__).parent.resolve() 7 | long_description = (here / 'README.md').read_text(encoding='utf-8') 8 | 9 | setup( 10 | name="thumb_gen", 11 | version=__version__, 12 | description="Python application that can be used to generate video thumbnail for mp4 and mkv file types.", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | author="tharindu.dev", 16 | author_email="tharindu.nm@yahoo.com", 17 | url="https://github.com/truethari/thumb-gen", 18 | keywords="thumbnails video screenshot", 19 | license='MIT', 20 | project_urls={ 21 | "Bug Tracker": "https://github.com/truethari/thumb-gen/issues", 22 | }, 23 | classifiers=[ 24 | "Programming Language :: Python :: 3", 25 | "License :: OSI Approved :: MIT License", 26 | ], 27 | packages=['thumb_gen'], 28 | include_package_data=True, 29 | package_data = {'' : ['fonts/*.ttf']}, 30 | install_requires=["Pillow", "infomedia", "opencv-python"], 31 | entry_points={ 32 | "console_scripts": [ 33 | "thumb-gen=thumb_gen.__main__:main", 34 | ] 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /thumb_gen/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pathlib 4 | 5 | from infomedia import mediainfo 6 | 7 | import thumb_gen as _ 8 | 9 | def get_datadir() -> pathlib.Path: 10 | 11 | """ 12 | Returns a parent directory path 13 | where persistent application data can be stored. 14 | 15 | # linux: ~/.local/share 16 | # macOS: ~/Library/Application Support 17 | # windows: C:/Users//AppData/Roaming 18 | """ 19 | 20 | home = pathlib.Path.home() 21 | 22 | if sys.platform == "win32": 23 | return home / "AppData/Roaming" 24 | elif sys.platform == "linux": 25 | return home / ".local/share" 26 | elif sys.platform == "darwin": 27 | return home / "Library/Application Support" 28 | 29 | def check_os(): 30 | if sys.platform == 'win32': 31 | return 'win32' 32 | elif sys.platform == 'linux': 33 | return 'linux' 34 | elif sys.platform == 'darwin': 35 | return 'darwin' 36 | 37 | def listToString(s, chars=" "): 38 | str1 = chars 39 | if chars == 'sys': 40 | oss = check_os() 41 | if oss == 'win32': 42 | return ('\\'.join(s)) 43 | else: 44 | return ('/'.join(s)) 45 | else: 46 | return (str1.join(s)) 47 | 48 | def video_info(video_path): 49 | audio_properties = {} 50 | video_properties = {} 51 | media_info = mediainfo(video_path) 52 | for key in media_info: 53 | try: 54 | if media_info[key]['codec_type'] == 'audio': 55 | audio_properties = media_info[key] 56 | elif media_info[key]['codec_type'] == 'video': 57 | video_properties = media_info[key] 58 | except KeyError: 59 | continue 60 | 61 | return video_properties, audio_properties, media_info['format'] 62 | 63 | def convert_unit(size_in_bytes, unit='KiB'): 64 | if unit == "KiB": 65 | return round(size_in_bytes/1024, 2) 66 | elif unit == "MiB": 67 | return round(size_in_bytes/(1024*1024), 2) 68 | elif unit == "GiB": 69 | return round(size_in_bytes/(1024*1024*1024), 2) 70 | else: 71 | return size_in_bytes 72 | 73 | def get_file_size(file_name, unit="MiB"): 74 | size = os.path.getsize(file_name) 75 | return convert_unit(size, unit) 76 | 77 | def CheckIfFileExists(file_path): 78 | if os.path.exists(file_path): 79 | return True 80 | else: 81 | return False 82 | 83 | def packagePath(): 84 | return _.__path__[0] 85 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # tempdata.ini 132 | tempdata.ini 133 | -------------------------------------------------------------------------------- /thumb_gen/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import optparse 4 | 5 | from .viewer import configurations 6 | from .config import modify_config 7 | from .worker import Generator 8 | from .version import __version__ 9 | 10 | def check_files(paths_or_files): 11 | videos = [] 12 | current_directory = os.getcwd() 13 | 14 | for path_or_file in paths_or_files: 15 | if not os.path.exists(path_or_file): 16 | if not os.path.exists(os.path.join(current_directory, path_or_file)): 17 | sys.exit("{}: no such file or directory".format(path_or_file)) 18 | else: 19 | real_path = os.path.join(current_directory, path_or_file) 20 | else: 21 | if os.path.isfile(path_or_file): 22 | real_path = path_or_file 23 | else: 24 | real_path = os.path.join(current_directory, path_or_file) 25 | 26 | if os.path.isfile(real_path): 27 | if real_path.endswith('.mp4') or real_path.endswith('.mkv'): 28 | videos.append(real_path) 29 | else: 30 | sys.exit("{}: file not supported".format(real_path)) 31 | elif os.path.isdir(real_path): 32 | for file in os.listdir(real_path): 33 | if file.endswith('.mp4') or file.endswith('.mkv'): 34 | videos.append(os.path.join(real_path, file)) 35 | if videos == []: 36 | sys.exit("{}: all of files in the directory are not supported".format(real_path)) 37 | 38 | return videos 39 | 40 | def main(): 41 | usage = "usage: %prog file file\nusage: %prog dir dir" 42 | parser = optparse.OptionParser(description="THUMB-GEN v" + __version__, usage=usage) 43 | parser.add_option("-c", "--config", 44 | action="store_true", 45 | default=False, 46 | help="configurations (images, image quality, font, font size, \ 47 | custom text, bg color, font color)" 48 | ) 49 | parser.add_option("-v", "--version", 50 | action="store_true", 51 | default=False, 52 | help="show thumb-gen version and exit" 53 | ) 54 | 55 | (options, args) = parser.parse_args() 56 | options = vars(options) 57 | 58 | if options['config']: 59 | conf_images, conf_image_quality, conf_font, conf_font_size, \ 60 | conf_custom_text, conf_bg_colour, conf_font_colour = configurations() 61 | 62 | if conf_images != 0: 63 | modify_config('images', conf_images) 64 | if conf_image_quality != 0: 65 | modify_config('image_quality', conf_image_quality) 66 | if conf_font != '0': 67 | modify_config('font', conf_font) 68 | if conf_font_size != 0: 69 | modify_config('font_size', conf_font_size) 70 | if conf_custom_text != '': 71 | modify_config('custom_text', conf_custom_text) 72 | if conf_bg_colour != '': 73 | modify_config('bg_colour', conf_bg_colour) 74 | if conf_font_colour != '': 75 | modify_config('font_colour', conf_font_colour) 76 | 77 | sys.exit() 78 | 79 | elif options['version']: 80 | sys.exit(__version__) 81 | 82 | elif args != []: 83 | videos = check_files(args) 84 | for video in videos: 85 | app = Generator(video) 86 | app.run() 87 | 88 | else: 89 | print("no argument given!\n") 90 | parser.print_help() 91 | 92 | if __name__ == '__main__': 93 | main() -------------------------------------------------------------------------------- /tests/test_worker.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from thumb_gen import Generator 4 | 5 | #video_path, output_path='', custom_text=True, font_dir='', font_size=0, bg_colour='', font_colour='' 6 | 7 | video_path = "/home/sample-mp4-file.mp4" 8 | output_path = "/home/runner/work/thumb-gen/thumb-gen/" 9 | 10 | def test_worker_1(): 11 | # Checking whether it gives an error when the required arguments are not given 12 | try: 13 | app = Generator(video_path, output_path=output_path, font_size=10, bg_colour="yellow", font_colour="black", columns=10) 14 | assert False 15 | except ValueError: 16 | assert True 17 | 18 | def test_worker_2(): 19 | # Checking whether it gives an error when the required arguments are not given 20 | try: 21 | app = Generator(video_path, output_path=output_path, font_size=10, bg_colour="yellow", font_colour="black", rows=10) 22 | assert False 23 | except ValueError: 24 | assert True 25 | 26 | def test_worker_3(): 27 | # Checking if an error is returned when the required arguments are given 28 | try: 29 | app = Generator(video_path, output_path=output_path, font_size=10, bg_colour="yellow", font_colour="black", rows=10, columns=10) 30 | assert True 31 | except: 32 | assert False 33 | 34 | def test_worker_4(): 35 | # Checking if an error is returned when the required arguments are given 36 | try: 37 | app = Generator(video_path, output_path=output_path, font_size=10, bg_colour="yellow", font_colour="black", rows=10, imgCount=30) 38 | assert True 39 | except: 40 | assert False 41 | 42 | def test_worker_5(): 43 | # Checking if an error is returned when the required arguments are given 44 | try: 45 | app = Generator(video_path, output_path=output_path, font_size=10, bg_colour="yellow", font_colour="black", columns=10, imgCount=30) 46 | assert True 47 | except: 48 | assert False 49 | 50 | def test_worker_6(): 51 | # check if an get error is returened when image count is lower than rows 52 | try: 53 | app = Generator(video_path, output_path=output_path, font_size=10, bg_colour="yellow", font_colour="black", rows=10, imgCount=5) 54 | assert False 55 | except ValueError: 56 | assert True 57 | 58 | def test_worker_7(): 59 | # check if an get error is returened when image count is lower than columns 60 | try: 61 | app = Generator(video_path, output_path=output_path, font_size=10, bg_colour="yellow", font_colour="black", columns=10, imgCount=5) 62 | assert False 63 | except ValueError: 64 | assert True 65 | 66 | def test_worker_8(): 67 | # check if an get error is returened when defining 'rows', 'columns' and 'imgCount' at once 68 | try: 69 | app = Generator(video_path, output_path=output_path, font_size=10, bg_colour="yellow", font_colour="black", columns=10, rows=5, imgCount=50) 70 | assert False 71 | except TypeError: 72 | assert True 73 | 74 | def test_worker_9(): 75 | app = Generator(video_path, output_path=output_path, custom_text="thumb gen") 76 | test_worker_value = app.run() 77 | assert test_worker_value == True 78 | 79 | def test_worker_10(): 80 | app = Generator(video_path, output_path=output_path, font_size=10, bg_colour="yellow", font_colour="black") 81 | test_worker_value = app.run() 82 | assert test_worker_value == True 83 | 84 | def test_worker_11(): 85 | app = Generator(video_path, output_path=output_path, font_size=10, bg_colour="yellow", font_colour="black", columns=5, rows=10) 86 | test_worker_value = app.run() 87 | assert test_worker_value == True 88 | -------------------------------------------------------------------------------- /thumb_gen/worker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import tempfile 4 | 5 | from .application import screenshots, resize, timestamps, thumb 6 | from .viewer import print_process, print_success 7 | from .utils import listToString 8 | 9 | class Generator: 10 | def __init__(self, video_path, output_path='', rows=0, columns=0, imgCount=0, custom_text='True', font_dir='', font_size=0, bg_colour='', font_colour=''): 11 | self.video_path = video_path 12 | self.rows = rows 13 | self.columns = columns 14 | self.imgCount = imgCount 15 | 16 | if self.imgCount != 0: 17 | if self.rows == 0 and self.columns == 0: 18 | self.columns = 3 19 | self.rows, mod = divmod(self.imgCount, self.columns) 20 | if mod != 0 and self.imgCount > self.columns: 21 | self.rows += 1 22 | 23 | elif self.rows == 0: 24 | if self.columns > self.imgCount: 25 | raise ValueError("'columns' value greater than 'imgCount'.") 26 | self.rows, mod = divmod(self.imgCount, self.columns) 27 | if mod != 0 and self.imgCount > self.columns: 28 | self.rows += 1 29 | 30 | elif self.columns == 0: 31 | if self.rows > self.imgCount: 32 | raise ValueError("'rows' value greater than 'imgCount'.") 33 | self.columns, mod = divmod(self.imgCount, self.rows) 34 | if mod != 0 and self.imgCount > self.rows: 35 | self.columns += 1 36 | 37 | else: 38 | raise TypeError("defining 'rows', 'columns' and 'imgCount' at once is not functional. remove one of them") 39 | else: 40 | if (self.rows == 0 and self.columns != 0) or (self.rows != 0 and self.columns == 0): 41 | raise ValueError("missing 1 required positional argument: 'imgCount'") 42 | 43 | if 0 < self.imgCount < 3: 44 | raise ValueError("'imgCount' value must be greater than 3") 45 | 46 | if 0 < self.columns < 3: 47 | raise ValueError("'columns' value must be greater than 3") 48 | 49 | if output_path == '': 50 | self.output_path = self.video_path[:-4] 51 | self.output_folder = listToString(re.split(pattern = r"[/\\]", string = self.video_path)[:-1], "sys") 52 | 53 | else: 54 | self.filename = re.split(pattern = r"[/\\]", string = self.video_path)[-1] 55 | self.output_path = os.path.join(output_path, self.filename[:-4]) 56 | self.output_folder = self.output_path 57 | 58 | self.custom_text = str(custom_text) 59 | self.font_dir = font_dir 60 | 61 | if isinstance(font_size, int): 62 | self.font_size = font_size 63 | elif isinstance(font_size, str): 64 | raise ValueError("'font_size' must be an integer") 65 | 66 | self.bg_colour = bg_colour 67 | self.font_colour = font_colour 68 | 69 | self.temp_dir = tempfile.TemporaryDirectory() 70 | self.secure_temp = self.temp_dir.name 71 | self.screenshot_folder = os.path.join(self.secure_temp, 'screenshots') 72 | self.resize_folder = os.path.join(self.secure_temp, 'resized') 73 | os.mkdir(self.screenshot_folder) 74 | os.mkdir(self.resize_folder) 75 | 76 | def run(self): 77 | print_process(self.video_path) 78 | self.ss_time = screenshots(self.video_path, self.screenshot_folder, self.imgCount) 79 | resize(self.screenshot_folder, self.resize_folder) 80 | timestamps(self.resize_folder, self.font_dir, self.font_size, self.ss_time) 81 | thumb_out = thumb(self.video_path, 82 | self.rows, 83 | self.columns, 84 | self.output_path, 85 | self.resize_folder, 86 | self.secure_temp, 87 | self.custom_text, 88 | self.font_dir, 89 | self.font_size, 90 | self.bg_colour, 91 | self.font_colour) 92 | 93 | if thumb_out: 94 | print_success(self.output_folder) 95 | 96 | return True 97 | -------------------------------------------------------------------------------- /thumb_gen/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import configparser 3 | 4 | from .utils import get_datadir, CheckIfFileExists 5 | from .version import config_version 6 | 7 | def create_config(IMAGES=12, IMAGE_QUALITY=80, FONT='', FONT_SIZE=20, CUSTOM_TEXT='', BG_COLOUR='white', FONT_COLOUR='black'): 8 | my_datadir = get_datadir() / "thumb-gen" 9 | 10 | try: 11 | my_datadir.mkdir(parents=True) 12 | 13 | except FileExistsError: 14 | pass 15 | 16 | finally: 17 | configfile_path = os.path.join(str(my_datadir), "config.ini") 18 | config_object = configparser.ConfigParser() 19 | config_object["DEFAULT"] = {"images": IMAGES, "image_quality": IMAGE_QUALITY, "font":FONT, "font_size":FONT_SIZE, "custom_text":CUSTOM_TEXT, "bg_colour":BG_COLOUR, "font_colour":FONT_COLOUR} 20 | config_object["VERSION"] = {"config_version": config_version} 21 | with open(configfile_path, 'w') as conf: 22 | config_object.write(conf) 23 | 24 | return True 25 | 26 | def modify_config(options, value): 27 | my_datadir = get_datadir() / "thumb-gen" 28 | configfile_path = os.path.join(str(my_datadir), "config.ini") 29 | config_object = configparser.ConfigParser() 30 | 31 | config_object.read(configfile_path) 32 | userinfo = config_object["DEFAULT"] 33 | 34 | if options == "images": 35 | userinfo["images"] = str(value) 36 | 37 | elif options == "image_quality": 38 | userinfo["image_quality"] = str(value) 39 | 40 | elif options == "font": 41 | userinfo["font"] = str(value) 42 | 43 | elif options == "font_size": 44 | userinfo["font_size"] = str(value) 45 | 46 | elif options == "custom_text": 47 | if value == '000' or value == 'clear': 48 | userinfo["custom_text"] = '' 49 | else: 50 | userinfo["custom_text"] = str(value) 51 | 52 | elif options == "bg_colour": 53 | userinfo["bg_colour"] = str(value) 54 | 55 | elif options == "font_colour": 56 | userinfo["font_colour"] = str(value) 57 | 58 | with open(configfile_path, 'w') as conf: 59 | config_object.write(conf) 60 | 61 | return True 62 | 63 | def read_config(option): 64 | my_datadir = get_datadir() / "thumb-gen" 65 | configfile_path = os.path.join(str(my_datadir), "config.ini") 66 | config_object = configparser.ConfigParser() 67 | 68 | if CheckIfFileExists(configfile_path): 69 | config_object.read(configfile_path) 70 | 71 | try: 72 | config_object["VERSION"]["config_version"] == config_version 73 | except KeyError: 74 | create_config() 75 | 76 | config_object.read(configfile_path) 77 | if config_object["VERSION"]["config_version"] == config_version: 78 | default = config_object["DEFAULT"] 79 | 80 | else: 81 | create_config_return = create_config() 82 | if create_config_return: 83 | config_object.read(configfile_path) 84 | default = config_object["DEFAULT"] 85 | 86 | else: 87 | create_config_return = create_config() 88 | if create_config_return: 89 | config_object.read(configfile_path) 90 | default = config_object["DEFAULT"] 91 | 92 | loop = True 93 | while loop: 94 | try: 95 | if option == 'images': 96 | return int(default['images']) 97 | elif option == 'image_quality': 98 | return int(default['image_quality']) 99 | elif option == 'font': 100 | return str(default['font']) 101 | elif option == 'font_size': 102 | return int(default['font_size']) 103 | elif option == 'custom_text': 104 | return str(default['custom_text']) 105 | elif option == 'bg_colour': 106 | return str(default['bg_colour']) 107 | elif option == 'font_colour': 108 | return str(default['font_colour']) 109 | loop = False 110 | 111 | except KeyError: 112 | create_config_return = create_config() 113 | if create_config_return: 114 | config_object.read(configfile_path) 115 | default = config_object["DEFAULT"] 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thumbnail Generator 🎬 2 | 3 | [![github actions](https://github.com/truethari/thumb-gen/actions/workflows/thumb-gen.yml/badge.svg)](https://github.com/truethari/thumb-gen/actions/workflows/thumb-gen.yml) 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/01b66feeb94743ac80e413e4e9075595)](https://www.codacy.com/gh/truethari/thumb-gen/dashboard?utm_source=github.com&utm_medium=referral&utm_content=truethari/thumb-gen&utm_campaign=Badge_Grade) 5 | [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) 6 | [![PyPI version](https://badge.fury.io/py/thumb-gen.svg)](https://badge.fury.io/py/thumb-gen) 7 | [![Downloads](https://pepy.tech/badge/thumb-gen)](https://pepy.tech/project/thumb-gen) 8 | 9 | ## What is This 10 | 11 | --- 12 | 13 | This is a Python application that can be used to generate video thumbnail for mp4 and mkv file types. 14 | 15 | [![Imgur](https://i.imgur.com/qRHQRK7.png)](https://imgur.com/a/xpkkyqH) 16 | 17 | ## Installation 18 | 19 | You can use pip: 20 | 21 | ```console 22 | ~$ pip3 install thumb-gen 23 | ``` 24 | 25 | ## Configurations 26 | 27 | - The number of screen images that should be included in the final thumbnail image 28 | - Thumbnail image quality 29 | - Font type in the video info panel. You can add a file path of a font file (.ttf) to this 30 | - Font size in the video info panel 31 | - Custom text in the video info panel 32 | - Background color of the thumbnail (Hex codes are also supported) 33 | - Font colour of the thumbnail (Hex codes are also supported) 34 | 35 | Download font files : [FontSquirrel](https://www.fontsquirrel.com/) 36 | 37 | ```console 38 | ~$ thumb-gen -c 39 | ``` 40 | 41 | or 42 | 43 | ```console 44 | ~$ thumb-gen --config 45 | ``` 46 | 47 | By program default: 48 | 49 | ```ini 50 | IMAGES = 12 51 | IMAGE_QUALITY = 80 52 | FONT = 53 | FONT_SIZE = 20 54 | CUSTOM_TEXT = 55 | BG_COLOUR = white 56 | FONT_COLOUR = black 57 | ``` 58 | 59 | ## Usage 60 | 61 | ### Usage options 62 | 63 | ```text 64 | Usage: thumb-gen file file 65 | usage: thumb-gen dir dir 66 | 67 | Options: 68 | -h, --help show this help message and exit 69 | -c, --config configurations (images, image quality, font, font size, 70 | custom text, bg color, font color) 71 | -v, --version show thumb-gen version and exit 72 | ``` 73 | 74 | ### Console 75 | 76 | ```console 77 | ~$ thumb-gen -h 78 | ~$ thumb-gen --help 79 | 80 | ~$ thumb-gen -c 81 | ~$ thumb-gen --config 82 | 83 | ~$ thumb-gen -v 84 | ~$ thumb-gen --version 85 | 86 | ~$ thumb-gen input.mp4 87 | ~$ thumb-gen input.mp4 input2.mp4 88 | ~$ thumb-gen "d:/videos/input.mp4" 89 | 90 | ~$ thumb-gen "/videos" 91 | ~$ thumb-gen "/videos" "/videos2" 92 | ~$ thumb-gen "d:/videos" 93 | ``` 94 | 95 | ### Python 96 | 97 | - If you don't set an output folder, thumbnail images will be saved in the video folder (video_path). 98 | - If you don't need a custom text and custom font file (including font size) and you have already set these for the configuration file (using console or defaults), it will be added automatically. To avoid this set the `custom_text` value to `False` and add a custom font file location. 99 | - If you do not set the `columns` size, it will use the default value (3) 100 | 101 | ```python 102 | Generator(video_path, output_path='', rows=0, columns=0, imgCount=0, custom_text='True', font_dir='', font_size=0, bg_colour='', font_colour='') 103 | ``` 104 | 105 | #### Example 1 106 | 107 | ```python 108 | from thumb_gen import Generator 109 | 110 | app = Generator(video_path="C:/input/video.mp4", 111 | output_path="C:/output/", 112 | custom_text="www.example.com", 113 | font_dir="C:/Windows/Fonts/Arial.ttf", 114 | font_size=30) 115 | app.run() 116 | ``` 117 | 118 | #### Example 2 119 | 120 | ```Python 121 | import os 122 | from thumb_gen import Generator 123 | 124 | folder = 'C:/input' 125 | for video in os.listdir(folder): 126 | if video.endswith('.mp4') or video.endswith('.mkv'): 127 | app = Generator(video_path=os.path.join(folder, video), 128 | custom_text=False, 129 | font_dir="C:/Project/font.ttf", 130 | font_size=25, 131 | bg_colour='blue', 132 | font_colour='red') 133 | app.run() 134 | ``` 135 | 136 | #### Example 3 137 | 138 | ```Python 139 | import os 140 | from thumb_gen import Generator 141 | 142 | folder = 'C:/input' 143 | for video in os.listdir(folder): 144 | if video.endswith('.mp4') or video.endswith('.mkv'): 145 | app = Generator(video_path=os.path.join(folder, video), 146 | columns=5, 147 | rows=10, 148 | custom_text=False, 149 | font_dir="C:/Project/font.ttf", 150 | font_size=25, 151 | bg_colour='blue', 152 | font_colour='red') 153 | app.run() 154 | ``` 155 | -------------------------------------------------------------------------------- /thumb_gen/viewer.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from datetime import datetime 4 | from PIL import ImageColor 5 | from .config import read_config 6 | from .utils import CheckIfFileExists, check_os 7 | 8 | now = datetime.now() 9 | current_time = now.strftime("%H:%M:%S") 10 | 11 | def configurations(): 12 | print("Configurations\n") 13 | print("Images = {}\n" \ 14 | "Thumbnail Quality = {}\n" \ 15 | "Font = {}\n" \ 16 | "Font Size = {}\n" \ 17 | "Custom Text = {}\n" \ 18 | "Background Colour = {}\n" \ 19 | "Font Colour = {}\n" \ 20 | .format(read_config('images'), \ 21 | read_config('image_quality'), \ 22 | read_config('font'), \ 23 | read_config('font_size'), \ 24 | read_config('custom_text'), \ 25 | read_config('bg_colour'), \ 26 | read_config('font_colour'))) 27 | 28 | while True: 29 | try: 30 | while True: 31 | print("If you do not want to change the values, leave the input blank and press Enter.\n") 32 | print("CTRL + C to exit.") 33 | try: 34 | images = int(input("Images: ") or 0) 35 | break 36 | except ValueError: 37 | print("Invalid input! Please enter a valid number.") 38 | 39 | while True: 40 | try: 41 | while True: 42 | image_quality = int(input("Thumbnail Quality (10 - 100): ") or 0) 43 | if 101 > image_quality > 9 or image_quality == 0: 44 | break 45 | else: 46 | print("Enter number between 10 - 100!") 47 | break 48 | except ValueError: 49 | print("Invalid input! Please enter a valid number.") 50 | 51 | while True: 52 | font_path = str(input("Font Path: ") or '0') 53 | font_path_status = CheckIfFileExists(font_path) 54 | if font_path_status or font_path == '0': 55 | break 56 | else: 57 | print("No font file found. Check the path and re-enter it") 58 | 59 | while True: 60 | try: 61 | while True: 62 | font_size = int(input("Font Size (10 - 100): ") or 0) 63 | if 9 < font_size < 101 or font_size == 0: 64 | break 65 | else: 66 | print("Enter number between 10 - 100!") 67 | break 68 | except ValueError: 69 | print("Invalid input! Please enter a valid number.") 70 | 71 | print("Input 'clear' or '000' to clear custom text") 72 | custom_text = str(input("Custom text: ") or '') 73 | 74 | while True: 75 | try: 76 | bg_colour = str(input("Background Colour: ") or '') 77 | if bg_colour == '': 78 | pass 79 | else: 80 | ImageColor.getrgb(bg_colour) 81 | except ValueError: 82 | print("Color not recognized: {}. Enter a valid colour!".format(bg_colour)) 83 | else: 84 | break 85 | 86 | while True: 87 | try: 88 | font_colour = str(input("Font Colour: ") or '') 89 | if font_colour == '': 90 | pass 91 | else: 92 | ImageColor.getrgb(font_colour) 93 | except ValueError: 94 | print("Color not recognized: {}. Enter a valid colour!".format(font_colour)) 95 | else: 96 | break 97 | 98 | except KeyboardInterrupt: 99 | sys.exit('\n') 100 | 101 | break 102 | 103 | return images, image_quality, font_path, font_size, custom_text, bg_colour, font_colour 104 | 105 | def print_process(name): 106 | oss = check_os() 107 | if oss == 'linux': 108 | print("\033[33m [{}] \033[36m Processing: {}\033[00m" .format(current_time, name)) 109 | else: 110 | print(" [{}] Processing: {}".format(current_time, name)) 111 | 112 | def print_success(name): 113 | oss = check_os() 114 | if oss == 'linux': 115 | print("\033[33m [{}] \033[36m Thumbnail saved in: {}\033[00m" .format(current_time, name)) 116 | else: 117 | print(" [{}] Thumbnail saved in: {}".format(current_time, name)) 118 | -------------------------------------------------------------------------------- /thumb_gen/application.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import ntpath 4 | import sys 5 | import datetime 6 | 7 | from cv2 import cv2 8 | from PIL import Image 9 | from PIL import ImageFont 10 | from PIL import ImageDraw 11 | from PIL import ImageColor 12 | 13 | from .config import read_config 14 | from .utils import listToString, video_info, get_file_size, convert_unit, packagePath 15 | 16 | def font_info(text, font, font_size): 17 | try: 18 | font = ImageFont.truetype(font, font_size) 19 | except OSError: 20 | font = ImageFont.load_default() 21 | 22 | text_width = font.getsize(text)[0] 23 | text_height = font.getsize(text)[1] 24 | 25 | return text_width, text_height 26 | 27 | def lining(text, font, font_size, image_width): 28 | lines = {'line1': []} 29 | tmp_list = [] 30 | text_list = text.split(" ") 31 | tmp_list = text_list 32 | rounds = 0 33 | 34 | while True: 35 | list_len = len(text_list) 36 | paragraph = listToString(tmp_list) 37 | text_width = font_info(paragraph, font, font_size)[0] 38 | 39 | rounds = rounds + 1 40 | 41 | if not text_width > image_width: 42 | list_number = 0 43 | 44 | while True: 45 | list_number = list_number + 1 46 | 47 | try: 48 | tmp_line = lines['line{}'.format(list_number)] 49 | except KeyError: 50 | tmp_line = [] 51 | 52 | if '0tKJz' not in tmp_line: 53 | lines['line{}'.format(list_number)] = [paragraph] 54 | lines['line{}'.format(list_number)].append('0tKJz') 55 | 56 | for _ in range(0, (list_len - rounds) + 1): 57 | try: 58 | text_list.pop(0) 59 | except IndexError: 60 | pass 61 | 62 | tmp_list = text_list 63 | rounds = 0 64 | break 65 | 66 | else: 67 | tmp_list = tmp_list[:-1] 68 | 69 | if text_list == []: 70 | break 71 | 72 | r_line = 0 73 | 74 | for _ in lines: 75 | r_line = r_line + 1 76 | 77 | if "0tKJz" in lines['line{}'.format(r_line)]: 78 | lines['line{}'.format(r_line)].remove("0tKJz") 79 | 80 | return lines 81 | 82 | def imageText(video_path, secure_tmp, bg_width, bg_height, custom_text, 83 | font_dir, font_size, bg_colour, font_colour): 84 | if font_dir == '': 85 | font_name = read_config('font') 86 | if font_name == '': 87 | package_dir = packagePath() 88 | font_name = os.path.join(package_dir, 'fonts', 'RobotoCondensed-Regular.ttf') 89 | 90 | else: 91 | font_name = font_dir 92 | 93 | if font_size == 0: 94 | font_size = read_config('font_size') 95 | 96 | if custom_text == 'True': 97 | custom_text = read_config('custom_text') 98 | elif custom_text == 'False': 99 | custom_text = '' 100 | 101 | if bg_colour == '': 102 | bg_colour = read_config('bg_colour') 103 | 104 | if font_colour == '': 105 | font_colour = read_config('font_colour') 106 | 107 | video_properties, audio_properties, default_properties = video_info(video_path) 108 | 109 | #file 110 | info_filename = "Filename: " + ntpath.basename(video_path) 111 | filesize = get_file_size(video_path) 112 | if filesize <= 1024: 113 | info_filesize = "Size: " + str(filesize) + "MiB" 114 | else: 115 | info_filesize = "Size: " + str(round((filesize / 1024), 2)) + "GiB" 116 | 117 | try: 118 | duration = round(float(default_properties['duration'])) 119 | info_duration = "Duration: " + str(datetime.timedelta(seconds=duration)) 120 | except KeyError: 121 | info_duration = '' 122 | try: 123 | avg_bitrate = convert_unit(int(default_properties['bit_rate'])) 124 | info_avgbitrate = "avg. Bitrate: " + str(avg_bitrate) + "KB/s" 125 | except KeyError: 126 | info_avgbitrate = '' 127 | #video 128 | try: 129 | info_video = "Video: " + video_properties['codec_name'] 130 | except KeyError: 131 | info_video = '' 132 | try: 133 | info_video_res = str(video_properties['width']) + 'x' + str(video_properties['height']) 134 | except KeyError: 135 | info_video_res = '' 136 | try: 137 | video_bitrate = video_properties['bit_rate'] 138 | if str(video_bitrate) == 'N/A': 139 | raise KeyError 140 | info_video_bitrate = 'bitrate = ' + str(convert_unit(int(video_bitrate))) + "KB/s" 141 | except KeyError: 142 | info_video_bitrate = '' 143 | try: 144 | video_fps = video_properties['avg_frame_rate'].split('/') 145 | video_fps = round(int(video_fps[0]) / int(video_fps[1]), 2) 146 | info_video_fps = str(video_fps) + 'fps' 147 | except KeyError: 148 | info_video_fps = '' 149 | #audio 150 | try: 151 | info_audio = "Audio: " + audio_properties['codec_name'] 152 | except KeyError: 153 | info_audio = '' 154 | try: 155 | info_audio_rate = str(audio_properties['sample_rate']) + 'Hz' 156 | except KeyError: 157 | info_audio_rate = '' 158 | try: 159 | info_audio_channels = str(audio_properties['channels']) + ' channels' 160 | except KeyError: 161 | info_audio_channels = '' 162 | try: 163 | audio_bitrate = audio_properties['bit_rate'] 164 | if str(audio_bitrate) == 'N/A': 165 | raise KeyError 166 | info_audio_bitrate = 'bitrate = ' + str(convert_unit(int(audio_bitrate))) + "KB/s" 167 | except KeyError: 168 | info_audio_bitrate = '' 169 | #custom 170 | custom_text_bx = custom_text 171 | 172 | info_line2 = info_filesize + ' ' + info_duration + ' ' + info_avgbitrate 173 | info_line3 = info_video + ' ' + info_video_res + ' ' \ 174 | + info_video_bitrate + ' ' + info_video_fps 175 | info_line4 = info_audio + ' ' + info_audio_rate + ' ' \ 176 | + info_audio_channels + ' ' + info_audio_bitrate 177 | 178 | font_height_filename = 0 179 | font_height_normal = font_info(info_duration, font_name, font_size)[1] 180 | font_height_custom_text = 0 181 | 182 | filename_text_lines = lining(info_filename, font_name, font_size, bg_width) 183 | rounds = 0 184 | for _ in filename_text_lines: 185 | rounds = rounds + 1 186 | for lines in filename_text_lines['line{}'.format(rounds)]: 187 | if lines != []: 188 | font_height = font_info(lines, font_name, font_size)[1] 189 | font_height_filename = font_height_filename + font_height + 5 190 | 191 | rounds = 0 192 | if custom_text_bx != '': 193 | custom_text_lines = lining(custom_text_bx, font_name, font_size, bg_width) 194 | for _ in custom_text_lines: 195 | rounds = rounds + 1 196 | for lines in custom_text_lines['line{}'.format(rounds)]: 197 | if lines != []: 198 | font_height = font_info(lines, font_name, font_size)[1] 199 | font_height_custom_text = font_height_custom_text + font_height 200 | 201 | valid_lines = 0 202 | for i in (info_line2, info_line3, info_line4): 203 | if len(i) - i.count(" ") != 0: 204 | valid_lines += 1 205 | 206 | text_area_height = 5 + font_height_filename + (font_height_normal + 5) * valid_lines \ 207 | + font_height_custom_text 208 | 209 | bg_new_height = text_area_height + bg_height 210 | 211 | try: 212 | bg_colour = ImageColor.getrgb(bg_colour) 213 | except ValueError: 214 | bg_colour = bg_colour 215 | print("ValueError: unknown color specifier: {}".format(bg_colour)) 216 | print("This can be fixed by changing the configurations.\n" \ 217 | "Ex: Background colour = 'white' / Background colour = '#ffffff'") 218 | sys.exit() 219 | 220 | try: 221 | font_colour = ImageColor.getrgb(font_colour) 222 | except ValueError: 223 | font_colour = font_colour 224 | print("ValueError: unknown color specifier: {}".format(font_colour)) 225 | print("This can be fixed by changing the configurations.\n" \ 226 | "Ex: Font colour = 'black' / Font colour = '#ffffff'") 227 | sys.exit() 228 | 229 | img = Image.new('RGB', (bg_width, bg_new_height), bg_colour) 230 | img.save(os.path.join(secure_tmp, 'bg.png')) 231 | 232 | background = Image.open(os.path.join(secure_tmp, 'bg.png')) 233 | org_width = background.size[0] 234 | 235 | draw = ImageDraw.Draw(background) 236 | 237 | try: 238 | font = ImageFont.truetype(font_name, font_size) 239 | except OSError: 240 | print("{} file not found! Default font is loaded.".format(font_name)) 241 | package_dir = packagePath() 242 | font_name = os.path.join(package_dir, 'fonts', 'RobotoCondensed-Regular.ttf') 243 | font = ImageFont.truetype(font_name, font_size) 244 | 245 | x = 10 246 | y = 5 247 | 248 | rounds = 0 249 | 250 | #line1 251 | info_filename_line = lining(info_filename, font_name, font_size, bg_width) 252 | for _ in info_filename_line: 253 | rounds = rounds + 1 254 | for lines in info_filename_line['line{}'.format(rounds)]: 255 | if lines != []: 256 | font_height = font_info(info_filename, font_name, font_size)[1] 257 | draw.text((x, y), lines, font_colour, font=font) 258 | y = y + font_height 259 | 260 | font_height = font_info(info_filesize, font_name, font_size)[1] 261 | 262 | #line2 263 | if len(info_line2) - info_line2.count(" ") != 0: 264 | draw.text((x, y), info_line2, font_colour, font=font) 265 | y = y + 5 + font_height 266 | 267 | #line3 268 | if len(info_line3) - info_line3.count(" ") != 0: 269 | draw.text((x, y), info_line3, font_colour, font=font) 270 | y = y + 5 + font_height 271 | 272 | #line4 273 | if len(info_line4) - info_line4.count(" ") != 0: 274 | draw.text((x, y), info_line4, font_colour, font=font) 275 | y = y + 5 + font_height 276 | 277 | rounds = 0 278 | if custom_text != '': 279 | text_lines = lining(custom_text, font_name, font_size, org_width) 280 | for _ in text_lines: 281 | rounds = rounds + 1 282 | for lines in text_lines['line{}'.format(rounds)]: 283 | if lines != []: 284 | font_height = font_info(lines, font_name, font_size)[1] 285 | draw.text((x, y), lines, font_colour, font=font) 286 | y = y + font_height 287 | y = y + 5 288 | 289 | background.save(os.path.join(secure_tmp, 'bg.png')) 290 | 291 | return y + 5 292 | 293 | def timestamps(folder, font_dir, font_size, timelist): 294 | if font_dir == '': 295 | font_name = os.path.join(packagePath(), 'fonts', 'RobotoCondensed-Regular.ttf') 296 | else: 297 | font_name = font_dir 298 | 299 | font_size = 20 300 | 301 | count = -1 302 | resized_images = os.listdir(folder) 303 | resized_images.sort(key=lambda f: int(re.sub('\\D', '', f))) 304 | for img in resized_images: 305 | count += 1 306 | image = Image.open(os.path.join(folder, img)) 307 | img_width, img_height = image.size 308 | 309 | text = str(datetime.timedelta(seconds=round(timelist[count]))) 310 | text_width, text_height = font_info(text, font_name, font_size) 311 | x = img_width - text_width - 10 312 | y = img_height - text_height - 10 313 | 314 | font = ImageFont.truetype(font_name, font_size) 315 | 316 | draw = ImageDraw.Draw(image) 317 | draw.text((x, y), text , (255,255,255), font=font, stroke_width=2, stroke_fill=(0,0,0)) 318 | image.save(os.path.join(folder, (img))) 319 | 320 | def screenshots(video_path, screenshot_folder, imgCount): 321 | for img in os.listdir(screenshot_folder): 322 | os.remove(os.path.join(screenshot_folder, img)) 323 | 324 | images = imgCount 325 | if imgCount == 0: 326 | images = int(read_config('images')) 327 | 328 | timestamp = round(float(video_info(video_path)[2]['duration'])) / (images + 1) 329 | playtime = timestamp # if playtime = 0, it captures the first frame of the video 330 | ss_time = [] 331 | count = 1 332 | 333 | for _ in range(0, images): 334 | ss_time.append(playtime) 335 | playtime += timestamp 336 | 337 | vidcap = cv2.VideoCapture(video_path) 338 | for time in ss_time: 339 | vidcap.set(cv2.CAP_PROP_POS_MSEC, time*1000) 340 | success, image = vidcap.read() 341 | if success: 342 | cv2.imwrite(os.path.join(screenshot_folder, str(count)) + ".jpg", image) 343 | count += 1 344 | 345 | return ss_time 346 | 347 | def resize(screenshot_folder, resize_folder): 348 | for img in os.listdir(resize_folder): 349 | os.remove(resize_folder + img) 350 | 351 | for img in os.listdir(screenshot_folder): 352 | image = Image.open(os.path.join(screenshot_folder, img)) 353 | org_width, org_height = image.size 354 | 355 | new_height = 300 * org_height / org_width 356 | 357 | resized_im = image.resize((300, round(new_height))) 358 | resized_im.save(os.path.join(resize_folder, img)) 359 | 360 | return True 361 | 362 | def thumb(video_path, img_rows, columns, output_folder, resize_folder, secure_temp, custom_text, 363 | font_dir, font_size, bg_colour, font_colour): 364 | 365 | for img in os.listdir(resize_folder): 366 | image = Image.open(os.path.join(resize_folder, img)) 367 | r_new_width, new_height = image.size 368 | break 369 | 370 | if img_rows == 0: 371 | img_rows = 3 372 | 373 | if columns == 0: 374 | columns = 3 375 | bg_new_width = int((r_new_width * 3) + 20) 376 | else: 377 | bg_new_width = int((r_new_width * columns) + (columns*5) + 5) 378 | 379 | bg_new_height = int((new_height * img_rows) + ((5 * img_rows) + 5)) 380 | 381 | y = imageText(video_path, secure_temp, bg_new_width, bg_new_height, custom_text, 382 | font_dir, font_size, bg_colour, font_colour) 383 | 384 | backgroud = Image.open(os.path.join(secure_temp, 'bg.png')) 385 | 386 | img_list = [] 387 | resized_images = os.listdir(resize_folder) 388 | resized_images.sort(key=lambda f: int(re.sub('\\D', '', f))) 389 | 390 | for img in resized_images: 391 | image = Image.open(os.path.join(resize_folder, img)) 392 | img_list.append(image) 393 | 394 | back_im = backgroud.copy() 395 | 396 | count = 0 397 | x = 5 398 | for img in img_list: 399 | count = count + 1 400 | if (count - 1) % columns == 0 and count != 1: 401 | y = y + new_height + 5 402 | x = 5 403 | 404 | back_im.paste(img_list[count - 1], (x, y)) 405 | x = x + r_new_width + 5 406 | 407 | back_im.save(output_folder + '.jpg', quality=read_config('image_quality')) 408 | 409 | return True 410 | --------------------------------------------------------------------------------