├── zopfli ├── py.typed ├── _zopfli.pyi ├── _zopfli │ ├── _zopflimodule.h │ ├── _zopflimodule.c │ └── zopflipng.cpp └── __init__.py ├── .gitmodules ├── .gitignore ├── MANIFEST.in ├── tox.ini ├── setup.py ├── .github └── workflows │ ├── codeql.yml │ ├── wheel.yml │ └── ci.yml ├── pyproject.toml ├── CHANGES.rst ├── README.rst ├── LICENSE.txt └── tests └── test_zopfli.py /zopfli/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "_zopfli/zopfli"] 2 | path = zopfli/_zopfli/zopfli 3 | url = https://github.com/google/zopfli 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | .tox/ 3 | build/ 4 | dist/ 5 | *.py? 6 | *.so 7 | *.sw? 8 | .coverage 9 | MANIFEST 10 | zopfli/__version__.py 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.rst 2 | include LICENSE.txt 3 | include MANIFEST.in 4 | include README.rst 5 | include pyproject.toml 6 | recursive-include tests *.py 7 | recursive-include zopfli/_zopfli *.h CONTRIBUTORS COPYING README* 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.3 3 | envlist = py3{10-14}, py3{13-14}t 4 | isolated_build = True 5 | 6 | [testenv] 7 | deps = 8 | coverage[toml] >= 5.0 9 | setuptools >= 61.0 10 | ruff 11 | mypy 12 | scmver[toml] >= 1.7 13 | passenv = *FLAGS, INCLUDE, LC_*, LIB, MSSdk, Program*, PYTHON* 14 | commands = 15 | python setup.py build_ext --inplace 16 | # test 17 | coverage erase 18 | coverage run --source=zopfli -m unittest discover -s tests {posargs} 19 | coverage report 20 | # lint 21 | ruff check 22 | # type 23 | mypy zopfli 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # 3 | # setup.py -- zopflipy setup script 4 | # 5 | 6 | import os 7 | import sysconfig 8 | 9 | from setuptools import setup, Extension 10 | 11 | 12 | def sources(path, exts): 13 | for root, dirs, files in os.walk(path): 14 | dirs[:] = (d for d in dirs if not d.startswith('.')) 15 | for f in files: 16 | n, ext = os.path.splitext(f) 17 | if (ext in exts 18 | and not n.endswith('_bin')): 19 | yield os.path.normpath(os.path.join(root, f)) 20 | 21 | 22 | setup( 23 | ext_modules=[Extension('zopfli._zopfli', 24 | sources=list(sources('zopfli', ['.c', '.cc', '.cpp'])), 25 | include_dirs=[os.path.join('zopfli', '_zopfli', 'zopfli', 'src')], 26 | define_macros=[('Py_GIL_DISABLED', 1)] if sysconfig.get_config_var('Py_GIL_DISABLED') else [])], 27 | ) 28 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 6 * * 3' 7 | permissions: 8 | security-events: write 9 | jobs: 10 | analyze: 11 | strategy: 12 | matrix: 13 | language: 14 | - C++ 15 | - Python 16 | fail-fast: false 17 | name: ${{ matrix.language }} 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v5 22 | with: 23 | persist-credentials: false 24 | submodules: recursive 25 | - name: Setup Python 26 | uses: actions/setup-python@v6 27 | with: 28 | python-version: 3.x 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install -U pip setuptools 'scmver[toml]' 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v4 34 | with: 35 | languages: ${{ matrix.language }} 36 | queries: +security-and-quality 37 | - name: Autobuild 38 | uses: github/codeql-action/autobuild@v4 39 | - name: Perform CodeQL analysis 40 | uses: github/codeql-action/analyze@v4 41 | -------------------------------------------------------------------------------- /.github/workflows/wheel.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | platform: 10 | - Linux 11 | - macOS 12 | - Windows 13 | include: 14 | - platform: Linux 15 | os: ubuntu-latest 16 | - platform: macOS 17 | os: macos-latest 18 | - platform: Windows 19 | os: windows-latest 20 | name: Build on ${{ matrix.platform }} 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v5 25 | with: 26 | fetch-depth: 0 27 | persist-credentials: false 28 | submodules: recursive 29 | - name: Build 30 | uses: pypa/cibuildwheel@v3.2 31 | env: 32 | CIBW_SKIP: '*musllinux*' 33 | CIBW_ENABLE: cpython-freethreading 34 | CIBW_ARCHS_MACOS: universal2 35 | - name: Upload artifacts 36 | uses: actions/upload-artifact@v5 37 | with: 38 | name: dist-${{ matrix.platform }} 39 | path: wheelhouse 40 | merge: 41 | name: Merge artifacts 42 | needs: build 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Merge artifacts 46 | uses: actions/upload-artifact/merge@v5 47 | with: 48 | name: dist 49 | delete-merged: true 50 | -------------------------------------------------------------------------------- /zopfli/_zopfli.pyi: -------------------------------------------------------------------------------- 1 | # 2 | # zopfli._zopfli 3 | # 4 | # Copyright (c) 2021-2025 Akinori Hattori 5 | # 6 | # SPDX-License-Identifier: MIT 7 | # 8 | 9 | from collections.abc import Sequence 10 | 11 | 12 | ZOPFLI_FORMAT_GZIP: int 13 | ZOPFLI_FORMAT_ZLIB: int 14 | ZOPFLI_FORMAT_DEFLATE: int 15 | 16 | 17 | class ZopfliCompressor: 18 | 19 | def __init__(self, format: int = ..., verbose: bool | None = ..., iterations: int = ..., 20 | block_splitting: bool | None = ..., block_splitting_max: int = ...) -> None: ... 21 | def compress(self, data: bytes) -> bytes: ... 22 | def flush(self) -> bytes: ... 23 | 24 | 25 | class ZopfliDeflater: 26 | 27 | def __init__(self, verbose: bool | None = ..., iterations: int = ..., 28 | block_splitting: bool | None = ..., block_splitting_max: int = ...) -> None: ... 29 | def compress(self, data: bytes) -> bytes: ... 30 | def flush(self) -> bytes: ... 31 | 32 | 33 | class ZopfliPNG: 34 | 35 | verbose: bool 36 | lossy_transparent: bool 37 | lossy_8bit: bool 38 | filter_strategies: str 39 | auto_filter_strategy: bool 40 | keep_color_type: bool 41 | keep_chunks: tuple[str, ...] 42 | use_zopfli: bool 43 | iterations: int 44 | iterations_large: int 45 | 46 | def __init__(self, verbose: bool | None = ..., lossy_transparent: bool | None = ..., lossy_8bit: bool | None = ..., filter_strategies: str = ..., 47 | auto_filter_strategy: bool | None = ..., keep_color_type: bool | None = ..., keep_chunks: Sequence[str] = ..., 48 | use_zopfli: bool | None = ..., iterations: int = ..., iterations_large: int = ...) -> None: ... 49 | def optimize(self, data: bytes) -> bytes: ... 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 77.0", 4 | "scmver[toml] >= 1.7", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [project] 9 | name = "zopflipy" 10 | description = "A Python bindings for Zopfli" 11 | readme = "README.rst" 12 | authors = [ 13 | {name = "Akinori Hattori", email = "hattya@gmail.com"}, 14 | ] 15 | license = "Apache-2.0" 16 | classifiers = [ 17 | "Development Status :: 5 - Production/Stable", 18 | "Intended Audience :: Developers", 19 | "Operating System :: OS Independent", 20 | "Programming Language :: C", 21 | "Programming Language :: C++", 22 | "Programming Language :: Python :: 3", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | "Programming Language :: Python :: 3.14", 28 | "Topic :: Software Development :: Libraries :: Python Modules", 29 | "Topic :: System :: Archiving :: Compression", 30 | "Typing :: Typed", 31 | ] 32 | requires-python = ">= 3.10" 33 | dynamic = [ 34 | "version", 35 | ] 36 | 37 | [project.urls] 38 | Homepage = "https://github.com/hattya/zopflipy" 39 | 40 | [tool.setuptools] 41 | include-package-data = false 42 | packages = [ 43 | "zopfli", 44 | ] 45 | 46 | [tool.setuptools.package-data] 47 | zopfli = [ 48 | "py.typed", 49 | "*.pyi", 50 | ] 51 | 52 | [tool.scmver] 53 | spec = "micro" 54 | write-to = "zopfli/__version__.py" 55 | fallback = {attr = "__version__:version", path = "zopfli"} 56 | 57 | [tool.coverage.run] 58 | branch = true 59 | 60 | [tool.coverage.report] 61 | partial_branches = [ 62 | "pragma: no partial", 63 | "if sys.version_info", 64 | ] 65 | 66 | [tool.mypy] 67 | disable_error_code = [ 68 | "attr-defined", 69 | "misc", 70 | "override", 71 | ] 72 | strict = true 73 | 74 | [tool.ruff] 75 | line-length = 160 76 | 77 | [tool.ruff.lint] 78 | select = [ 79 | "E", 80 | "W", 81 | "F", 82 | "UP", 83 | "B0", 84 | "C4", 85 | ] 86 | ignore = [ 87 | "E74", 88 | ] 89 | 90 | [tool.ruff.lint.pyupgrade] 91 | keep-runtime-typing = true 92 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | ZopfliPy Changelog 2 | ================== 3 | 4 | Version 1.12 5 | ------------ 6 | 7 | Release date: 2025-10-31 8 | 9 | * Add support for Free Threading. 10 | * Support Python 3.14. 11 | * Drop Python 3.9 support. 12 | * Update Zopfli to commit ccf9f0588d4a4509cb1040310ec122243e670ee6. 13 | 14 | 15 | Version 1.11 16 | ------------ 17 | 18 | Release date: 2024-10-18 19 | 20 | * Drop Python 3.8 support. 21 | * Support Python 3.13. 22 | 23 | 24 | Version 1.10 25 | ------------ 26 | 27 | Release date: 2024-02-04 28 | 29 | * Improve type annotations. 30 | 31 | 32 | Version 1.9 33 | ----------- 34 | 35 | Release date: 2023-10-15 36 | 37 | * Drop Python 3.7 support. 38 | * Support Python 3.12. 39 | 40 | 41 | Version 1.8 42 | ----------- 43 | 44 | Release date: 2022-11-08 45 | 46 | * Drop Python 3.6 support. 47 | * Improve compatibility of ``ZipFile`` with ``zipfile.ZipFile``. 48 | * Support Python 3.11. 49 | 50 | 51 | Version 1.7 52 | ----------- 53 | 54 | Release date: 2021-11-07 55 | 56 | * Support Python 3.10. 57 | 58 | 59 | Version 1.6 60 | ----------- 61 | 62 | Release date: 2021-08-30 63 | 64 | * Add ``keep_color_type`` attribute to the ``ZopfliPNG`` class. 65 | * Drop Python 2.7 support. 66 | * Add type annotations. 67 | 68 | 69 | Version 1.5 70 | ----------- 71 | 72 | Release date: 2021-01-11 73 | 74 | * Update Zopfli to commit 6673e39fba6122c948c9ec34f07166812d473eb6. 75 | 76 | 77 | Version 1.4 78 | ----------- 79 | 80 | Release date: 2020-11-08 81 | 82 | * Drop Python 3.5 support. 83 | * Support Python 3.9. 84 | 85 | 86 | Version 1.3 87 | ----------- 88 | 89 | Release date: 2019-12-04 90 | 91 | * Update Zopfli to version 1.0.3. 92 | 93 | 94 | Version 1.2 95 | ----------- 96 | 97 | Release date: 2019-11-27 98 | 99 | * Drop Python 3.4 support. 100 | * Support Python 3.8. 101 | 102 | 103 | Version 1.1 104 | ----------- 105 | 106 | Release date: 2018-07-17 107 | 108 | * Update Zopfli to version 1.0.2. 109 | * Drop Python 3.3 support. 110 | * Support Python 3.7. 111 | 112 | 113 | Version 1.0 114 | ----------- 115 | 116 | Release date: 2017-09-26 117 | 118 | * Initial release. 119 | -------------------------------------------------------------------------------- /zopfli/_zopfli/_zopflimodule.h: -------------------------------------------------------------------------------- 1 | /* 2 | * zopfli._zopfli :: _zopflimodule.h 3 | * 4 | * Copyright (c) 2015-2025 Akinori Hattori 5 | * 6 | * SPDX-License-Identifier: Apache-2.0 7 | */ 8 | 9 | #ifndef ZOPFLIPY_H 10 | # define ZOPFLIPY_H 11 | 12 | # define PY_SSIZE_T_CLEAN 13 | # include 14 | # include 15 | # ifdef WITH_THREAD 16 | # include 17 | # endif 18 | 19 | # ifdef __cplusplus 20 | extern "C" { 21 | # endif /* __cplusplus */ 22 | 23 | 24 | # define MODULE "_zopfli" 25 | 26 | # ifdef WITH_THREAD 27 | # define ALLOCATE_LOCK(self) \ 28 | do { \ 29 | if ((self)->lock != NULL) { \ 30 | break; \ 31 | } \ 32 | (self)->lock = PyThread_allocate_lock(); \ 33 | if ((self)->lock == NULL) { \ 34 | PyErr_SetString(PyExc_MemoryError, "unable to allocate lock"); \ 35 | } \ 36 | } while (0) 37 | # define FREE_LOCK(self) \ 38 | do { \ 39 | if ((self)->lock != NULL) { \ 40 | PyThread_free_lock((self)->lock); \ 41 | } \ 42 | } while (0) 43 | # define ACQUIRE_LOCK(self) \ 44 | do { \ 45 | if (!PyThread_acquire_lock((self)->lock, NOWAIT_LOCK)) { \ 46 | Py_BEGIN_ALLOW_THREADS \ 47 | PyThread_acquire_lock((self)->lock, WAIT_LOCK); \ 48 | Py_END_ALLOW_THREADS \ 49 | } \ 50 | } while (0) 51 | # define RELEASE_LOCK(self) PyThread_release_lock((self)->lock) 52 | # else 53 | # define FREE_LOCK(self) 54 | # define ACQUIRE_LOCK(self) 55 | # define RELEASE_LOCK(self) 56 | # endif 57 | 58 | 59 | extern PyTypeObject Compressor_Type; 60 | extern PyTypeObject Deflater_Type; 61 | extern PyTypeObject PNG_Type; 62 | 63 | 64 | # ifdef __cplusplus 65 | } 66 | # endif /* __cplusplus */ 67 | 68 | #endif /* ZOPFLIPY_H */ 69 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ZopfliPy 2 | ======== 3 | 4 | A Python_ bindings for Zopfli_. 5 | 6 | .. image:: https://img.shields.io/pypi/v/zopflipy.svg 7 | :target: https://pypi.org/project/zopflipy 8 | 9 | .. image:: https://github.com/hattya/zopflipy/actions/workflows/ci.yml/badge.svg 10 | :target: https://github.com/hattya/zopflipy/actions/workflows/ci.yml 11 | 12 | .. image:: https://ci.appveyor.com/api/projects/status/98a7e7d6qlkvs6vl/branch/master?svg=true 13 | :target: https://ci.appveyor.com/project/hattya/zopflipy 14 | 15 | .. image:: https://codecov.io/gh/hattya/zopflipy/branch/master/graph/badge.svg 16 | :target: https://codecov.io/gh/hattya/zopflipy 17 | 18 | .. _Python: https://www.python.org/ 19 | .. _Zopfli: https://github.com/google/zopfli 20 | 21 | 22 | Installation 23 | ------------ 24 | 25 | .. code:: console 26 | 27 | $ pip install zopflipy 28 | 29 | 30 | Requirements 31 | ------------ 32 | 33 | - Python 3.10+ 34 | 35 | 36 | Usage 37 | ----- 38 | 39 | ZopfliCompressor 40 | ~~~~~~~~~~~~~~~~ 41 | 42 | .. code:: pycon 43 | 44 | >>> import zopfli 45 | >>> c = zopfli.ZopfliCompressor(zopfli.ZOPFLI_FORMAT_DEFLATE) 46 | >>> z = c.compress(b'Hello, world!') + c.flush() 47 | >>> d = zopfli.ZopfliDecompressor(zopfli.ZOPFLI_FORMAT_DEFLATE) 48 | >>> d.decompress(z) + d.flush() 49 | b'Hello, world!'' 50 | 51 | 52 | ZopfliDeflater 53 | ~~~~~~~~~~~~~~ 54 | 55 | .. code:: pycon 56 | 57 | >>> import zopfli 58 | >>> c = zopfli.ZopfliDeflater() 59 | >>> z = c.compress(b'Hello, world!') + c.flush() 60 | >>> d = zopfli.ZopfliDecompressor(zopfli.ZOPFLI_FORMAT_DEFLATE) 61 | >>> d.decompress(z) + d.flush() 62 | b'Hello, world!'' 63 | 64 | 65 | ZopfliPNG 66 | ~~~~~~~~~ 67 | 68 | .. code:: pycon 69 | 70 | >>> import zopfli 71 | >>> png = zopfli.ZopfliPNG() 72 | >>> with open('in.png', 'rb') as fp: 73 | ... data = fp.read() 74 | >>> len(png.optimize(data)) < len(data) 75 | True 76 | 77 | 78 | ZipFile 79 | ~~~~~~~ 80 | 81 | A subclass of |zipfile.ZipFile|_ which uses |ZopfliCompressor|_ for the 82 | |zipfile.ZIP_DEFLATED|_ compression method. 83 | 84 | .. code:: pycon 85 | 86 | >>> import zipfile 87 | >>> import zopfli 88 | >>> with zopfli.ZipFile('a.zip', 'w', zipfile.ZIP_DEFLATED) as zf: 89 | ... zf.writestr('a.txt', b'Hello, world!') 90 | 91 | 92 | .. |zipfile.ZipFile| replace:: ``zipfile.ZipFile`` 93 | .. _zipfile.ZipFile: https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile 94 | .. |ZopfliCompressor| replace:: ``ZopfliCompressor`` 95 | .. |zipfile.ZIP_DEFLATED| replace:: ``zipfile.ZIP_DEFLATED`` 96 | .. _zipfile.ZIP_DEFLATED: https://docs.python.org/3/library/zipfile.html#zipfile.ZIP_DEFLATED 97 | 98 | 99 | License 100 | ------- 101 | 102 | ZopfliPy is distributed under the terms of the Apache License, Version 2.0. 103 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | platform: 10 | - Linux 11 | - macOS 12 | - Windows 13 | python-version: 14 | - '3.10' 15 | - '3.11' 16 | - '3.12' 17 | - '3.13' 18 | - '3.13t' 19 | - '3.14' 20 | - '3.14t' 21 | architecture: 22 | - x86 23 | - x64 24 | - arm64 25 | include: 26 | - platform: Linux 27 | os: ubuntu-latest 28 | - platform: Linux 29 | os: ubuntu-24.04-arm 30 | architecture: arm64 31 | - platform: macOS 32 | os: macos-latest 33 | - platform: macOS 34 | os: macos-15-intel 35 | architecture: x64 36 | - platform: Windows 37 | os: windows-latest 38 | exclude: 39 | - platform: Linux 40 | architecture: x86 41 | - platform: macOS 42 | architecture: x86 43 | - platform: Windows 44 | architecture: arm64 45 | fail-fast: false 46 | name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) on ${{ matrix.platform }} 47 | runs-on: ${{ matrix.os }} 48 | env: 49 | CFLAGS: -Wall -Wextra --coverage 50 | CXXFLAGS: -Wall -Wextra --coverage 51 | HOMEBREW_NO_ANALYTICS: 1 52 | HOMEBREW_NO_AUTO_UPDATE: 1 53 | timeout-minutes: 10 54 | steps: 55 | - name: Checkout code 56 | uses: actions/checkout@v5 57 | with: 58 | persist-credentials: false 59 | submodules: recursive 60 | - name: Setup Python 61 | uses: actions/setup-python@v6 62 | with: 63 | python-version: ${{ matrix.python-version }} 64 | architecture: ${{ matrix.architecture }} 65 | - name: Install dependencies 66 | run: | 67 | python -m pip install -U pip 68 | python -m pip install -U coverage tox 69 | - name: Install LCOV on Linux 70 | if: matrix.platform == 'Linux' 71 | run: sudo apt install lcov 72 | - name: Install LCOV on macOS 73 | if: matrix.platform == 'macOS' 74 | run: brew install lcov 75 | - name: Test 76 | run: tox -e py 77 | - name: Generate LCOV trace file 78 | if: matrix.platform != 'Windows' 79 | run: | 80 | lcov -c -d . -o lcov.info --no-external --exclude '*/_zopfli/zopfli/*' 81 | rm -rf build 82 | - name: Upload coverage to Codecov 83 | uses: codecov/codecov-action@v5 84 | with: 85 | token: ${{ secrets.CODECOV_TOKEN }} 86 | env_vars: PYTHON_VERSION,ARCH 87 | fail_ci_if_error: true 88 | flags: ${{ matrix.platform }} 89 | env: 90 | PYTHON_VERSION: ${{ matrix.python-version }} 91 | ARCH: ${{ matrix.architecture }} 92 | -------------------------------------------------------------------------------- /zopfli/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # zopfli 3 | # 4 | # Copyright (c) 2015-2025 Akinori Hattori 5 | # 6 | # SPDX-License-Identifier: Apache-2.0 7 | # 8 | 9 | """Zopfli Compression Algorithm""" 10 | 11 | from __future__ import annotations 12 | import codecs 13 | import os 14 | import struct 15 | import sys 16 | import threading 17 | from typing import cast, Any, AnyStr, IO, Literal, TypeAlias 18 | import zipfile 19 | import zlib 20 | 21 | from ._zopfli import (ZOPFLI_FORMAT_GZIP, ZOPFLI_FORMAT_ZLIB, ZOPFLI_FORMAT_DEFLATE, 22 | ZopfliCompressor, ZopfliDeflater, ZopfliPNG) 23 | 24 | 25 | __all__ = ['ZOPFLI_FORMAT_GZIP', 'ZOPFLI_FORMAT_ZLIB', 'ZOPFLI_FORMAT_DEFLATE', 26 | 'ZopfliCompressor', 'ZopfliDeflater', 'ZopfliDecompressor', 'ZopfliPNG', 27 | 'ZipFile', 'ZipInfo'] 28 | __author__ = 'Akinori Hattori ' 29 | try: 30 | from .__version__ import version as __version__ 31 | except ImportError: 32 | __version__ = 'unknown' 33 | 34 | P: TypeAlias = str | os.PathLike[str] 35 | 36 | 37 | class ZopfliDecompressor: 38 | 39 | def __init__(self, format: int = ZOPFLI_FORMAT_DEFLATE) -> None: 40 | if format == ZOPFLI_FORMAT_GZIP: 41 | wbits = zlib.MAX_WBITS + 16 42 | elif format == ZOPFLI_FORMAT_ZLIB: 43 | wbits = zlib.MAX_WBITS 44 | elif format == ZOPFLI_FORMAT_DEFLATE: 45 | wbits = -zlib.MAX_WBITS 46 | else: 47 | raise ValueError('unknown format') 48 | self.__z = zlib.decompressobj(wbits) 49 | 50 | @property 51 | def unused_data(self) -> bytes: 52 | return self.__z.unused_data 53 | 54 | @property 55 | def unconsumed_tail(self) -> bytes: 56 | return self.__z.unconsumed_tail 57 | 58 | @property 59 | def eof(self) -> bool: 60 | return self.__z.eof 61 | 62 | def decompress(self, data: bytes, max_length: int = 0) -> bytes: 63 | return self.__z.decompress(data, max_length) 64 | 65 | def flush(self, length: int = zlib.DEF_BUF_SIZE) -> bytes: 66 | return self.__z.flush(length) 67 | 68 | 69 | class ZipFile(zipfile.ZipFile): 70 | 71 | fp: IO[bytes] 72 | compression: int 73 | _lock: threading.RLock 74 | 75 | def __init__(self, file: P | IO[bytes], mode: Literal['r', 'w', 'x', 'a'] = 'r', compression: int = zipfile.ZIP_DEFLATED, allowZip64: bool = True, 76 | compresslevel: int | None = None, *, strict_timestamps: bool = True, encoding: str = 'cp437', **kwargs: Any) -> None: 77 | self.encoding = encoding 78 | self._options = kwargs 79 | super().__init__(file, mode, compression, allowZip64, compresslevel) 80 | self._strict_timestamps = strict_timestamps 81 | 82 | def _RealGetContents(self) -> None: 83 | super()._RealGetContents() 84 | for i, zi in enumerate(self.filelist): 85 | self.filelist[i] = zi = self._convert(zi) 86 | if not zi.flag_bits & 0x800: 87 | n = zi.orig_filename.encode('cp437').decode(self.encoding) 88 | if os.sep != '/': 89 | n = n.replace(os.sep, '/') 90 | del self.NameToInfo[zi.filename] 91 | zi.filename = n 92 | self.NameToInfo[zi.filename] = zi 93 | 94 | def open(self, name: str | zipfile.ZipInfo, mode: Literal['r', 'w'] = 'r', pwd: bytes | None = None, 95 | *, force_zip64: bool = False, **kwargs: Any) -> IO[bytes]: 96 | fp = super().open(name, mode, pwd, force_zip64=force_zip64) 97 | if (mode == 'w' 98 | and self._zopflify(None) 99 | and fp._compressor): 100 | fp._compressor = ZopfliCompressor(ZOPFLI_FORMAT_DEFLATE, **self._options | kwargs) 101 | return fp 102 | 103 | def _open_to_write(self, zinfo: zipfile.ZipInfo, force_zip64: bool = False) -> IO[bytes]: 104 | return cast(IO[bytes], super()._open_to_write(self._convert(zinfo), force_zip64)) 105 | 106 | def write(self, filename: P, arcname: P | None = None, 107 | compress_type: int | None = None, compresslevel: int | None = None, **kwargs: Any) -> None: 108 | zopflify = self._zopflify(compress_type) 109 | z: ZopfliCompressor | None = None 110 | if zopflify: 111 | compress_type = zipfile.ZIP_STORED 112 | z = ZopfliCompressor(ZOPFLI_FORMAT_DEFLATE, **self._options | kwargs) 113 | with self._lock: 114 | fp = self.fp 115 | try: 116 | self.fp = self._file(z) 117 | super().write(filename, arcname, compress_type, compresslevel) 118 | zi = self._convert(self.filelist[-1]) 119 | if zopflify: 120 | zi.compress_size = self.fp.size 121 | if not zi.is_dir(): 122 | zi.compress_type = zipfile.ZIP_DEFLATED 123 | finally: 124 | self.fp = fp 125 | if zopflify: 126 | self.fp.seek(zi.header_offset) 127 | self.fp.write(zi.FileHeader(self._zip64(zi))) 128 | self.fp.seek(self.start_dir) 129 | self.filelist[-1] = zi 130 | self.NameToInfo[zi.filename] = zi 131 | 132 | def writestr(self, zinfo_or_arcname: str | zipfile.ZipInfo, data: AnyStr, 133 | compress_type: int | None = None, compresslevel: int | None = None, **kwargs: Any) -> None: 134 | if isinstance(zinfo_or_arcname, zipfile.ZipInfo): 135 | compress_type = zinfo_or_arcname.compress_type 136 | if isinstance(zinfo_or_arcname, ZipInfo): 137 | zinfo_or_arcname.encoding = self.encoding 138 | zopflify = self._zopflify(compress_type) 139 | z: ZopfliCompressor | None = None 140 | if zopflify: 141 | compress_type = zipfile.ZIP_STORED 142 | z = ZopfliCompressor(ZOPFLI_FORMAT_DEFLATE, **self._options | kwargs) 143 | with self._lock: 144 | fp = self.fp 145 | try: 146 | self.fp = self._file(z) 147 | super().writestr(zinfo_or_arcname, data, compress_type, compresslevel) 148 | zi = self._convert(self.filelist[-1]) 149 | if zopflify: 150 | zi.compress_type = zipfile.ZIP_DEFLATED 151 | zi.compress_size = self.fp.size 152 | finally: 153 | self.fp = fp 154 | if zopflify: 155 | self.fp.seek(zi.header_offset) 156 | self.fp.write(zi.FileHeader(self._zip64(zi))) 157 | self.fp.seek(self.start_dir) 158 | self.filelist[-1] = zi 159 | self.NameToInfo[zi.filename] = zi 160 | 161 | if sys.version_info >= (3, 11): 162 | def mkdir(self, zinfo_or_directory: str | zipfile.ZipInfo, mode: int = 511) -> None: 163 | with self._lock: 164 | fp = self.fp 165 | try: 166 | self.fp = self._file(None) 167 | super().mkdir(zinfo_or_directory, mode) 168 | zi = self._convert(self.filelist[-1]) 169 | finally: 170 | self.fp = fp 171 | self.filelist[-1] = zi 172 | self.NameToInfo[zi.filename] = zi 173 | 174 | def _convert(self, src: zipfile.ZipInfo) -> ZipInfo: 175 | if isinstance(src, ZipInfo): 176 | dst = src 177 | else: 178 | dst = ZipInfo() 179 | for n in src.__slots__: 180 | try: 181 | setattr(dst, n, getattr(src, n)) 182 | except AttributeError: 183 | pass 184 | dst.encoding = self.encoding 185 | return dst 186 | 187 | def _file(self, z: ZopfliCompressor | None) -> IO[bytes]: 188 | LFH = '<4s5H3L2H' 189 | EFS = 1 << 11 190 | 191 | class ZopfliFile: 192 | 193 | def __init__(self, zf: ZipFile, z: ZopfliCompressor | None) -> None: 194 | self.size = 0 195 | self._zf = zf 196 | self._fp = zf.fp 197 | self._pos = zf.start_dir 198 | self._z = z 199 | self._fh = 0 200 | 201 | def __getattr__(self, name: str) -> Any: 202 | return getattr(self._fp, name) 203 | 204 | def seek(self, offset: int, whence: int = os.SEEK_SET) -> int: 205 | if (offset == self._pos 206 | and whence == os.SEEK_SET): 207 | self._fh = -self._fh + 1 208 | if (self._fh > 1 209 | and self._z): 210 | data = self._z.flush() 211 | self.size += len(data) 212 | self._fp.write(data) 213 | self._z = None 214 | self._zf.start_dir = self._fp.tell() 215 | return self._fp.seek(offset, whence) 216 | 217 | def write(self, data: bytes) -> None: 218 | if self._fh > 0: 219 | self._fp.write(self._rewrite(data)) 220 | self._fh = -self._fh 221 | else: 222 | if self._z: 223 | data = self._z.compress(data) 224 | self.size += len(data) 225 | self._fp.write(data) 226 | 227 | def _rewrite(self, fh: bytes) -> bytes: 228 | sig, ver, flag, meth, lmt, lmd, crc, csize, fsize, n, m = struct.unpack(LFH, fh[:30]) 229 | if flag & EFS: 230 | try: 231 | name = fh[30:30+n].decode('utf-8').encode(self._zf.encoding) 232 | if name != fh[30:30+n]: 233 | return struct.pack(LFH, sig, ver, flag & ~EFS, meth, lmt, lmd, crc, csize, fsize, len(name), m) + name + fh[30+n:] 234 | except UnicodeEncodeError: 235 | pass 236 | return fh 237 | 238 | return cast(IO[bytes], ZopfliFile(self, z)) 239 | 240 | def _zip64(self, zi: zipfile.ZipInfo) -> bool: 241 | return (zi.file_size > zipfile.ZIP64_LIMIT 242 | or zi.compress_size > zipfile.ZIP64_LIMIT) 243 | 244 | def _zopflify(self, compression: int | None) -> bool: 245 | return (compression == zipfile.ZIP_DEFLATED 246 | or (compression is None 247 | and self.compression == zipfile.ZIP_DEFLATED)) 248 | 249 | 250 | class ZipInfo(zipfile.ZipInfo): 251 | 252 | __slots__ = ('encoding',) 253 | 254 | encoding: str | None 255 | orig_filename: str 256 | 257 | def __init__(self, *args: Any, **kwargs: Any) -> None: 258 | super().__init__(*args, **kwargs) 259 | self.encoding = None 260 | 261 | def _encodeFilenameFlags(self) -> tuple[bytes, int]: 262 | if isinstance(self.filename, bytes): 263 | return self.filename, self.flag_bits 264 | encoding = codecs.lookup(self.encoding).name if self.encoding else 'ascii' 265 | if encoding != 'utf-8': 266 | try: 267 | return self.filename.encode(encoding), self.flag_bits 268 | except UnicodeEncodeError: 269 | pass 270 | return self.filename.encode('utf-8'), self.flag_bits | 0x800 271 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /zopfli/_zopfli/_zopflimodule.c: -------------------------------------------------------------------------------- 1 | /* 2 | * zopfli._zopfli :: _zopflimodule.c 3 | * 4 | * Copyright (c) 2015-2024 Akinori Hattori 5 | * 6 | * SPDX-License-Identifier: Apache-2.0 7 | */ 8 | 9 | #include "_zopflimodule.h" 10 | 11 | #include "zopfli/zopfli.h" 12 | #include "zopfli/deflate.h" 13 | 14 | 15 | #define PARSE_BOOL(self, var) \ 16 | do { \ 17 | (self)->options.var = PyObject_IsTrue(var); \ 18 | if ((self)->options.var < 0) { \ 19 | return -1; \ 20 | } \ 21 | } while (0) 22 | 23 | 24 | typedef struct { 25 | PyObject_HEAD 26 | ZopfliFormat format; 27 | ZopfliOptions options; 28 | PyObject *data; 29 | int flushed; 30 | #ifdef WITH_THREAD 31 | PyThread_type_lock lock; 32 | #endif 33 | } Compressor; 34 | 35 | static void 36 | Compressor_dealloc(Compressor *self) { 37 | Py_XDECREF(self->data); 38 | FREE_LOCK(self); 39 | Py_TYPE(self)->tp_free((PyObject *)self); 40 | } 41 | 42 | PyDoc_STRVAR(Compressor__doc__, 43 | "ZopfliCompressor(format=ZOPFLI_FORMAT_DEFLATE, verbose=False," 44 | " iterations=15, block_splitting=True, block_splitting_max=15)\n" 45 | "\n" 46 | "Create a compressor object which is using the ZopfliCompress()\n" 47 | "function for compressing data."); 48 | 49 | static int 50 | Compressor_init(Compressor *self, PyObject *args, PyObject *kwargs) { 51 | static char *kwlist[] = { 52 | "format", 53 | "verbose", 54 | "iterations", 55 | "block_splitting", 56 | "block_splitting_max", 57 | NULL, 58 | }; 59 | PyObject *verbose, *blocksplitting, *io; 60 | 61 | self->format = ZOPFLI_FORMAT_DEFLATE; 62 | ZopfliInitOptions(&self->options); 63 | verbose = Py_False; 64 | blocksplitting = Py_True; 65 | if (!PyArg_ParseTupleAndKeywords(args, kwargs, 66 | "|iOiOi:ZopfliCompressor", kwlist, 67 | &self->format, 68 | &verbose, 69 | &self->options.numiterations, 70 | &blocksplitting, 71 | &self->options.blocksplittingmax)) { 72 | return -1; 73 | } 74 | 75 | switch (self->format) { 76 | case ZOPFLI_FORMAT_GZIP: 77 | case ZOPFLI_FORMAT_ZLIB: 78 | case ZOPFLI_FORMAT_DEFLATE: 79 | break; 80 | default: 81 | PyErr_SetString(PyExc_ValueError, "unknown format"); 82 | return -1; 83 | } 84 | 85 | PARSE_BOOL(self, verbose); 86 | PARSE_BOOL(self, blocksplitting); 87 | 88 | io = PyImport_ImportModule("io"); 89 | if (io == NULL) { 90 | return -1; 91 | } 92 | Py_XDECREF(self->data); 93 | self->data = PyObject_CallMethod(io, "BytesIO", NULL); 94 | Py_DECREF(io); 95 | if (self->data == NULL) { 96 | return -1; 97 | } 98 | 99 | self->flushed = 0; 100 | #ifdef WITH_THREAD 101 | ALLOCATE_LOCK(self); 102 | if (PyErr_Occurred() != NULL) { 103 | return -1; 104 | } 105 | #endif 106 | return 0; 107 | } 108 | 109 | PyDoc_STRVAR(Compressor_compress__doc__, 110 | "compress(data) -> bytes"); 111 | 112 | static PyObject * 113 | Compressor_compress(Compressor *self, PyObject *data) { 114 | PyObject *v, *n; 115 | 116 | v = NULL; 117 | ACQUIRE_LOCK(self); 118 | if (self->flushed) { 119 | PyErr_SetString(PyExc_ValueError, "Compressor has been flushed"); 120 | goto out; 121 | } 122 | n = PyObject_CallMethod(self->data, "write", "O", data); 123 | if (n == NULL) { 124 | goto out; 125 | } 126 | Py_DECREF(n); 127 | v = PyBytes_FromString(""); 128 | out: 129 | RELEASE_LOCK(self); 130 | return v; 131 | } 132 | 133 | PyDoc_STRVAR(Compressor_flush__doc__, 134 | "flush() -> bytes\n" 135 | "\n" 136 | "The compressor object cannot be used after this method is called."); 137 | 138 | static PyObject * 139 | Compressor_flush(Compressor *self) { 140 | PyObject *v, *b; 141 | Py_buffer in = {0}; 142 | unsigned char *out; 143 | size_t outsize; 144 | 145 | v = NULL; 146 | b = NULL; 147 | ACQUIRE_LOCK(self); 148 | if (self->flushed) { 149 | PyErr_SetString(PyExc_ValueError, "repeated call to flush()"); 150 | goto out; 151 | } 152 | b = PyObject_CallMethod(self->data, "getbuffer", NULL); 153 | if (b == NULL 154 | || PyObject_GetBuffer(b, &in, PyBUF_CONTIG_RO) < 0) { 155 | goto out; 156 | } 157 | 158 | out = NULL; 159 | outsize = 0; 160 | Py_BEGIN_ALLOW_THREADS 161 | ZopfliCompress(&self->options, self->format, in.buf, in.len, 162 | &out, &outsize); 163 | Py_END_ALLOW_THREADS 164 | 165 | v = PyBytes_FromStringAndSize((char *)out, outsize); 166 | free(out); 167 | PyBuffer_Release(&in); 168 | out: 169 | self->flushed = 1; 170 | Py_XDECREF(b); 171 | RELEASE_LOCK(self); 172 | return v; 173 | } 174 | 175 | static PyMethodDef Compressor_methods[] = { 176 | {"compress", (PyCFunction)Compressor_compress, METH_O, Compressor_compress__doc__}, 177 | {"flush", (PyCFunction)Compressor_flush, METH_NOARGS, Compressor_flush__doc__}, 178 | {0}, 179 | }; 180 | 181 | PyTypeObject Compressor_Type = { 182 | PyVarObject_HEAD_INIT(NULL, 0) 183 | .tp_name = MODULE ".ZopfliCompressor", 184 | .tp_basicsize = sizeof(Compressor), 185 | .tp_dealloc = (destructor)Compressor_dealloc, 186 | .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, 187 | .tp_doc = Compressor__doc__, 188 | .tp_methods = Compressor_methods, 189 | .tp_init = (initproc)Compressor_init, 190 | .tp_new = PyType_GenericNew, 191 | }; 192 | 193 | 194 | typedef struct { 195 | PyObject_HEAD 196 | ZopfliOptions options; 197 | unsigned char bp; 198 | unsigned char *out; 199 | size_t outsize; 200 | PyObject *data; 201 | int flushed; 202 | #ifdef WITH_THREAD 203 | PyThread_type_lock lock; 204 | #endif 205 | } Deflater; 206 | 207 | static void 208 | Deflater_dealloc(Deflater *self) { 209 | free(self->out); 210 | Py_XDECREF(self->data); 211 | FREE_LOCK(self); 212 | Py_TYPE(self)->tp_free((PyObject *)self); 213 | } 214 | 215 | PyDoc_STRVAR(Deflater__doc__, 216 | "ZopfliDeflater(verbose=False, iterations=15, block_splitting=True," 217 | " block_splitting_max=15)\n" 218 | "\n" 219 | "Create a compressor object which is using the ZopfliDeflatePart()\n" 220 | "function for compressing data."); 221 | 222 | static int 223 | Deflater_init(Deflater *self, PyObject *args, PyObject *kwargs) { 224 | static char *kwlist[] = { 225 | "verbose", 226 | "iterations", 227 | "block_splitting", 228 | "block_splitting_max", 229 | NULL, 230 | }; 231 | PyObject *verbose, *blocksplitting; 232 | 233 | ZopfliInitOptions(&self->options); 234 | verbose = Py_False; 235 | blocksplitting = Py_True; 236 | if (!PyArg_ParseTupleAndKeywords(args, kwargs, 237 | "|OiOi:ZopfliDeflater", kwlist, 238 | &verbose, 239 | &self->options.numiterations, 240 | &blocksplitting, 241 | &self->options.blocksplittingmax)) { 242 | return -1; 243 | } 244 | 245 | PARSE_BOOL(self, verbose); 246 | PARSE_BOOL(self, blocksplitting); 247 | 248 | self->bp = 0; 249 | free(self->out); 250 | self->out = NULL; 251 | self->outsize = 0; 252 | Py_CLEAR(self->data); 253 | self->flushed = 0; 254 | #ifdef WITH_THREAD 255 | ALLOCATE_LOCK(self); 256 | if (PyErr_Occurred() != NULL) { 257 | return -1; 258 | } 259 | #endif 260 | return 0; 261 | } 262 | 263 | static PyObject * 264 | deflate_part(Deflater *self, int final) { 265 | PyObject *v; 266 | Py_buffer in = {0}; 267 | size_t pos, off, n; 268 | 269 | if (self->data == NULL) { 270 | return PyBytes_FromString(""); 271 | } 272 | 273 | v = NULL; 274 | if (PyObject_GetBuffer(self->data, &in, PyBUF_CONTIG_RO) < 0) { 275 | goto out; 276 | } 277 | 278 | pos = self->outsize; 279 | Py_BEGIN_ALLOW_THREADS 280 | ZopfliDeflatePart(&self->options, 2, final, in.buf, 0, in.len, &self->bp, 281 | &self->out, &self->outsize); 282 | Py_END_ALLOW_THREADS 283 | if (!final) { 284 | /* exclude '256 (end of block)' symbol */ 285 | if (pos == 0) { 286 | off = pos; 287 | n = self->outsize - 1; 288 | } else { 289 | off = pos - 1; 290 | n = self->outsize - pos; 291 | } 292 | } else { 293 | /* include '256 (end of block)' symbol */ 294 | if (pos == 0) { 295 | off = pos; 296 | n = self->outsize; 297 | } else { 298 | off = pos - 1; 299 | n = self->outsize - pos + 1; 300 | } 301 | } 302 | v = PyBytes_FromStringAndSize((char *)self->out + off, n); 303 | out: 304 | PyBuffer_Release(&in); 305 | Py_CLEAR(self->data); 306 | return v; 307 | } 308 | 309 | PyDoc_STRVAR(Deflater_compress__doc__, 310 | "compress(data) -> bytes"); 311 | 312 | static PyObject * 313 | Deflater_compress(Deflater *self, PyObject *data) { 314 | PyObject *v; 315 | 316 | v = NULL; 317 | ACQUIRE_LOCK(self); 318 | if (self->flushed) { 319 | PyErr_SetString(PyExc_ValueError, "Deflater has been flushed"); 320 | goto out; 321 | } 322 | v = deflate_part(self, 0); 323 | if (v == NULL) { 324 | goto out; 325 | } 326 | Py_INCREF(data); 327 | self->data = data; 328 | out: 329 | RELEASE_LOCK(self); 330 | return v; 331 | } 332 | 333 | PyDoc_STRVAR(Deflater_flush__doc__, 334 | "flush() -> bytes\n" 335 | "\n" 336 | "The compressor object cannot be used after this method is called." 337 | ""); 338 | 339 | static PyObject * 340 | Deflater_flush(Deflater *self) { 341 | PyObject *v; 342 | 343 | v = NULL; 344 | ACQUIRE_LOCK(self); 345 | if (self->flushed) { 346 | PyErr_SetString(PyExc_ValueError, "repeated call to flush()"); 347 | goto out; 348 | } 349 | self->flushed = 1; 350 | v = deflate_part(self, 1); 351 | out: 352 | RELEASE_LOCK(self); 353 | return v; 354 | } 355 | 356 | static PyMethodDef Deflater_methods[] = { 357 | {"compress", (PyCFunction)Deflater_compress, METH_O, Deflater_compress__doc__}, 358 | {"flush", (PyCFunction)Deflater_flush, METH_NOARGS, Deflater_flush__doc__}, 359 | {0}, 360 | }; 361 | 362 | PyTypeObject Deflater_Type = { 363 | PyVarObject_HEAD_INIT(NULL, 0) 364 | .tp_name = MODULE ".ZopfliDeflater", 365 | .tp_basicsize = sizeof(Deflater), 366 | .tp_dealloc = (destructor)Deflater_dealloc, 367 | .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, 368 | .tp_doc = Deflater__doc__, 369 | .tp_methods = Deflater_methods, 370 | .tp_init = (initproc)Deflater_init, 371 | .tp_new = PyType_GenericNew, 372 | }; 373 | 374 | 375 | static struct PyModuleDef _zopflimodule = { 376 | PyModuleDef_HEAD_INIT, 377 | .m_name = MODULE, 378 | .m_size = -1, 379 | }; 380 | 381 | 382 | PyMODINIT_FUNC 383 | PyInit__zopfli(void) { 384 | PyObject *m; 385 | 386 | m = PyModule_Create(&_zopflimodule); 387 | if (m == NULL) { 388 | goto err; 389 | } 390 | #ifdef Py_GIL_DISABLED 391 | PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); 392 | #endif 393 | if (PyModule_AddIntMacro(m, ZOPFLI_FORMAT_GZIP) < 0 394 | || PyModule_AddIntMacro(m, ZOPFLI_FORMAT_ZLIB) < 0 395 | || PyModule_AddIntMacro(m, ZOPFLI_FORMAT_DEFLATE) < 0) { 396 | goto err; 397 | } 398 | 399 | #define ADD_TYPE(m, tp) \ 400 | do { \ 401 | if (PyType_Ready(tp) < 0) { \ 402 | goto err; \ 403 | } \ 404 | Py_INCREF(tp); \ 405 | if (PyModule_AddObject((m), strrchr((tp)->tp_name, '.') + 1, (PyObject *)(tp)) < 0) { \ 406 | Py_DECREF(tp); \ 407 | goto err; \ 408 | } \ 409 | } while (0) 410 | 411 | ADD_TYPE(m, &Compressor_Type); 412 | ADD_TYPE(m, &Deflater_Type); 413 | ADD_TYPE(m, &PNG_Type); 414 | 415 | #undef ADD_TYPE 416 | 417 | return m; 418 | err: 419 | return NULL; 420 | } 421 | -------------------------------------------------------------------------------- /tests/test_zopfli.py: -------------------------------------------------------------------------------- 1 | # 2 | # test_zopfli 3 | # 4 | # Copyright (c) 2015-2022 Akinori Hattori 5 | # 6 | # SPDX-License-Identifier: Apache-2.0 7 | # 8 | 9 | import os 10 | import sys 11 | import tempfile 12 | import time 13 | import unittest 14 | import unittest.mock 15 | import zipfile 16 | 17 | import zopfli 18 | 19 | 20 | class ZopfliTestCase(unittest.TestCase): 21 | 22 | def test_format(self): 23 | self.assertEqual(zopfli.ZOPFLI_FORMAT_GZIP, 0) 24 | self.assertEqual(zopfli.ZOPFLI_FORMAT_ZLIB, 1) 25 | self.assertEqual(zopfli.ZOPFLI_FORMAT_DEFLATE, 2) 26 | 27 | def test_gzip(self): 28 | self._test_zopfli(zopfli.ZOPFLI_FORMAT_GZIP) 29 | 30 | def test_zlib(self): 31 | self._test_zopfli(zopfli.ZOPFLI_FORMAT_ZLIB) 32 | 33 | def test_deflate(self): 34 | self._test_zopfli(zopfli.ZOPFLI_FORMAT_DEFLATE) 35 | 36 | def _test_zopfli(self, fmt): 37 | for i in range(2): 38 | c = zopfli.ZopfliCompressor(fmt, block_splitting=i) 39 | b = b'Hello, world!' 40 | z = c.compress(b) + c.flush() 41 | self._test_decompress(fmt, z, b) 42 | 43 | def test_unknown(self): 44 | with self.assertRaises(ValueError): 45 | zopfli.ZopfliCompressor(-1) 46 | 47 | with self.assertRaises(ValueError): 48 | zopfli.ZopfliDecompressor(-1) 49 | 50 | def test_compressor(self): 51 | with self.assertRaises(TypeError): 52 | zopfli.ZopfliCompressor(None) 53 | 54 | c = zopfli.ZopfliCompressor() 55 | with self.assertRaises(TypeError): 56 | c.compress(None) 57 | 58 | c = zopfli.ZopfliCompressor() 59 | self.assertEqual(c.flush(), b'\x03\x00') 60 | with self.assertRaises(ValueError): 61 | c.compress(b'') 62 | with self.assertRaises(ValueError): 63 | c.flush() 64 | 65 | def test_deflater(self): 66 | for i in range(2): 67 | c = zopfli.ZopfliDeflater(block_splitting=i) 68 | b = b'Hello, world!' 69 | z = c.compress(b) + c.flush() 70 | self._test_decompress(zopfli.ZOPFLI_FORMAT_DEFLATE, z, b) 71 | 72 | c = zopfli.ZopfliDeflater(block_splitting=i) 73 | b = b'Hello, world!' 74 | z = c.compress(b) + c.compress(b) + c.compress(b) + c.flush() 75 | self._test_decompress(zopfli.ZOPFLI_FORMAT_DEFLATE, z, b * 3) 76 | 77 | with self.assertRaises(TypeError): 78 | zopfli.ZopfliDeflater(iterations=None) 79 | 80 | with self.assertRaises(TypeError): 81 | zopfli.ZopfliDeflater(block_splitting_max=None) 82 | 83 | c = zopfli.ZopfliDeflater() 84 | c.compress(None) 85 | with self.assertRaises(TypeError): 86 | c.compress(None) 87 | 88 | c = zopfli.ZopfliDeflater() 89 | self.assertEqual(c.flush(), b'') 90 | with self.assertRaises(ValueError): 91 | c.compress(b'') 92 | with self.assertRaises(ValueError): 93 | c.flush() 94 | 95 | def _test_decompress(self, fmt, z, b): 96 | d = zopfli.ZopfliDecompressor(fmt) 97 | self.assertEqual(d.decompress(z) + d.flush(), b) 98 | self.assertEqual(d.unused_data, b'') 99 | self.assertEqual(d.unconsumed_tail, b'') 100 | self.assertTrue(d.eof) 101 | 102 | 103 | # 8-bit PNG (64x64 pixels) 104 | black_png = ( 105 | b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00' 106 | b'\x00\x40\x00\x00\x00\x40\x08\x00\x00\x00\x00\x8f\x02\x2e\x02\x00\x00\x00' 107 | b'\x1b\x49\x44\x41\x54\x78\x9c\xed\xc1\x81\x00\x00\x00\x00\xc3\xa0\xf9\x53' 108 | b'\xdf\xe0\x04\x55\x01\x00\x00\x00\x7c\x03\x10\x40\x00\x01\x46\x38\x0d\x1d' 109 | b'\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82' 110 | ) 111 | 112 | 113 | class ZopfliPNGTestCase(unittest.TestCase): 114 | 115 | def test_verbose(self): 116 | png = zopfli.ZopfliPNG() 117 | self.assertFalse(png.verbose) 118 | 119 | png.verbose = True 120 | self.assertTrue(png.verbose) 121 | 122 | png = zopfli.ZopfliPNG(verbose=True) 123 | self.assertTrue(png.verbose) 124 | 125 | with self.assertRaises(TypeError): 126 | del zopfli.ZopfliPNG().verbose 127 | 128 | def test_lossy_transparent(self): 129 | png = zopfli.ZopfliPNG() 130 | self.assertFalse(png.lossy_transparent) 131 | 132 | png.lossy_transparent = True 133 | self.assertTrue(png.lossy_transparent) 134 | 135 | png = zopfli.ZopfliPNG(lossy_transparent=True) 136 | self.assertTrue(png.lossy_transparent) 137 | 138 | with self.assertRaises(TypeError): 139 | del zopfli.ZopfliPNG().lossy_transparent 140 | 141 | def test_lossy_8bit(self): 142 | png = zopfli.ZopfliPNG() 143 | self.assertFalse(png.lossy_8bit) 144 | 145 | png.lossy_8bit = True 146 | self.assertTrue(png.lossy_8bit) 147 | 148 | png = zopfli.ZopfliPNG(lossy_8bit=True) 149 | self.assertTrue(png.lossy_8bit) 150 | 151 | with self.assertRaises(TypeError): 152 | del zopfli.ZopfliPNG().lossy_8bit 153 | 154 | def test_filter_strategies(self): 155 | png = zopfli.ZopfliPNG() 156 | self.assertEqual(png.filter_strategies, '') 157 | self.assertTrue(png.auto_filter_strategy) 158 | 159 | png.filter_strategies = '01234mepb' 160 | self.assertEqual(png.filter_strategies, '01234mepb') 161 | self.assertFalse(png.auto_filter_strategy) 162 | 163 | with self.assertRaises(ValueError): 164 | png.filter_strategies = '.' 165 | self.assertEqual(png.filter_strategies, '') 166 | self.assertTrue(png.auto_filter_strategy) 167 | 168 | png = zopfli.ZopfliPNG(filter_strategies='01234mepb') 169 | self.assertEqual(png.filter_strategies, '01234mepb') 170 | self.assertFalse(png.auto_filter_strategy) 171 | 172 | png.auto_filter_strategy = True 173 | self.assertEqual(png.filter_strategies, '') 174 | self.assertTrue(png.auto_filter_strategy) 175 | 176 | with self.assertRaises(TypeError): 177 | zopfli.ZopfliPNG(filter_strategies=None) 178 | with self.assertRaises(ValueError): 179 | zopfli.ZopfliPNG(filter_strategies='\u00B7') 180 | with self.assertRaises(ValueError): 181 | zopfli.ZopfliPNG(filter_strategies='z') 182 | with self.assertRaises(TypeError): 183 | del zopfli.ZopfliPNG().filter_strategies 184 | 185 | with self.assertRaises(TypeError): 186 | del zopfli.ZopfliPNG().auto_filter_strategy 187 | 188 | def test_keep_color_type(self): 189 | png = zopfli.ZopfliPNG() 190 | self.assertFalse(png.keep_color_type) 191 | 192 | png.keep_color_type = True 193 | self.assertTrue(png.keep_color_type) 194 | 195 | png = zopfli.ZopfliPNG(keep_color_type=True) 196 | self.assertTrue(png.keep_color_type) 197 | 198 | with self.assertRaises(TypeError): 199 | del zopfli.ZopfliPNG().keep_color_type 200 | 201 | def test_keep_chunks(self): 202 | png = zopfli.ZopfliPNG() 203 | self.assertEqual(png.keep_chunks, ()) 204 | 205 | png.keep_chunks = ['tEXt', 'zTXt', 'iTXt'] 206 | self.assertEqual(png.keep_chunks, ('tEXt', 'zTXt', 'iTXt')) 207 | 208 | png = zopfli.ZopfliPNG(keep_chunks=['tEXt', 'zTXt', 'iTXt']) 209 | self.assertEqual(png.keep_chunks, ('tEXt', 'zTXt', 'iTXt')) 210 | 211 | with self.assertRaises(TypeError): 212 | zopfli.ZopfliPNG(keep_chunks=None) 213 | with self.assertRaises(TypeError): 214 | zopfli.ZopfliPNG(keep_chunks=[None]) 215 | with self.assertRaises(ValueError): 216 | zopfli.ZopfliPNG(keep_chunks=['\u00B7']) 217 | with self.assertRaises(TypeError): 218 | del zopfli.ZopfliPNG().keep_chunks 219 | 220 | def test_use_zopfli(self): 221 | png = zopfli.ZopfliPNG() 222 | self.assertTrue(png.use_zopfli) 223 | 224 | png.use_zopfli = False 225 | self.assertFalse(png.use_zopfli) 226 | 227 | png = zopfli.ZopfliPNG(use_zopfli=False) 228 | self.assertFalse(png.use_zopfli) 229 | 230 | with self.assertRaises(TypeError): 231 | del zopfli.ZopfliPNG().use_zopfli 232 | 233 | def test_iterations(self): 234 | png = zopfli.ZopfliPNG() 235 | self.assertEqual(png.iterations, 15) 236 | 237 | png.iterations *= 2 238 | self.assertEqual(png.iterations, 30) 239 | 240 | png = zopfli.ZopfliPNG(iterations=30) 241 | self.assertEqual(png.iterations, 30) 242 | 243 | with self.assertRaises(TypeError): 244 | zopfli.ZopfliPNG(iterations=None) 245 | with self.assertRaises(TypeError): 246 | zopfli.ZopfliPNG().iterations = None 247 | with self.assertRaises(TypeError): 248 | del zopfli.ZopfliPNG().iterations 249 | 250 | def test_iterations_large(self): 251 | png = zopfli.ZopfliPNG() 252 | self.assertEqual(png.iterations_large, 5) 253 | 254 | png.iterations_large *= 2 255 | self.assertEqual(png.iterations_large, 10) 256 | 257 | png = zopfli.ZopfliPNG(iterations_large=10) 258 | self.assertEqual(png.iterations_large, 10) 259 | 260 | with self.assertRaises(TypeError): 261 | zopfli.ZopfliPNG(iterations_large=None) 262 | with self.assertRaises(TypeError): 263 | zopfli.ZopfliPNG().iterations_large = None 264 | with self.assertRaises(TypeError): 265 | del zopfli.ZopfliPNG().iterations_large 266 | 267 | def test_optimize(self): 268 | png = zopfli.ZopfliPNG() 269 | self.assertGreater(len(black_png), len(png.optimize(black_png))) 270 | 271 | with self.assertRaises(TypeError): 272 | png.optimize(None) 273 | with self.assertRaises(ValueError): 274 | png.optimize(b'') 275 | 276 | 277 | @unittest.mock.patch('time.time') 278 | class ZipFileTest(unittest.TestCase): 279 | 280 | def setUp(self): 281 | self._dir = tempfile.TemporaryDirectory(prefix='zopfli-') 282 | self.path = self._dir.name 283 | self.time = time.mktime(time.strptime('1980-01-01', '%Y-%m-%d')) 284 | 285 | def tearDown(self): 286 | self._dir.cleanup() 287 | 288 | def test_ascii(self, time): 289 | time.return_value = self.time 290 | 291 | encoding = 'ascii' 292 | names = { 293 | 'New Folder': 'New Folder', 294 | 'spam': 'spam', 295 | 'eggs': 'eggs', 296 | 'ham': 'ham', 297 | 'toast': 'toast', 298 | 'beans': 'beans', 299 | 'bacon': 'bacon', 300 | 'sausage': 'sausage', 301 | 'tomato': 'tomato', 302 | } 303 | self._test_zip(encoding, names) 304 | 305 | def test_cp932(self, time): 306 | time.return_value = self.time 307 | 308 | encoding = 'cp932' 309 | names = { 310 | 'New Folder': '\u65b0\u3057\u3044\u30d5\u30a9\u30eb\u30c0\u30fc', 311 | 'spam': '\u30b9\u30d1\u30e0', 312 | 'eggs': '\u30a8\u30c3\u30b0\u30b9', 313 | 'ham': '\u30cf\u30e0', 314 | 'toast': '\u30c8\u30fc\u30b9\u30c8', 315 | 'beans': '\u30d3\u30fc\u30f3\u30ba', 316 | 'bacon': '\u30d9\u30fc\u30b3\u30f3', 317 | 'sausage': '\u30bd\u30fc\u30bb\u30fc\u30b8', 318 | 'tomato': '\u30c8\u30de\u30c8', 319 | } 320 | self._test_encode(encoding, names) 321 | self._test_zip(encoding, names) 322 | 323 | def test_utf_8(self, time): 324 | time.return_value = self.time 325 | 326 | encoding = 'utf-8' 327 | names = { 328 | 'New Folder': '\u65b0\u3057\u3044\u30d5\u30a9\u30eb\u30c0\u30fc', 329 | 'spam': '\u30b9\u30d1\u30e0', 330 | 'eggs': '\u30a8\u30c3\u30b0\u30b9', 331 | 'ham': '\u30cf\u30e0', 332 | 'toast': '\u30c8\u30fc\u30b9\u30c8', 333 | 'beans': '\u30d3\u30fc\u30f3\u30ba', 334 | 'bacon': '\u30d9\u30fc\u30b3\u30f3', 335 | 'sausage': '\u30bd\u30fc\u30bb\u30fc\u30b8', 336 | 'tomato': '\u30c8\u30de\u30c8', 337 | } 338 | self._test_encode(encoding, names) 339 | self._test_zip(encoding, names) 340 | 341 | def _test_encode(self, encoding, names): 342 | f = self._f(names) 343 | 344 | def writestr(zf, name, raw_name=None): 345 | zi = zopfli.ZipInfo(name) 346 | if raw_name: 347 | zi.filename = raw_name 348 | zf.writestr(zi, os.path.splitext(os.path.basename(name))[0].encode(encoding)) 349 | 350 | path = os.path.join(self.path, 'cp437.zip') 351 | with zopfli.ZipFile(path, 'w', encoding='cp437') as zf: 352 | writestr(zf, f('{spam}.txt')) 353 | writestr(zf, f('{eggs}.txt'), f('{eggs}.txt').encode(encoding)) 354 | with zopfli.ZipFile(path, 'r', encoding=encoding) as zf: 355 | for n, flag_bits in ( 356 | ('{spam}.txt', 0x800), 357 | ('{eggs}.txt', 0), 358 | ): 359 | name = f(n) 360 | raw_name = name.encode('utf-8' if flag_bits else encoding).decode('utf-8' if flag_bits else 'cp437') 361 | zi = zf.getinfo(name) 362 | self.assertEqual(zi.orig_filename, raw_name) 363 | self.assertEqual(zi.filename, name) 364 | self.assertEqual(zi.flag_bits, flag_bits) 365 | self.assertNotEqual(zi.CRC, 0) 366 | self.assertGreater(zi.compress_size, 0) 367 | self.assertEqual(zf.read(zi), f(os.path.splitext(n)[0]).encode(encoding)) 368 | 369 | def _test_zip(self, encoding, names): 370 | f = self._f(names) 371 | 372 | def openw(zf, name): 373 | with zf.open(name, 'w') as fp: 374 | fp.write(os.path.splitext(os.path.basename(name))[0].encode(encoding)) 375 | 376 | def write(zf, name, deflate=True): 377 | p = os.path.join(self.path, name) 378 | with open(p, 'w', encoding=encoding) as fp: 379 | fp.write(os.path.splitext(os.path.basename(name))[0]) 380 | os.utime(p, (self.time,) * 2) 381 | zf.write(p, name, zipfile.ZIP_DEFLATED if deflate else zipfile.ZIP_STORED) 382 | 383 | def writestr(zf, name, deflate=True, zinfo=False): 384 | data = os.path.splitext(os.path.basename(name))[0].encode(encoding) 385 | compress_type = zipfile.ZIP_DEFLATED if deflate else zipfile.ZIP_STORED 386 | if zinfo: 387 | name = zopfli.ZipInfo(name) 388 | name.compress_type = compress_type 389 | zf.writestr(name, data, compress_type) 390 | 391 | path = os.path.join(self.path, f'{encoding}.zip') 392 | folder = '{New Folder}' 393 | os.mkdir(f(os.path.join(self.path, folder))) 394 | os.utime(f(os.path.join(self.path, folder)), (self.time,) * 2) 395 | with zopfli.ZipFile(path, 'w', encoding=encoding) as zf: 396 | if sys.version_info >= (3, 11): 397 | zf.mkdir(f(folder)) 398 | else: 399 | zf.write(f(os.path.join(self.path, folder)), f(folder)) 400 | write(zf, f(os.path.join(folder, '{spam}.txt'))) 401 | write(zf, f(os.path.join(folder, '{eggs}.txt')), deflate=False) 402 | writestr(zf, f(os.path.join(folder, '{ham}.txt'))) 403 | writestr(zf, f(os.path.join(folder, '{toast}.txt')), deflate=False) 404 | openw(zf, f(os.path.join(folder, '{beans}.txt'))) 405 | writestr(zf, f(os.path.join(folder, '{bacon}.txt')), zinfo=True) 406 | writestr(zf, f(os.path.join(folder, '{sausage}.txt')), deflate=False, zinfo=True) 407 | write(zf, f(os.path.join(folder, '{tomato}.txt')), deflate=False) 408 | with zopfli.ZipFile(path, 'r', encoding=encoding) as zf: 409 | for n, compress_type in ( 410 | ('{New Folder}/', zipfile.ZIP_STORED), 411 | ('{New Folder}/{spam}.txt', zipfile.ZIP_DEFLATED), 412 | ('{New Folder}/{eggs}.txt', zipfile.ZIP_STORED), 413 | ('{New Folder}/{ham}.txt', zipfile.ZIP_DEFLATED), 414 | ('{New Folder}/{toast}.txt', zipfile.ZIP_STORED), 415 | ('{New Folder}/{beans}.txt', zipfile.ZIP_DEFLATED), 416 | ('{New Folder}/{bacon}.txt', zipfile.ZIP_DEFLATED), 417 | ('{New Folder}/{sausage}.txt', zipfile.ZIP_STORED), 418 | ('{New Folder}/{tomato}.txt', zipfile.ZIP_STORED), 419 | ): 420 | name = f(n) 421 | raw_name = name.encode(encoding).decode('utf-8' if encoding == 'utf-8' else 'cp437') 422 | zi = zf.getinfo(name) 423 | self.assertEqual(zi.orig_filename, raw_name) 424 | self.assertEqual(zi.filename, name) 425 | self.assertEqual(zi.compress_type, compress_type) 426 | self.assertEqual(zi.flag_bits, 0x800 if encoding == 'utf-8' else 0) 427 | if zi.is_dir(): 428 | self.assertEqual(zi.CRC, 0) 429 | self.assertEqual(zi.compress_size, 0) 430 | else: 431 | self.assertNotEqual(zi.CRC, 0) 432 | self.assertGreater(zi.compress_size, 0) 433 | self.assertEqual(zf.read(zi), os.path.splitext(os.path.basename(name))[0].encode(encoding)) 434 | 435 | def _f(self, names): 436 | def f(s): 437 | return s.format(**names) 438 | return f 439 | -------------------------------------------------------------------------------- /zopfli/_zopfli/zopflipng.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // zopfli._zopfli :: zopflipng.cpp 3 | // 4 | // Copyright (c) 2015-2024 Akinori Hattori 5 | // 6 | // SPDX-License-Identifier: Apache-2.0 7 | // 8 | 9 | #include "_zopflimodule.h" 10 | 11 | #include "zopflipng/zopflipng_lib.h" 12 | #include "zopflipng/lodepng/lodepng.h" 13 | 14 | 15 | template 16 | static inline void clear(T*& p) { 17 | delete p; 18 | p = nullptr; 19 | } 20 | 21 | static inline PyObject* int_FromLong(long i) { 22 | return PyLong_FromLong(i); 23 | } 24 | 25 | static inline PyObject* str_AsASCIIString(PyObject* u) { 26 | return PyUnicode_AsASCIIString(u); 27 | } 28 | 29 | static inline bool str_Check(PyObject* v) { 30 | if (PyUnicode_Check(v)) { 31 | return true; 32 | } 33 | PyErr_Format(PyExc_TypeError, "expected str, got '%.200s'", Py_TYPE(v)->tp_name); 34 | return false; 35 | } 36 | 37 | static inline PyObject* str_FromString(const char* s) { 38 | return PyUnicode_FromString(s); 39 | } 40 | 41 | 42 | struct PNG { 43 | PyObject_HEAD 44 | PyObject* filter_strategies; 45 | PyObject* keep_chunks; 46 | ZopfliPNGOptions* options; 47 | #ifdef WITH_THREAD 48 | PyThread_type_lock lock; 49 | #endif 50 | }; 51 | 52 | static int PNG_traverse(PNG* self, visitproc visit, void* arg) { 53 | Py_VISIT(self->filter_strategies); 54 | Py_VISIT(self->keep_chunks); 55 | return 0; 56 | } 57 | 58 | static int PNG_clear(PNG* self) { 59 | Py_CLEAR(self->filter_strategies); 60 | Py_CLEAR(self->keep_chunks); 61 | return 0; 62 | } 63 | 64 | static void PNG_dealloc(PNG* self) { 65 | PyObject_GC_UnTrack(self); 66 | PNG_clear(self); 67 | clear(self->options); 68 | FREE_LOCK(self); 69 | Py_TYPE(self)->tp_free(reinterpret_cast(self)); 70 | } 71 | 72 | static int parse_filter_strategies(PNG* self, PyObject* filter_strategies) { 73 | PyObject* b = nullptr; 74 | char* s; 75 | Py_CLEAR(self->filter_strategies); 76 | if (!str_Check(filter_strategies)) { 77 | goto err; 78 | } 79 | b = str_AsASCIIString(filter_strategies); 80 | if (b == nullptr) { 81 | goto err; 82 | } 83 | s = PyBytes_AsString(b); 84 | if (s == nullptr) { 85 | goto err; 86 | } 87 | self->options->filter_strategies.clear(); 88 | for (; *s != '\0'; ++s) { 89 | ZopfliPNGFilterStrategy fs; 90 | switch (*s) { 91 | case '0': 92 | fs = kStrategyZero; 93 | break; 94 | case '1': 95 | fs = kStrategyOne; 96 | break; 97 | case '2': 98 | fs = kStrategyTwo; 99 | break; 100 | case '3': 101 | fs = kStrategyThree; 102 | break; 103 | case '4': 104 | fs = kStrategyFour; 105 | break; 106 | case 'm': 107 | fs = kStrategyMinSum; 108 | break; 109 | case 'e': 110 | fs = kStrategyEntropy; 111 | break; 112 | case 'p': 113 | fs = kStrategyPredefined; 114 | break; 115 | case 'b': 116 | fs = kStrategyBruteForce; 117 | break; 118 | default: 119 | PyErr_Format(PyExc_ValueError, "unknown filter strategy: %c", *s); 120 | goto err; 121 | } 122 | self->options->filter_strategies.push_back(fs); 123 | self->options->auto_filter_strategy = false; 124 | } 125 | 126 | Py_DECREF(b); 127 | Py_INCREF(filter_strategies); 128 | self->filter_strategies = filter_strategies; 129 | return 0; 130 | err: 131 | Py_XDECREF(b); 132 | self->filter_strategies = str_FromString(""); 133 | self->options->filter_strategies.clear(); 134 | self->options->auto_filter_strategy = true; 135 | return -1; 136 | } 137 | 138 | static int parse_keep_chunks(PNG* self, PyObject* keep_chunks) { 139 | PyObject* u = nullptr; 140 | PyObject* b = nullptr; 141 | Py_CLEAR(self->keep_chunks); 142 | Py_ssize_t n = PySequence_Size(keep_chunks); 143 | if (n < 0) { 144 | goto err; 145 | } 146 | self->options->keepchunks.clear(); 147 | for (Py_ssize_t i = 0; i < n; ++i) { 148 | u = PySequence_GetItem(keep_chunks, i); 149 | if (u == nullptr 150 | || !str_Check(u)) { 151 | goto err; 152 | } 153 | b = str_AsASCIIString(u); 154 | if (b == nullptr) { 155 | goto err; 156 | } 157 | char* s = PyBytes_AsString(b); 158 | if (s == nullptr) { 159 | goto err; 160 | } 161 | self->options->keepchunks.push_back(s); 162 | Py_CLEAR(u); 163 | Py_CLEAR(b); 164 | } 165 | 166 | self->keep_chunks = PySequence_Tuple(keep_chunks); 167 | return 0; 168 | err: 169 | Py_XDECREF(u); 170 | Py_XDECREF(b); 171 | self->keep_chunks = PyTuple_New(0); 172 | self->options->keepchunks.clear(); 173 | return -1; 174 | } 175 | 176 | PyDoc_STRVAR(PNG__doc__, 177 | "ZopfliPNG(verbose=False, lossy_transparent=False, lossy_8bit=False," 178 | " filter_strategies='', auto_filter_strategy=True, keep_color_type=False," 179 | " keep_chunks=None, use_zopfli=True, iterations=15, iterations_large=5)\n" 180 | "\n" 181 | "Create a PNG optimizer which is using the ZopfliPNGOptimize()\n" 182 | "function for optimizing PNG files.\n" 183 | ""); 184 | 185 | static int PNG_init(PNG* self, PyObject* args, PyObject* kwargs) { 186 | static const char* kwlist[] = { 187 | "verbose", 188 | "lossy_transparent", 189 | "lossy_8bit", 190 | "filter_strategies", 191 | "auto_filter_strategy", 192 | "keep_color_type", 193 | "keep_chunks", 194 | "use_zopfli", 195 | "iterations", 196 | "iterations_large", 197 | nullptr, 198 | }; 199 | 200 | PyObject* verbose = Py_False; 201 | PyObject* lossy_transparent = Py_False; 202 | PyObject* lossy_8bit = Py_False; 203 | PyObject* filter_strategies = nullptr; 204 | PyObject* auto_filter_strategy = Py_True; 205 | PyObject* keep_color_type = Py_False; 206 | PyObject* keep_chunks = nullptr; 207 | PyObject* use_zopfli = Py_True; 208 | clear(self->options); 209 | self->options = new ZopfliPNGOptions; 210 | if (!PyArg_ParseTupleAndKeywords(args, kwargs, 211 | "|OOOOOOOOii:ZopfliPNG", const_cast(kwlist), 212 | &verbose, 213 | &lossy_transparent, 214 | &lossy_8bit, 215 | &filter_strategies, 216 | &auto_filter_strategy, 217 | &keep_color_type, 218 | &keep_chunks, 219 | &use_zopfli, 220 | &self->options->num_iterations, 221 | &self->options->num_iterations_large)) { 222 | return -1; 223 | } 224 | 225 | #define PARSE_BOOL(var, val) \ 226 | do { \ 227 | int b = PyObject_IsTrue(val); \ 228 | if (b < 0) { \ 229 | goto err; \ 230 | } \ 231 | var = !!b; \ 232 | } while (false) 233 | 234 | PARSE_BOOL(self->options->verbose, verbose); 235 | PARSE_BOOL(self->options->lossy_transparent, lossy_transparent); 236 | PARSE_BOOL(self->options->lossy_8bit, lossy_8bit); 237 | PARSE_BOOL(self->options->auto_filter_strategy, auto_filter_strategy); 238 | PARSE_BOOL(self->options->keep_colortype, keep_color_type); 239 | PARSE_BOOL(self->options->use_zopfli, use_zopfli); 240 | 241 | #undef PARSE_BOOL 242 | 243 | #define PARSE_OBJECT(self, var, dv) \ 244 | do { \ 245 | if (var != nullptr) { \ 246 | if (parse_ ## var((self), var) < 0) { \ 247 | goto err; \ 248 | } \ 249 | } else { \ 250 | Py_XDECREF((self)->var); \ 251 | (self)->var = (dv); \ 252 | } \ 253 | } while (false) 254 | 255 | PARSE_OBJECT(self, filter_strategies, str_FromString("")); 256 | PARSE_OBJECT(self, keep_chunks, PyTuple_New(0)); 257 | 258 | #undef PARSE_OBJECT 259 | 260 | #ifdef WITH_THREAD 261 | ALLOCATE_LOCK(self); 262 | if (PyErr_Occurred() != nullptr) { 263 | goto err; 264 | } 265 | #endif 266 | 267 | return 0; 268 | err: 269 | Py_CLEAR(self->filter_strategies); 270 | Py_CLEAR(self->keep_chunks); 271 | clear(self->options); 272 | return -1; 273 | } 274 | 275 | PyDoc_STRVAR(PNG_optimize__doc__, 276 | "optimize(data) -> bytes"); 277 | 278 | static PyObject* PNG_optimize(PNG* self, PyObject* data) { 279 | PyObject* v = nullptr; 280 | Py_buffer in = {}; 281 | std::vector out, buf; 282 | unsigned char* p; 283 | ACQUIRE_LOCK(self); 284 | if (PyObject_GetBuffer(data, &in, PyBUF_CONTIG_RO) < 0) { 285 | goto out; 286 | } 287 | p = static_cast(in.buf); 288 | buf.assign(p, p + in.len); 289 | unsigned err; 290 | Py_BEGIN_ALLOW_THREADS 291 | err = ZopfliPNGOptimize(buf, *self->options, self->options->verbose, &out); 292 | Py_END_ALLOW_THREADS 293 | if (err) { 294 | PyErr_SetString(PyExc_ValueError, lodepng_error_text(err)); 295 | goto out; 296 | } 297 | buf.clear(); 298 | unsigned w, h; 299 | Py_BEGIN_ALLOW_THREADS 300 | err = lodepng::decode(buf, w, h, out); 301 | Py_END_ALLOW_THREADS 302 | if (err) { 303 | PyErr_SetString(PyExc_ValueError, "verification failed"); 304 | goto out; 305 | } 306 | v = PyBytes_FromStringAndSize(reinterpret_cast(&out[0]), out.size()); 307 | out: 308 | PyBuffer_Release(&in); 309 | RELEASE_LOCK(self); 310 | return v; 311 | } 312 | 313 | static PyMethodDef PNG_methods[] = { 314 | {"optimize", reinterpret_cast(PNG_optimize), METH_O, PNG_optimize__doc__}, 315 | {}, 316 | }; 317 | 318 | static PyObject* PNG_get_object(PNG* self, void* closure) { 319 | const char *s = static_cast(closure); 320 | PyObject* v = nullptr; 321 | if (strcmp(s, "filter_strategies") == 0) { 322 | v = self->filter_strategies; 323 | } else if (strcmp(s, "keep_chunks") == 0) { 324 | v = self->keep_chunks; 325 | } 326 | 327 | Py_INCREF(v); 328 | return v; 329 | } 330 | 331 | static int PNG_set_object(PNG* self, PyObject* value, void* closure) { 332 | const char* s = static_cast(closure); 333 | if (value == nullptr) { 334 | PyErr_Format(PyExc_TypeError, "cannot delete %s", s); 335 | return -1; 336 | } 337 | 338 | if (strcmp(s, "filter_strategies") == 0) { 339 | if (parse_filter_strategies(self, value) < 0) { 340 | return -1; 341 | } 342 | } else if (strcmp(s, "keep_chunks") == 0) { 343 | if (parse_keep_chunks(self, value) < 0) { 344 | return -1; 345 | } 346 | } 347 | return 0; 348 | } 349 | 350 | static PyObject* PNG_get_bool(PNG* self, void* closure) { 351 | const char *s = static_cast(closure); 352 | bool v = false; 353 | if (strcmp(s, "verbose") == 0) { 354 | v = self->options->verbose; 355 | } else if (strcmp(s, "lossy_transparent") == 0) { 356 | v = self->options->lossy_transparent; 357 | } else if (strcmp(s, "lossy_8bit") == 0) { 358 | v = self->options->lossy_8bit; 359 | } else if (strcmp(s, "auto_filter_strategy") == 0) { 360 | v = self->options->auto_filter_strategy; 361 | } else if (strcmp(s, "keep_color_type") == 0) { 362 | v = self->options->keep_colortype; 363 | } else if (strcmp(s, "use_zopfli") == 0) { 364 | v = self->options->use_zopfli; 365 | } 366 | 367 | if (v) { 368 | Py_RETURN_TRUE; 369 | } 370 | Py_RETURN_FALSE; 371 | } 372 | 373 | static int PNG_set_bool(PNG* self, PyObject* value, void* closure) { 374 | const char* s = static_cast(closure); 375 | if (value == nullptr) { 376 | PyErr_Format(PyExc_TypeError, "cannot delete %s", s); 377 | return -1; 378 | } 379 | int b = PyObject_IsTrue(value); 380 | if (b < 0) { 381 | return -1; 382 | } 383 | bool v = !!b; 384 | 385 | if (strcmp(s, "verbose") == 0) { 386 | self->options->verbose = v; 387 | } else if (strcmp(s, "lossy_transparent") == 0) { 388 | self->options->lossy_transparent = v; 389 | } else if (strcmp(s, "lossy_8bit") == 0) { 390 | self->options->lossy_8bit = v; 391 | } else if (strcmp(s, "auto_filter_strategy") == 0) { 392 | if (v) { 393 | Py_CLEAR(self->filter_strategies); 394 | self->filter_strategies = str_FromString(""); 395 | self->options->filter_strategies.clear(); 396 | } 397 | self->options->auto_filter_strategy = v; 398 | } else if (strcmp(s, "keep_color_type") == 0) { 399 | self->options->keep_colortype = v; 400 | } else if (strcmp(s, "use_zopfli") == 0) { 401 | self->options->use_zopfli = v; 402 | } 403 | return 0; 404 | } 405 | 406 | static PyObject* PNG_get_int(PNG* self, void* closure) { 407 | const char* s = static_cast(closure); 408 | long v = 0; 409 | if (strcmp(s, "iterations") == 0) { 410 | v = self->options->num_iterations; 411 | } else if (strcmp(s, "iterations_large") == 0) { 412 | v = self->options->num_iterations_large; 413 | } 414 | 415 | return int_FromLong(v); 416 | } 417 | 418 | static int PNG_set_int(PNG* self, PyObject* value, void* closure) { 419 | const char* s = static_cast(closure); 420 | if (value == nullptr) { 421 | PyErr_Format(PyExc_TypeError, "cannot delete %s", s); 422 | return -1; 423 | } 424 | long v = PyLong_AsLong(value); 425 | if (PyErr_Occurred() != nullptr) { 426 | return -1; 427 | } 428 | 429 | if (strcmp(s, "iterations") == 0) { 430 | self->options->num_iterations = v; 431 | } else if (strcmp(s, "iterations_large") == 0) { 432 | self->options->num_iterations_large = v; 433 | } 434 | return 0; 435 | } 436 | 437 | #define GET_SET(v, tp) {const_cast(#v), reinterpret_cast(PNG_get_ ## tp), reinterpret_cast(PNG_set_ ## tp), nullptr, const_cast(#v)} 438 | 439 | static PyGetSetDef PNG_getset[] = { 440 | GET_SET(filter_strategies, object), 441 | GET_SET(keep_chunks, object), 442 | GET_SET(verbose, bool), 443 | GET_SET(lossy_transparent, bool), 444 | GET_SET(lossy_8bit, bool), 445 | GET_SET(auto_filter_strategy, bool), 446 | GET_SET(keep_color_type, bool), 447 | GET_SET(use_zopfli, bool), 448 | GET_SET(iterations, int), 449 | GET_SET(iterations_large, int), 450 | {}, 451 | }; 452 | 453 | #undef GET_SET 454 | 455 | PyTypeObject PNG_Type = { 456 | PyVarObject_HEAD_INIT(0, 0) 457 | MODULE ".ZopfliPNG", // tp_name 458 | sizeof(PNG), // tp_basicsize 459 | 0, // tp_itemsize 460 | reinterpret_cast(PNG_dealloc), // tp_dealloc 461 | 0, // tp_vectorcall_offset 462 | 0, // tp_getattr 463 | 0, // tp_setattr 464 | 0, // tp_reserved 465 | 0, // tp_repr 466 | 0, // tp_as_number 467 | 0, // tp_as_sequence 468 | 0, // tp_as_mapping 469 | 0, // tp_hash 470 | 0, // tp_call 471 | 0, // tp_str 472 | 0, // tp_getattro 473 | 0, // tp_setattro 474 | 0, // tp_as_buffer 475 | Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, // tp_flags 476 | PNG__doc__, // tp_doc 477 | reinterpret_cast(PNG_traverse), // tp_traverse 478 | reinterpret_cast(PNG_clear), // tp_clear 479 | 0, // tp_richcompare 480 | 0, // tp_weaklistoffset 481 | 0, // tp_iter 482 | 0, // tp_iternext 483 | PNG_methods, // tp_methods 484 | 0, // tp_members 485 | PNG_getset, // tp_getset 486 | 0, // tp_base 487 | 0, // tp_dict 488 | 0, // tp_descr_get 489 | 0, // tp_descr_set 490 | 0, // tp_dictoffset 491 | reinterpret_cast(PNG_init), // tp_init 492 | 0, // tp_alloc 493 | PyType_GenericNew, // tp_new 494 | }; 495 | --------------------------------------------------------------------------------