├── .github └── workflows │ ├── codeql-analysis.yml │ ├── python-linux.yml │ ├── python-windows.yml │ ├── release-linux.yml │ ├── release-windows.yml │ └── windows-tools.yml ├── .gitignore ├── CreateExe.ps1 ├── LICENSE ├── README.md ├── build_cjxl.sh ├── makejxl.py ├── requirements.txt ├── setup.py └── test_main.py /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '42 3 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-20.04 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/python-linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-20.04 14 | strategy: 15 | matrix: 16 | python-version: ['3.8', '3.9'] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | python -m pip install setuptools wheel twine flake8 pytest Pillow 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=15 --max-line-length=250 --statistics 38 | 39 | - name: Install cjxl 40 | run: sudo /bin/bash build_cjxl.sh 41 | 42 | - name: Test 43 | run: pytest 44 | 45 | - name: Build package 46 | run: python setup.py sdist bdist_wheel 47 | 48 | # - name: Publish 49 | # env: 50 | # TWINE_USERNAME: ${{ secrets.TEST_PYPI_USERNAME }} 51 | # TWINE_PASSWORD: ${{ secrets.TEST_PYPI_PASSWORD }} 52 | # run: twine upload --repository testpypi dist/* 53 | -------------------------------------------------------------------------------- /.github/workflows/python-windows.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Windows build 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | name: Create Release 16 | runs-on: windows-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Set up Python 3.9 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: 3.9 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | python -m pip install -r requirements.txt 30 | python -m pip install pyinstaller 31 | 32 | - name: Build .exe 33 | run: set PYTHONOPTIMIZE=1 && pyinstaller --onefile makejxl.py 34 | 35 | # - name: Run UPX 36 | # uses: crazy-max/ghaction-upx@v1 37 | # with: 38 | # version: latest 39 | # file: ./dist/ExifDateTool.exe 40 | # args: --best 41 | 42 | - name: Archive exe file 43 | uses: actions/upload-artifact@v2 44 | with: 45 | name: exe-file 46 | path: ./dist/makejxl.exe 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /.github/workflows/release-linux.yml: -------------------------------------------------------------------------------- 1 | # This workflows 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: Linux release 5 | 6 | on: 7 | push: 8 | # Sequence of patterns matched against refs/tags 9 | tags: 10 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 11 | workflow_dispatch: 12 | 13 | jobs: 14 | deploy: 15 | 16 | runs-on: ubuntu-20.04 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: '3.9' 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | python -m pip install setuptools wheel twine flake8 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | 32 | - name: Lint with flake8 33 | run: | 34 | flake8 . --count --max-line-length=250 --statistics 35 | 36 | - name: Build package 37 | run: python setup.py sdist bdist_wheel 38 | 39 | - name: Publish 40 | env: 41 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 42 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 43 | run: twine upload dist/* 44 | 45 | -------------------------------------------------------------------------------- /.github/workflows/release-windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows release 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | name: Create Release 13 | runs-on: windows-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Set up Python 3.9 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.9 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | python -m pip install -r requirements.txt 27 | python -m pip install pyinstaller 28 | 29 | - name: Build .exe 30 | run: set PYTHONOPTIMIZE=1 && pyinstaller --onefile makejxl.py 31 | 32 | # - name: Run UPX 33 | # uses: crazy-max/ghaction-upx@v1 34 | # with: 35 | # version: latest 36 | # file: ./dist/ExifDateTool.exe 37 | # args: --best 38 | 39 | - name: Create Release 40 | id: create_release 41 | uses: actions/create-release@v1 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 44 | with: 45 | tag_name: ${{ github.ref }} 46 | release_name: Release ${{ github.ref }} 47 | draft: true 48 | prerelease: false 49 | 50 | - name: Upload Release Asset 51 | id: upload-release-asset 52 | uses: actions/upload-release-asset@v1 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | with: 56 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 57 | asset_path: ./dist/makejxl.exe 58 | asset_name: makejxl.exe 59 | asset_content_type: application/vnd.microsoft.portable-executable 60 | -------------------------------------------------------------------------------- /.github/workflows/windows-tools.yml: -------------------------------------------------------------------------------- 1 | name: Windows tools build 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: ubuntu-18.04 10 | 11 | steps: 12 | - name: Install dependencies 13 | run: | 14 | sudo apt-get update 15 | sudo apt-get install cmake clang-7 doxygen g++-8 extra-cmake-modules libgif-dev libjpeg-dev ninja-build libgoogle-perftools-dev git pkg-config libbrotli-dev 16 | sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-7 100 17 | sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-7 100 18 | git clone https://gitlab.com/wg1/jpeg-xl.git --recursive 19 | cd jpeg-xl/docker/scripts/ 20 | sudo bash jpegxl_builder.sh 21 | 22 | - name: Build windows exe 23 | run: | 24 | BUILD_TARGET=x86_64-w64-mingw32 SKIP_TEST=1 ./ci.sh release -DJPEGXL_DEP_LICENSE_DIR=/usr/share/doc -DJPEGXL_STATIC=ON -DBUILD_TESTING=OFF -DJPEGXL_ENABLE_VIEWERS=OFF -DJPEGXL_ENABLE_PLUGINS=OFF -DJPEGXL_ENABLE_OPENEXR=OFF -DJPEGXL_ENABLE_TCMALLOC=OFF -DJPEGXL_ENABLE_FUZZERS=OFF 25 | 26 | # - name: Create Release 27 | # id: create_release 28 | # uses: actions/create-release@v1 29 | # env: 30 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 31 | # with: 32 | # tag_name: ${{ github.ref }} 33 | # release_name: Release ${{ github.ref }} 34 | # draft: true 35 | # prerelease: false 36 | 37 | - name: Upload Release Asset 38 | id: upload-release-asset 39 | uses: actions/upload-release-asset@v1 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 44 | asset_path: build/tools/cjxl.exe 45 | asset_name: cjxl.exe 46 | asset_content_type: application/vnd.microsoft.portable-executable 47 | -------------------------------------------------------------------------------- /.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 | # PyCharm 132 | .idea 133 | /makejxl/__init__.py 134 | -------------------------------------------------------------------------------- /CreateExe.ps1: -------------------------------------------------------------------------------- 1 | & venv\Scripts\Activate.ps1 2 | $env:PYTHONOPTIMIZE = 1 3 | pyinstaller -y --onefile makejxl.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 varnav 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # makejxl 2 | 3 | This tool will bulk encode image files in given directory to [JPEG-XL](https://gitlab.com/wg1/jpeg-xl). It will leave original files intact, creating .jxl file next to each original. 4 | 5 | JPEG-XL encoder is still under development, but format is frozen since 2020-12-24 and files encoded today will be readable by later versions of decoders. 6 | 7 | **You don't need to use this tool at all. Examples for Shell and PowerShell scripts are below.** 8 | 9 | ## Supported input file formats: 10 | 11 | * .jpeg/.jpg (conversion is lossless and reversible) 12 | * .exr 13 | * .gif 14 | * .pfm 15 | * .pgm/.ppm 16 | * .pgx 17 | * .png 18 | 19 | Supports Windows, Linux, MacOS and probably other OSes. 20 | 21 | ## Linux 22 | 23 | ```sh 24 | pip install makejxl 25 | makejxl --recursive /home/username/myphotos 26 | ``` 27 | 28 | You will need cjxl in path. You can get it by running build_cjxl.sh as root 29 | 30 | Or simpler alternative that doesn't need this script: 31 | 32 | ```sh 33 | apt install imagemagick parallel 34 | find /path/to/images -type f -iregex '.*\(gif\|jpe?g\|png\)$' | parallel convert {} {.}.jxl 35 | ``` 36 | 37 | ## Windows 38 | 39 | You can download and use it as single Windows binary, see [Releases](https://github.com/varnav/makejxl/releases/) 40 | 41 | Unfortunately antiviruses [don't like packed Python executables](https://github.com/pyinstaller/pyinstaller/issues?q=is%3Aissue+virus), so expect false positives from them if you go this way. Best way is pip. 42 | 43 | You will need [cjxl](https://gitlab.com/wg1/jpeg-xl/-/blob/master/doc/developing_in_windows.md) in path. It's best to build it using [this tool](https://github.com/m-ab-s/media-autobuild_suite) and copy to `%USERPROFILE%\AppData\Local\Microsoft\WindowsApps` 44 | 45 | ```cmd 46 | ./makejxl.exe "c:\\Users\\username\\Pictures\\My Vacation" 47 | ``` 48 | 49 | Or simpler alternative that doesn't need this script: 50 | 51 | ```powershell 52 | cd c:\photos 53 | Get-ChildItem -Path c:\temp\ -File -Include '*.jpg', '*.jpeg' -Name | Foreach {cjxl $_ $([io.path]::ChangeExtension($_, "jxl"))} 54 | ``` 55 | 56 | ## See also 57 | * [filmcompress](https://github.com/varnav/filmcompress/) 58 | 59 | ## TODO 60 | 61 | * Decoder mode 62 | -------------------------------------------------------------------------------- /build_cjxl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | if [ "$EUID" -ne 0 ] 4 | then echo "Please run as root" 5 | exit 6 | fi 7 | 8 | if [ $(grep -c avx2 /proc/cpuinfo) == 0 ] 9 | then echo "AVX2 support required" 10 | exit 11 | fi 12 | set -ex 13 | 14 | export RUSTFLAGS="-C target-feature=+avx2,+fma" 15 | export CFLAGS="-mavx2 -mfma -ftree-vectorize -pipe" 16 | export MAKEFLAGS=-j$(nproc --ignore=2) 17 | export DEBIAN_FRONTEND=noninteractive 18 | 19 | apt-get update 20 | apt-get -y install python3-pip git wget ca-certificates cmake pkg-config libbrotli-dev libgif-dev libjpeg-dev libopenexr-dev libpng-dev libwebp-dev clang 21 | 22 | cd /tmp/ 23 | rm -rf libjxl 24 | git clone https://github.com/libjxl/libjxl.git --recursive 25 | cd libjxl 26 | rm -rf build 27 | mkdir build 28 | cd build 29 | cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF .. 30 | cmake --build . -- -j$(nproc --ignore=2) 31 | 32 | cp tools/cjxl /usr/bin/ 33 | -------------------------------------------------------------------------------- /makejxl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Based on https://geoffruddock.com/bulk-makejxl-x265-with-ffmpeg/ 3 | 4 | import os 5 | import pathlib 6 | import sys 7 | from shutil import which 8 | from subprocess import run 9 | from typing import Iterable 10 | 11 | import click 12 | 13 | __version__ = '0.1.5' 14 | SUPPORTED_FORMATS = ['jpeg', 'jpg', 'png', 'apng', 'gif', 'exr', 'ppm', 'pfm', 'pgx'] 15 | 16 | 17 | # Ported from: https://github.com/victordomingos/optimize-images 18 | def search_files(dirpath: str, recursive: bool) -> Iterable[str]: 19 | if recursive: 20 | for root, dirs, files in os.walk(dirpath): 21 | for f in files: 22 | if not os.path.isfile(os.path.join(root, f)): 23 | continue 24 | extension = os.path.splitext(f)[1][1:] 25 | if extension.lower() in SUPPORTED_FORMATS: 26 | yield os.path.join(root, f) 27 | else: 28 | with os.scandir(dirpath) as directory: 29 | for f in directory: 30 | if not os.path.isfile(os.path.normpath(f)): 31 | continue 32 | extension = os.path.splitext(f)[1][1:] 33 | if extension.lower() in SUPPORTED_FORMATS: 34 | yield os.path.normpath(f) 35 | 36 | 37 | @click.command() 38 | @click.argument('directory', type=click.Path(exists=True)) 39 | @click.option('-r', '--recursive', is_flag=True, help='Recursive') 40 | @click.option('-s', '--speed', default='kitten', help='Speed') 41 | def main(directory, recursive=False, speed='kitten'): 42 | if which('cjxl') is None: 43 | print('cjxl not found') 44 | exit(1) 45 | 46 | jobs = 1 47 | try: 48 | jobs = len(os.sched_getaffinity(0)) - 1 49 | except Exception: 50 | pass 51 | try: 52 | jobs = int(os.environ['NUMBER_OF_PROCESSORS']) - 1 53 | except Exception: 54 | pass 55 | 56 | total = 0 57 | num = 0 58 | 59 | if recursive: 60 | print('Processing recursively starting from', directory) 61 | recursive = True 62 | else: 63 | print('Processing non-recursively starting from', directory) 64 | recursive = False 65 | 66 | if not os.access(directory, os.W_OK) or not os.path.exists(directory): 67 | print('No such directory or not writable') 68 | sys.exit(1) 69 | 70 | for filepath in search_files(str(directory), recursive=recursive): 71 | fp = pathlib.PurePath(filepath) 72 | newpath = fp.parent.joinpath(fp.stem + '.' + 'jxl') 73 | convert_cmd = ['cjxl', '--quiet', '-s', speed, f'--num_threads={jobs}', fp, newpath] 74 | conversion_return_code = run(convert_cmd).returncode 75 | if conversion_return_code == 0: 76 | saved = os.path.getsize(fp) - os.path.getsize(newpath) 77 | total += saved 78 | num += 1 79 | print(newpath, 'ready, saved', round(saved / 1024), 'KB') 80 | else: 81 | print(fp, 'error') 82 | print('Total saved', round(total / 1024 / 1024), 'MB in', num, 'files') 83 | 84 | 85 | if __name__ == '__main__': 86 | main() 87 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click~=7.1.2 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import os 3 | import shutil 4 | import makejxl 5 | 6 | if not os.path.exists('makejxl'): 7 | os.mkdir('makejxl') 8 | shutil.copyfile('makejxl.py', 'makejxl/__init__.py') 9 | 10 | with open("README.md", "r") as fh: 11 | long_description = fh.read() 12 | 13 | install_requires = [ 14 | 'click>=7.1.2' 15 | ] 16 | 17 | setuptools.setup( 18 | name="makejxl", 19 | version=makejxl.__version__, 20 | author="Evgeny Varnavskiy", 21 | author_email="varnavruz@gmail.com", 22 | description="This tool will bulk encode image files in given directory to JPEG-XL", 23 | long_description=long_description, 24 | long_description_content_type="text/markdown", 25 | url="https://github.com/varnav/makejxl", 26 | keywords=["jpeg", "jpeg-xl", "transcoder"], 27 | packages=setuptools.find_packages(), 28 | install_requires=install_requires, 29 | classifiers=[ 30 | "Development Status :: 4 - Beta", 31 | "Environment :: MacOS X", 32 | "Environment :: Win32 (MS Windows)", 33 | "Intended Audience :: End Users/Desktop", 34 | "License :: OSI Approved :: MIT License", 35 | "Operating System :: OS Independent", 36 | "Topic :: Scientific/Engineering :: Image Processing", 37 | "Topic :: Utilities", 38 | "Topic :: Multimedia :: Graphics :: Graphics Conversion", 39 | "Environment :: Console", 40 | "Programming Language :: Python :: 3.9", 41 | "Programming Language :: Python :: 3 :: Only", 42 | ], 43 | python_requires='>=3.7', 44 | entry_points={ 45 | "console_scripts": [ 46 | "makejxl = makejxl:main", 47 | ] 48 | } 49 | ) 50 | -------------------------------------------------------------------------------- /test_main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from PIL import Image 4 | import tempfile 5 | from click.testing import CliRunner 6 | 7 | from makejxl import main as conv 8 | 9 | tempdir = tempfile.mkdtemp() + os.sep 10 | # tempdir = '/mnt/c/temp/2/' 11 | 12 | n1 = tempdir + 'testimage1.jpg' 13 | n2 = tempdir + 'testimage with space.jpg' 14 | n3 = tempdir + 'testimage-with-dash.jpg' 15 | n4 = tempdir + 'testimage.with.dots.jpg' 16 | n5 = tempdir + 'файлик на кириллице.jpg' 17 | 18 | r1 = tempdir + 'testimage1.jxl' 19 | r2 = tempdir + 'testimage with space.jxl' 20 | r3 = tempdir + 'testimage-with-dash.jxl' 21 | r4 = tempdir + 'testimage.with.dots.jxl' 22 | r5 = tempdir + 'файлик на кириллице.jxl' 23 | 24 | # Generate test images 25 | img = Image.new('RGB', (1024, 1024), color='white') 26 | img.save(n1) 27 | img.save(n2) 28 | img.save(n3) 29 | img.save(n4) 30 | img.save(n5) 31 | 32 | 33 | def test_1(): 34 | runner = CliRunner() 35 | result = runner.invoke(conv, tempdir) 36 | assert result.exit_code == 0 37 | 38 | 39 | def test_2(): 40 | assert os.path.exists(r1) 41 | 42 | 43 | def test_3(): 44 | assert os.path.exists(r2) 45 | 46 | 47 | def test_4(): 48 | assert os.path.exists(r3) 49 | 50 | 51 | def test_5(): 52 | assert os.path.exists(r4) 53 | 54 | 55 | def test_6(): 56 | assert os.path.exists(r5) 57 | --------------------------------------------------------------------------------