├── test ├── shipitsquirrel.png ├── python-logo-only.raw.gz └── test_oxipng.py ├── Pipfile ├── src ├── error.rs ├── lib.rs ├── options.rs ├── raw.rs └── types.rs ├── Cargo.toml ├── .gitignore ├── pyproject.toml ├── LICENSE ├── CHANGELOG.md ├── oxipng.pyi ├── .github └── workflows │ └── CI.yml ├── Pipfile.lock ├── README.md └── Cargo.lock /test/shipitsquirrel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrasser/pyoxipng/HEAD/test/shipitsquirrel.png -------------------------------------------------------------------------------- /test/python-logo-only.raw.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrasser/pyoxipng/HEAD/test/python-logo-only.raw.gz -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [dev-packages] 7 | ruff = "*" 8 | maturin = "*" 9 | pytest = "*" 10 | atomicwrites = "*" 11 | importlib-metadata = "*" 12 | typing-extensions = "*" 13 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use pyo3::create_exception; 2 | use pyo3::exceptions::PyException; 3 | use pyo3::prelude::*; 4 | 5 | use ::oxipng as oxi; 6 | 7 | create_exception!(oxipng, PngError, PyException); 8 | 9 | pub fn handle_png_error(err: oxi::PngError) -> PyResult { 10 | Err(PngError::new_err(format!("{}", err))) 11 | } 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyoxipng" 3 | version = "9.1.1" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [lib] 8 | name = "oxipng" 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | pyo3 = { version = "0.25.1", features = ["extension-module"] } 13 | oxipng = { version = "9.1.*" } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | .pytest_cache/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | .venv/ 14 | env/ 15 | bin/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | include/ 26 | man/ 27 | venv/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | pip-selfcheck.json 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Mr Developer 49 | .mr.developer.cfg 50 | .project 51 | .pydevproject 52 | 53 | # Rope 54 | .ropeproject 55 | 56 | # Django stuff: 57 | *.log 58 | *.pot 59 | 60 | .DS_Store 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyCharm 66 | .idea/ 67 | 68 | # VSCode 69 | .vscode/ 70 | 71 | # Pyenv 72 | .python-version 73 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin~=1.8.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "pyoxipng" 7 | requires-python = ">=3.8" 8 | version = "9.1.1" 9 | description = "Python wrapper for multithreaded .png image file optimizer oxipng" 10 | readme = "README.md" 11 | license = { file = "LICENSE" } 12 | keywords = [ 13 | "rust", 14 | "image", 15 | "optimize", 16 | "optimizer", 17 | "optimization", 18 | "compress", 19 | "png", 20 | ] 21 | authors = [{ name = "Nick Frasser " }] 22 | classifiers = [ 23 | "License :: OSI Approved :: MIT License", 24 | "Programming Language :: Rust", 25 | "Programming Language :: Python :: Implementation :: CPython", 26 | "Programming Language :: Python :: Implementation :: PyPy", 27 | "Topic :: Utilities", 28 | ] 29 | 30 | [project.urls] 31 | homepage = "https://github.com/nfrasser/pyoxipng" 32 | documentation = "https://github.com/nfrasser/pyoxipng#readme" 33 | repository = "https://github.com/nfrasser/pyoxipng" 34 | changelog = "https://github.com/nfrasser/pyoxipng/blob/main/CHANGELOG.md" 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nick Frasser 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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 9.1.1 4 | 5 | - Updated to latest oxipng 9.1.5 6 | - Drop Python 3.8 support 7 | 8 | ## 9.1.0 9 | 10 | - Deprecated: options such as `filter` that that previously supported `set` 11 | types now expect `Sequence` types such as `list` or `tuple`. Support for `set` 12 | will be removed in v10. 13 | - Dropped build support for old operating systems that reached end-of-life 14 | - Disable x86, armv7, s390x, ppc64le manylinux pip builds (breaking due to deflate dependency) 15 | - Update to oxipng 9.1 16 | - Update build toolchain 17 | - Refactor internals 18 | 19 | ## 9.0.1 20 | 21 | - Support for Python 3.13 22 | - Upgrade pip dependencies 23 | - Fixed: Correct fast_evaluation argument in wrapper 24 | 25 | ## 9.0.0 26 | 27 | - Update to oxipng 9 28 | - BREAKING: Removed `backup` option 29 | - BREAKING: Removed `check` option 30 | - BREAKING: Removed `pretend` option 31 | - BREAKING: Removed `preserve_attrs` option 32 | - BREAKING: Replaced `oxipng.Headers` with `oxipng.StripChunks` 33 | - Added: `RawImage` class for optimizing raw RGBA data 34 | - Added: `scale_16` option 35 | - Fixed: correct `fast_evaluation` option implementation 36 | 37 | ## 8.0.1 38 | 39 | - Python 3.12 wheels 40 | - Drop Python 3.7 support 41 | 42 | ## 8.0.0 43 | 44 | - Update to oxipng 8 45 | - BREAKING: `interlace` option now expects `oxipng.Interlace` enum 46 | - BREAKING: replace `alphas` option with `optimize_alpha` boolean 47 | - Added: `check` option 48 | 49 | ## 7.0.0 50 | 51 | - Upgrade to oxipng 7 52 | - BREAKING: `filter` option now expects set of `oxipng.RowFilter` enum 53 | - BREAKING: `deflate` option now expects instance of `oxipng.Deflaters` 54 | - Added: `fast_evaluation` option 55 | 56 | ## 6.0.0 57 | 58 | - Add missing alphas, strip and deflate options 59 | 60 | ## 5.0.0 61 | 62 | - Sync version with oxipng major releases 63 | 64 | ## 0.2.0 65 | 66 | - Update project metadata 67 | 68 | ## 0.1.0 69 | 70 | - Initial release 71 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use ::oxipng as oxi; 2 | use pyo3::prelude::*; 3 | use pyo3::types::PyDict; 4 | use std::borrow::Cow; 5 | use std::path::PathBuf; 6 | 7 | mod error; 8 | mod options; 9 | mod raw; 10 | mod types; 11 | 12 | /// Optimize the png file at the given input location. Optionally send it to the 13 | /// given output location. 14 | #[pyfunction] 15 | #[pyo3(signature = (input, output=None, **kwargs))] 16 | fn optimize( 17 | input: PathBuf, 18 | output: Option, 19 | kwargs: Option<&Bound<'_, PyDict>>, 20 | ) -> PyResult<()> { 21 | let inpath = oxi::InFile::Path(input); 22 | let outpath = output 23 | .and_then(|path| Some(oxi::OutFile::from_path(path))) 24 | .unwrap_or(oxi::OutFile::Path { 25 | path: None, 26 | preserve_attrs: false, 27 | }); // No arg specified, in-place optimize 28 | 29 | oxi::optimize(&inpath, &outpath, &options::parse_kw_opts(kwargs)?) 30 | .or_else(error::handle_png_error)?; 31 | Ok(()) 32 | } 33 | 34 | /// Perform optimization on the input file using the options provided, where the 35 | /// file is already loaded in-memory 36 | #[pyfunction] 37 | #[pyo3(signature = (data, **kwargs))] 38 | fn optimize_from_memory<'a>( 39 | data: &'a [u8], 40 | kwargs: Option<&Bound<'_, PyDict>>, 41 | ) -> PyResult> { 42 | // Note: returned Cow<[u8]> interpreted as Python bytes 43 | oxi::optimize_from_memory(data, &options::parse_kw_opts(kwargs)?) 44 | .and_then(|data| Ok(data.into())) 45 | .or_else(error::handle_png_error) 46 | } 47 | 48 | /// A Python module implemented in Rust. 49 | #[pymodule] 50 | fn oxipng(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { 51 | m.add("PngError", py.get_type::())?; 52 | m.add_class::()?; 53 | m.add_class::()?; 54 | m.add_class::()?; 55 | m.add_class::()?; 56 | m.add_class::()?; 57 | m.add_class::()?; 58 | m.add_function(wrap_pyfunction!(optimize, m)?)?; 59 | m.add_function(wrap_pyfunction!(optimize_from_memory, m)?)?; 60 | Ok(()) 61 | } 62 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | use crate::types::*; 2 | use ::oxipng as oxi; 3 | use pyo3::exceptions::{PyTypeError, PyValueError}; 4 | use pyo3::prelude::*; 5 | use pyo3::types::{PyBool, PyDict, PyString}; 6 | 7 | pub fn parse_kw_opts(kwds: Option<&Bound<'_, PyDict>>) -> PyResult { 8 | if let Some(kwopts) = kwds { 9 | parse_kw_opts_dict(kwopts) 10 | } else { 11 | Ok(oxi::Options::default()) 12 | } 13 | } 14 | 15 | pub fn parse_kw_opts_dict(kwops: &Bound<'_, PyDict>) -> PyResult { 16 | let mut opts = if let Some(level) = kwops.get_item("level")? { 17 | let level: u8 = level.extract().or_else(|err| { 18 | Err(PyValueError::new_err(format!( 19 | "Invalid optimization level; countered {}", 20 | err 21 | ))) 22 | })?; 23 | if level > 6 { 24 | return Err(PyValueError::new_err( 25 | "Invalid optimization level; must be between 0 and 6 inclusive", 26 | )); 27 | } 28 | oxi::Options::from_preset(level) 29 | } else { 30 | oxi::Options::default() 31 | }; 32 | 33 | for (k, v) in kwops.iter() { 34 | let key = k.downcast::()?; 35 | let key = key.to_str()?; 36 | parse_kw_opt(key, &v, &mut opts).or_else(|err| { 37 | Err(PyTypeError::new_err(format!( 38 | "Invalid option '{}'; encountered {}", 39 | key, err 40 | ))) 41 | })?; 42 | } 43 | Ok(opts) 44 | } 45 | 46 | fn parse_kw_opt(key: &str, value: &Bound<'_, PyAny>, opts: &mut oxi::Options) -> PyResult<()> { 47 | match key { 48 | "level" => {} // Handled elsewhere, ignore 49 | "fix_errors" => opts.fix_errors = value.downcast::()?.is_true(), 50 | "force" => opts.force = value.downcast::()?.is_true(), 51 | "filter" => opts.filter = value.extract::>()?.remap(), 52 | "interlace" => opts.interlace = py_option_extract::(value)?, 53 | "optimize_alpha" => opts.optimize_alpha = value.downcast::()?.is_true(), 54 | "bit_depth_reduction" => opts.bit_depth_reduction = value.downcast::()?.is_true(), 55 | "color_type_reduction" => opts.color_type_reduction = value.downcast::()?.is_true(), 56 | "palette_reduction" => opts.palette_reduction = value.downcast::()?.is_true(), 57 | "grayscale_reduction" => opts.grayscale_reduction = value.downcast::()?.is_true(), 58 | "idat_recoding" => opts.idat_recoding = value.downcast::()?.is_true(), 59 | "scale_16" => opts.scale_16 = value.downcast::()?.is_true(), 60 | "strip" => opts.strip = value.extract::()?.0, 61 | "deflate" => opts.deflate = value.extract::()?.0, 62 | "fast_evaluation" => opts.fast_evaluation = value.downcast::()?.is_true(), 63 | "timeout" => opts.timeout = py_duration(value)?, 64 | _ => return Err(PyTypeError::new_err("Unsupported option")), 65 | } 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /src/raw.rs: -------------------------------------------------------------------------------- 1 | use crate::{error, options}; 2 | use ::oxipng as oxi; 3 | use pyo3::exceptions::PyValueError; 4 | use pyo3::prelude::*; 5 | use pyo3::types::PyDict; 6 | use std::borrow::Cow; 7 | 8 | #[pyclass] 9 | #[derive(Debug, Clone)] 10 | pub struct ColorType(pub oxi::ColorType); 11 | 12 | #[pymethods] 13 | impl ColorType { 14 | #[staticmethod] 15 | #[pyo3(signature = (transparent_shade=None))] 16 | fn grayscale(transparent_shade: Option) -> Self { 17 | Self(oxi::ColorType::Grayscale { transparent_shade }) 18 | } 19 | 20 | #[staticmethod] 21 | #[pyo3(signature = (transparent_color=None))] 22 | fn rgb(transparent_color: Option<[u16; 3]>) -> PyResult { 23 | let transparent_color = if let Some(col) = transparent_color { 24 | Some(oxi::RGB16::new(col[0], col[1], col[2])) 25 | } else { 26 | None 27 | }; 28 | Ok(Self(oxi::ColorType::RGB { transparent_color })) 29 | } 30 | 31 | #[staticmethod] 32 | #[pyo3(signature = (palette))] 33 | fn indexed(palette: Vec<[u8; 4]>) -> PyResult { 34 | let len = palette.len(); 35 | if len == 0 || len > 256 { 36 | return Err(PyValueError::new_err( 37 | "palette len must be greater than 0 and less than or equal to 256", 38 | )); 39 | } 40 | Ok(Self(oxi::ColorType::Indexed { 41 | palette: palette 42 | .iter() 43 | .map(|col| oxi::RGBA8::new(col[0], col[1], col[2], col[3])) 44 | .collect(), 45 | })) 46 | } 47 | 48 | #[staticmethod] 49 | fn grayscale_alpha() -> Self { 50 | Self(oxi::ColorType::GrayscaleAlpha) 51 | } 52 | 53 | #[staticmethod] 54 | fn rgba() -> Self { 55 | Self(oxi::ColorType::RGBA) 56 | } 57 | } 58 | #[pyclass] 59 | #[derive(Debug)] 60 | pub struct RawImage(pub oxi::RawImage); 61 | 62 | #[pymethods] 63 | impl RawImage { 64 | #[new] 65 | #[pyo3(signature = (data, width, height, *, color_type=None, bit_depth=8))] 66 | fn py_new( 67 | data: Vec, 68 | width: u32, 69 | height: u32, 70 | color_type: Option<&ColorType>, // default RGBA 71 | bit_depth: Option, // default 8 72 | ) -> PyResult { 73 | let color_type = if let Some(t) = color_type { 74 | t.0.clone() 75 | } else { 76 | oxi::ColorType::RGBA 77 | }; 78 | let bit_depth = if let Some(d) = bit_depth { 79 | match d { 80 | 1 => oxi::BitDepth::One, 81 | 2 => oxi::BitDepth::Two, 82 | 4 => oxi::BitDepth::Four, 83 | 8 => oxi::BitDepth::Eight, 84 | 16 => oxi::BitDepth::Sixteen, 85 | _ => { 86 | return Err(PyValueError::new_err(format!( 87 | "Invalid bit_depth {}; must be 1, 2, 4, 8 or 16", 88 | d 89 | ))) 90 | } 91 | } 92 | } else { 93 | oxi::BitDepth::Eight 94 | }; 95 | 96 | Ok(RawImage( 97 | oxi::RawImage::new(width, height, color_type, bit_depth, data) 98 | .or_else(error::handle_png_error)?, 99 | )) 100 | } 101 | 102 | #[pyo3(signature = (name, data))] 103 | fn add_png_chunk(&mut self, name: &[u8], data: Vec) -> PyResult<()> { 104 | self.0.add_png_chunk( 105 | name.try_into().or(Err(PyValueError::new_err( 106 | "Invalid chunk (must be 4 bytes long)", 107 | )))?, 108 | data, 109 | ); 110 | Ok(()) 111 | } 112 | 113 | #[pyo3(signature = (data))] 114 | fn add_icc_profile(&mut self, data: &[u8]) { 115 | self.0.add_icc_profile(data) 116 | } 117 | 118 | #[pyo3(signature = (**kwargs))] 119 | fn create_optimized_png<'a>(&self, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult> { 120 | self.0 121 | .create_optimized_png(&options::parse_kw_opts(kwargs)?) 122 | .and_then(|data| Ok(data.into())) 123 | .or_else(error::handle_png_error) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /test/test_oxipng.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from shutil import copy2 3 | import gzip 4 | import oxipng 5 | import pytest 6 | 7 | 8 | @pytest.fixture 9 | def infile(tmpdir): 10 | filename = "shipitsquirrel.png" 11 | orig_path = Path() / "test" / filename 12 | input_path = Path(tmpdir) / filename 13 | copy2(orig_path, input_path) 14 | return input_path 15 | 16 | 17 | @pytest.fixture 18 | def outfile(tmpdir): 19 | return Path(tmpdir) / "shipitsquirrel-optimized.png" 20 | 21 | 22 | @pytest.fixture(scope="session") 23 | def indata(): 24 | with open(Path() / "test" / "shipitsquirrel.png", "rb") as f: 25 | return f.read() 26 | 27 | 28 | @pytest.fixture(scope="session") 29 | def rawdata(): 30 | with gzip.GzipFile(Path() / "test" / "python-logo-only.raw.gz") as f: 31 | return f.read() 32 | 33 | 34 | def test_init(outfile: Path): 35 | assert not outfile.exists() 36 | 37 | 38 | def test_optimize(infile, outfile): 39 | oxipng.optimize(infile, outfile) 40 | assert outfile.exists() 41 | assert infile.stat().st_size > outfile.stat().st_size 42 | 43 | 44 | def test_optimize_level(infile, outfile): 45 | oxipng.optimize(infile, outfile, level=6) 46 | assert outfile.exists() 47 | assert infile.stat().st_size > outfile.stat().st_size 48 | 49 | 50 | def test_optimize_opts(infile): 51 | initial_size = infile.stat().st_size 52 | oxipng.optimize( 53 | infile, 54 | fix_errors=True, 55 | force=True, 56 | # NOTE: set args deprecated in v9.1, will change to sequence 57 | filter={oxipng.RowFilter.Sub, oxipng.RowFilter.Up, oxipng.RowFilter.Average}, # type: ignore 58 | interlace=oxipng.Interlacing.Adam7, 59 | optimize_alpha=True, 60 | bit_depth_reduction=False, 61 | palette_reduction=False, 62 | grayscale_reduction=False, 63 | idat_recoding=False, 64 | scale_16=True, 65 | strip=oxipng.StripChunks.strip([b"cICP", b"sRGB"]), 66 | deflate=oxipng.Deflaters.libdeflater(12), 67 | fast_evaluation=False, 68 | timeout=100, 69 | ) 70 | assert infile.stat().st_size != initial_size 71 | 72 | 73 | def test_optimize_inplace(infile: Path): 74 | orig_size = infile.stat().st_size 75 | oxipng.optimize(infile) 76 | assert infile.stat().st_size < orig_size 77 | 78 | 79 | def test_optimize_from_memory(indata): 80 | assert len(oxipng.optimize_from_memory(indata)) < len(indata) 81 | 82 | 83 | def test_raises_pngerror(): 84 | with pytest.raises(oxipng.PngError): 85 | oxipng.optimize_from_memory(b"Hello World!") 86 | 87 | 88 | def test_raises_typeerror(indata): 89 | with pytest.raises(TypeError): 90 | oxipng.optimize_from_memory(indata, filter={1: 2}) # type: ignore 91 | 92 | 93 | def test_strip_chunks(): 94 | assert oxipng.StripChunks.none() 95 | assert oxipng.StripChunks.strip([b"sRGB"]) 96 | assert oxipng.StripChunks.safe() 97 | assert oxipng.StripChunks.keep([b"sRGB", b"pHYs"]) 98 | 99 | with pytest.raises(TypeError): 100 | assert oxipng.StripChunks.strip(["sRGB", 42]) # type: ignore 101 | 102 | with pytest.raises(TypeError): 103 | assert oxipng.StripChunks.keep([b"RGB"]) 104 | 105 | with pytest.raises(TypeError): 106 | assert oxipng.StripChunks.keep([b"RGB123"]) 107 | 108 | 109 | def test_deflate_zopfli(): 110 | assert oxipng.Deflaters.zopfli(1) 111 | assert oxipng.Deflaters.zopfli(42) 112 | assert oxipng.Deflaters.zopfli(255) 113 | 114 | with pytest.raises(TypeError): 115 | oxipng.Deflaters.zopfli(0) 116 | 117 | with pytest.raises(OverflowError): 118 | oxipng.Deflaters.zopfli(256) 119 | 120 | 121 | def test_raw_image(rawdata): 122 | raw = oxipng.RawImage(rawdata, 269, 326) 123 | raw.add_png_chunk(b"sRBG", b"\0") 124 | raw.add_icc_profile(b"Color LCD") 125 | optimized = raw.create_optimized_png( 126 | level=2, 127 | fix_errors=True, 128 | interlace=oxipng.Interlacing.Adam7, 129 | ) 130 | assert len(optimized) < len(rawdata) 131 | 132 | 133 | def test_raw_image_rgb(): 134 | raw = oxipng.RawImage( 135 | b"\1\2\3\4\5\6\7\6\5\1\2\3", 136 | 2, 137 | 2, 138 | color_type=oxipng.ColorType.rgb((4, 5, 6)), 139 | bit_depth=8, 140 | ) 141 | assert raw.create_optimized_png() 142 | 143 | 144 | def test_color_type(): 145 | assert oxipng.ColorType.grayscale() 146 | assert oxipng.ColorType.grayscale(42) 147 | 148 | with pytest.raises(OverflowError): 149 | oxipng.ColorType.grayscale(1_000_000) 150 | 151 | assert oxipng.ColorType.rgb() 152 | assert oxipng.ColorType.rgb((0, 0, 0)) 153 | assert oxipng.ColorType.rgb([65535, 65535, 65535]) 154 | 155 | with pytest.raises(OverflowError): 156 | assert oxipng.ColorType.rgb((65535, 65536, 65535)) 157 | 158 | assert oxipng.ColorType.indexed([[1, 2, 3, 4]]) 159 | assert oxipng.ColorType.indexed([(i, i, i, 255) for i in range(256)]) 160 | 161 | with pytest.raises(OverflowError): 162 | assert oxipng.ColorType.indexed([(255, 255, 255, 256)]) 163 | 164 | with pytest.raises(ValueError): 165 | assert oxipng.ColorType.indexed([(255, 255, 255)]) 166 | 167 | assert oxipng.ColorType.grayscale_alpha() 168 | assert oxipng.ColorType.rgba() 169 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use ::oxipng as oxi; 2 | use core::time::Duration; 3 | use pyo3::exceptions::PyTypeError; 4 | use pyo3::prelude::*; 5 | use std::{ 6 | collections::{hash_map::DefaultHasher, HashSet}, 7 | fmt::Debug, 8 | hash::{Hash, Hasher}, 9 | iter::Iterator, 10 | num::NonZeroU8, 11 | }; 12 | 13 | // NOTE: Deprecated as of v9.1 14 | // Should use sequence or set where appropriate 15 | #[derive(FromPyObject)] 16 | pub enum Collection { 17 | #[pyo3(transparent, annotation = "list | tuple")] 18 | Seq(Vec), 19 | #[pyo3(transparent, annotation = "set | frozenset")] 20 | Set(HashSet), 21 | } 22 | 23 | pub enum CollectionIterator { 24 | SeqIter(std::vec::IntoIter), 25 | SetIter(std::collections::hash_set::IntoIter), 26 | } 27 | 28 | impl Iterator for CollectionIterator { 29 | type Item = T; 30 | 31 | fn next(&mut self) -> Option { 32 | match self { 33 | CollectionIterator::SeqIter(iter) => iter.next(), 34 | CollectionIterator::SetIter(iter) => iter.next(), 35 | } 36 | } 37 | } 38 | 39 | impl IntoIterator for Collection { 40 | type Item = T; 41 | type IntoIter = CollectionIterator; 42 | 43 | fn into_iter(self) -> Self::IntoIter { 44 | match self { 45 | Collection::Seq(vec) => CollectionIterator::SeqIter(vec.into_iter()), 46 | Collection::Set(set) => { 47 | eprintln!( 48 | "(pyoxipng) Deprecation Warning: Python sets will not be accepted arguments in a future release. Please use a list or tuple instead." 49 | ); 50 | CollectionIterator::SetIter(set.into_iter()) 51 | } 52 | } 53 | } 54 | } 55 | 56 | impl Collection { 57 | pub fn remap(self) -> C 58 | where 59 | U: From, 60 | C: FromIterator, 61 | { 62 | self.into_iter().map(|i| i.into()).collect() 63 | } 64 | } 65 | 66 | // Filter 67 | #[pyclass(eq, eq_int)] 68 | #[derive(PartialEq, Eq, Clone, Debug, Hash)] 69 | pub enum RowFilter { 70 | NoOp = oxi::RowFilter::None as isize, 71 | Sub = oxi::RowFilter::Sub as isize, 72 | Up = oxi::RowFilter::Up as isize, 73 | Average = oxi::RowFilter::Average as isize, 74 | Paeth = oxi::RowFilter::Paeth as isize, 75 | MinSum = oxi::RowFilter::MinSum as isize, 76 | Entropy = oxi::RowFilter::Entropy as isize, 77 | Bigrams = oxi::RowFilter::Bigrams as isize, 78 | BigEnt = oxi::RowFilter::BigEnt as isize, 79 | Brute = oxi::RowFilter::Brute as isize, 80 | } 81 | 82 | #[pymethods] 83 | impl RowFilter { 84 | fn __hash__(&self) -> u64 { 85 | let mut hasher = DefaultHasher::new(); 86 | (self.clone() as u64).hash(&mut hasher); 87 | hasher.finish() 88 | } 89 | } 90 | 91 | impl From for oxi::RowFilter { 92 | fn from(val: RowFilter) -> Self { 93 | match val { 94 | RowFilter::NoOp => Self::None, 95 | RowFilter::Sub => Self::Sub, 96 | RowFilter::Up => Self::Up, 97 | RowFilter::Average => Self::Average, 98 | RowFilter::Paeth => Self::Paeth, 99 | RowFilter::MinSum => Self::MinSum, 100 | RowFilter::Entropy => Self::Entropy, 101 | RowFilter::Bigrams => Self::Bigrams, 102 | RowFilter::BigEnt => Self::BigEnt, 103 | RowFilter::Brute => Self::Brute, 104 | } 105 | } 106 | } 107 | 108 | // Interlacing 109 | #[pyclass(eq, eq_int)] 110 | #[derive(PartialEq, Clone, Debug, Hash)] 111 | pub enum Interlacing { 112 | Off = oxi::Interlacing::None as isize, 113 | Adam7 = oxi::Interlacing::Adam7 as isize, 114 | } 115 | 116 | #[pymethods] 117 | impl Interlacing { 118 | fn __hash__(&self) -> u64 { 119 | let mut hasher = DefaultHasher::new(); 120 | self.hash(&mut hasher); 121 | hasher.finish() 122 | } 123 | } 124 | 125 | impl From for oxi::Interlacing { 126 | fn from(val: Interlacing) -> Self { 127 | match val { 128 | Interlacing::Off => Self::None, 129 | Interlacing::Adam7 => Self::Adam7, 130 | } 131 | } 132 | } 133 | 134 | #[pyclass] 135 | #[derive(Debug, Clone)] 136 | pub struct StripChunks(pub oxi::StripChunks); 137 | 138 | #[pymethods] 139 | impl StripChunks { 140 | #[staticmethod] 141 | fn none() -> Self { 142 | Self(oxi::StripChunks::None) 143 | } 144 | 145 | #[staticmethod] 146 | fn strip(val: Collection<[u8; 4]>) -> PyResult { 147 | Ok(Self(oxi::StripChunks::Strip(val.remap()))) 148 | } 149 | 150 | #[staticmethod] 151 | fn safe() -> Self { 152 | Self(oxi::StripChunks::Safe) 153 | } 154 | 155 | #[staticmethod] 156 | fn keep(val: Collection<[u8; 4]>) -> PyResult { 157 | Ok(Self(oxi::StripChunks::Keep(val.remap()))) 158 | } 159 | #[staticmethod] 160 | fn all() -> Self { 161 | Self(oxi::StripChunks::All) 162 | } 163 | } 164 | 165 | #[pyclass] 166 | #[derive(Debug, Clone)] 167 | pub struct Deflaters(pub oxi::Deflaters); 168 | 169 | #[pymethods] 170 | impl Deflaters { 171 | #[staticmethod] 172 | fn libdeflater(compression: u8) -> Self { 173 | Self(oxi::Deflaters::Libdeflater { compression }) 174 | } 175 | 176 | #[staticmethod] 177 | fn zopfli(iterations: u8) -> PyResult { 178 | if let Some(iterations) = NonZeroU8::new(iterations) { 179 | Ok(Self(oxi::Deflaters::Zopfli { iterations })) 180 | } else { 181 | Err(PyTypeError::new_err(format!( 182 | "Invalid zopfli iterations {}; must be in range [1, 255]", 183 | iterations 184 | ))) 185 | } 186 | } 187 | } 188 | 189 | /// Extract a python value that may be None 190 | pub fn py_option<'a, T: FromPyObject<'a>>(val: &Bound<'a, PyAny>) -> PyResult> { 191 | if val.is_none() { 192 | Ok(None) 193 | } else { 194 | Ok(Some(val.extract()?)) 195 | } 196 | } 197 | 198 | /// Extract a python value that may be None and convert to another type 199 | pub fn py_option_extract<'a, T, U>(val: &Bound<'a, PyAny>) -> PyResult> 200 | where 201 | T: FromPyObject<'a>, 202 | U: From, 203 | { 204 | Ok(py_option::(val)?.and_then(|v| Some(v.into()))) 205 | } 206 | 207 | /// Extract a python float as a rust Duration type 208 | pub fn py_duration(val: &Bound<'_, PyAny>) -> PyResult> { 209 | Ok(py_option::(val)?.and_then(|v| Some(Duration::from_millis((v * 1000.) as u64)))) 210 | } 211 | -------------------------------------------------------------------------------- /oxipng.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | Python wrapper for multithreaded .png image file optimizer oxipng 3 | (https://github.com/shssoichiro/oxipng - written in Rust). Use this module to 4 | reduce the file size of your PNG images. 5 | """ 6 | 7 | from typing import Optional, Union, Sequence 8 | from enum import Enum 9 | from os import PathLike 10 | 11 | StrOrBytesPath = Union[str, bytes, PathLike] 12 | 13 | class PngError(Exception): 14 | """ 15 | Raised by optimize functions when an error is encountered while optimizing PNG files 16 | """ 17 | 18 | ... 19 | 20 | class RowFilter(Enum): 21 | """ 22 | enum entries for filter option 23 | """ 24 | 25 | NoOp = ... 26 | Sub = ... 27 | Up = ... 28 | Average = ... 29 | Paeth = ... 30 | MinSum = ... 31 | Entropy = ... 32 | Bigrams = ... 33 | BigEnt = ... 34 | Brute = ... 35 | 36 | class Interlacing(Enum): 37 | """ 38 | enum entries for interlace option 39 | """ 40 | 41 | Off = ... 42 | Adam7 = ... 43 | 44 | class StripChunks: 45 | """ 46 | Initialization class for strip option 47 | """ 48 | 49 | @staticmethod 50 | def none() -> "StripChunks": ... 51 | @staticmethod 52 | def strip(val: Sequence[bytes]) -> "StripChunks": ... 53 | @staticmethod 54 | def safe() -> "StripChunks": ... 55 | @staticmethod 56 | def keep(val: Sequence[bytes]) -> "StripChunks": ... 57 | @staticmethod 58 | def all() -> "StripChunks": ... 59 | 60 | class Deflaters: 61 | """ 62 | Initialization class for deflate option 63 | """ 64 | 65 | @staticmethod 66 | def libdeflater(compression: int) -> "Deflaters": ... 67 | @staticmethod 68 | def zopfli(iterations: int) -> "Deflaters": ... 69 | 70 | class Zopfli: 71 | """ 72 | Initialize a Zopfli deflate configuration option value 73 | """ 74 | 75 | def __init__(self, iterations: int) -> None: ... 76 | 77 | class Libdeflater: 78 | """ 79 | Initialize a Libdeflater deflate configuration option value 80 | """ 81 | 82 | def __init__(self) -> None: ... 83 | 84 | class ColorType: 85 | """ 86 | Initialization class for RawImage color_type option 87 | """ 88 | 89 | @staticmethod 90 | def grayscale(transparent_shade: Optional[int] = None) -> "ColorType": 91 | """ 92 | Grayscale, with one color channel. 93 | """ 94 | ... 95 | 96 | @staticmethod 97 | def rgb(transparent_color: Optional[Sequence[int]] = None) -> "ColorType": 98 | """ 99 | RGB, with three color channels. Specify optional color value that should 100 | be rendered as transparent. 101 | """ 102 | ... 103 | 104 | @staticmethod 105 | def indexed(palette: Sequence[Sequence[int]]) -> "ColorType": 106 | """ 107 | Indexed, with one byte per pixel representing a color from the palette. 108 | Specify palette containing the colors used, up to 256 entries 109 | """ 110 | ... 111 | 112 | @staticmethod 113 | def grayscale_alpha() -> "ColorType": 114 | """ 115 | Grayscale + Alpha, with two color channels. 116 | """ 117 | ... 118 | 119 | @staticmethod 120 | def rgba() -> "ColorType": 121 | """ 122 | RGBA, with four color channels. 123 | """ 124 | ... 125 | 126 | class RawImage: 127 | """ 128 | Create an optimized PNG file from raw image data 129 | """ 130 | 131 | def __init__( 132 | self, 133 | data: Union[bytes, bytearray], 134 | width: int, 135 | height: int, 136 | *, 137 | color_type: ColorType = ..., 138 | bit_depth: int = 8, 139 | ) -> None: ... 140 | 141 | def add_png_chunk(self, name: bytes, data: Union[bytes, bytearray]) -> None: 142 | """ 143 | Add a png chunk, such as `b"iTXt"`, to be included in the output 144 | """ 145 | ... 146 | 147 | def add_icc_profile(self, data: bytes) -> None: 148 | """ 149 | Add an ICC profile for the image 150 | """ 151 | ... 152 | 153 | def create_optimized_png( 154 | self, 155 | *, 156 | level: int = 2, 157 | fix_errors: bool = False, 158 | force: bool = False, 159 | filter: Sequence[RowFilter] = [RowFilter.NoOp], 160 | interlace: Optional[Interlacing] = None, 161 | optimize_alpha: bool = False, 162 | bit_depth_reduction: bool = True, 163 | color_type_reduction: bool = True, 164 | palette_reduction: bool = True, 165 | grayscale_reduction: bool = True, 166 | idat_recoding: bool = True, 167 | scale_16: bool = False, 168 | strip: StripChunks = StripChunks.none(), 169 | deflate: Deflaters = Deflaters.libdeflater(11), 170 | fast_evaluation: bool = False, 171 | timeout: Optional[int] = None, 172 | ) -> bytes: 173 | """ 174 | Create an optimized png from the raw image data. Full option 175 | descriptions at https://github.com/nfrasser/pyoxipng#options 176 | """ 177 | ... 178 | 179 | def optimize( 180 | input: StrOrBytesPath, 181 | output: Optional[StrOrBytesPath] = ..., 182 | *, 183 | level: int = 2, 184 | fix_errors: bool = False, 185 | force: bool = False, 186 | filter: Sequence[RowFilter] = [RowFilter.NoOp], 187 | interlace: Optional[Interlacing] = None, 188 | optimize_alpha: bool = False, 189 | bit_depth_reduction: bool = True, 190 | color_type_reduction: bool = True, 191 | palette_reduction: bool = True, 192 | grayscale_reduction: bool = True, 193 | idat_recoding: bool = True, 194 | scale_16: bool = False, 195 | strip: StripChunks = StripChunks.none(), 196 | deflate: Deflaters = Deflaters.libdeflater(11), 197 | fast_evaluation: bool = False, 198 | timeout: Optional[int] = None, 199 | ) -> None: 200 | """ 201 | Optimize a file on disk. Full option descriptions at 202 | https://github.com/nfrasser/pyoxipng#options 203 | """ 204 | ... 205 | 206 | def optimize_from_memory( 207 | data: bytes, 208 | *, 209 | level: int = 2, 210 | fix_errors: bool = False, 211 | force: bool = False, 212 | filter: Sequence[RowFilter] = [RowFilter.NoOp], 213 | interlace: Optional[Interlacing] = None, 214 | optimize_alpha: bool = False, 215 | bit_depth_reduction: bool = True, 216 | color_type_reduction: bool = True, 217 | palette_reduction: bool = True, 218 | grayscale_reduction: bool = True, 219 | idat_recoding: bool = True, 220 | scale_16: bool = False, 221 | strip: StripChunks = StripChunks.none(), 222 | deflate: Deflaters = Deflaters.libdeflater(11), 223 | fast_evaluation: bool = False, 224 | timeout: Optional[int] = None, 225 | ) -> bytes: 226 | """ 227 | Optimize raw data from a PNG file loaded in Python as a bytes object. Full 228 | option descriptions at https://github.com/nfrasser/pyoxipng#options 229 | """ 230 | ... 231 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | # This file is autogenerated by maturin v1.8.0 2 | # To update, run 3 | # 4 | # maturin generate-ci github --pytest 5 | # 6 | name: CI 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | - master 13 | tags: 14 | - "*" 15 | pull_request: 16 | workflow_dispatch: 17 | 18 | permissions: 19 | contents: read 20 | 21 | jobs: 22 | linux: 23 | runs-on: ${{ matrix.platform.runner }} 24 | strategy: 25 | matrix: 26 | platform: 27 | - runner: ubuntu-22.04 28 | target: x86_64 29 | # - runner: ubuntu-22.04 30 | # target: x86 31 | - runner: ubuntu-22.04 32 | target: aarch64 33 | # - runner: ubuntu-22.04 34 | # target: armv7 35 | # - runner: ubuntu-22.04 36 | # target: s390x 37 | # - runner: ubuntu-22.04 38 | # target: ppc64le 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: actions/setup-python@v5 42 | with: 43 | python-version: "3.13" 44 | - name: Build wheels 45 | uses: PyO3/maturin-action@v1 46 | with: 47 | target: ${{ matrix.platform.target }} 48 | before-script-linux: "(python3 -m pip --version || python3 -m ensurepip)" 49 | args: --release --out dist --interpreter 3.9 3.10 3.11 3.12 3.13 50 | sccache: "true" 51 | manylinux: 2_28 52 | - name: Upload wheels 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: wheels-linux-${{ matrix.platform.target }} 56 | path: dist 57 | - name: pytest 58 | if: ${{ startsWith(matrix.platform.target, 'x86_64') }} 59 | shell: bash 60 | run: | 61 | set -e 62 | python3 -m venv .venv 63 | source .venv/bin/activate 64 | pip install pyoxipng --find-links dist --force-reinstall 65 | pip install pytest 66 | pytest 67 | - name: pytest 68 | if: ${{ !startsWith(matrix.platform.target, 'x86') && matrix.platform.target != 'ppc64' }} 69 | uses: uraimo/run-on-arch-action@v2 70 | with: 71 | arch: ${{ matrix.platform.target }} 72 | distro: ubuntu22.04 73 | githubToken: ${{ github.token }} 74 | install: | 75 | apt-get update 76 | apt-get install -y --no-install-recommends python3 python3-pip 77 | pip3 install -U pip pytest 78 | run: | 79 | set -e 80 | pip3 install pyoxipng --find-links dist --force-reinstall 81 | pytest 82 | 83 | musllinux: 84 | runs-on: ${{ matrix.platform.runner }} 85 | strategy: 86 | matrix: 87 | platform: 88 | - runner: ubuntu-22.04 89 | target: x86_64 90 | - runner: ubuntu-22.04 91 | target: x86 92 | - runner: ubuntu-22.04 93 | target: aarch64 94 | - runner: ubuntu-22.04 95 | target: armv7 96 | steps: 97 | - uses: actions/checkout@v4 98 | - uses: actions/setup-python@v5 99 | with: 100 | python-version: "3.13" 101 | - name: Build wheels 102 | uses: PyO3/maturin-action@v1 103 | with: 104 | target: ${{ matrix.platform.target }} 105 | args: --release --out dist --interpreter 3.9 3.10 3.11 3.12 3.13 106 | sccache: "true" 107 | manylinux: musllinux_1_2 108 | - name: Upload wheels 109 | uses: actions/upload-artifact@v4 110 | with: 111 | name: wheels-musllinux-${{ matrix.platform.target }} 112 | path: dist 113 | - name: pytest 114 | if: ${{ startsWith(matrix.platform.target, 'x86_64') }} 115 | uses: addnab/docker-run-action@v3 116 | with: 117 | image: alpine:latest 118 | options: -v ${{ github.workspace }}:/io -w /io 119 | run: | 120 | set -e 121 | apk add py3-pip py3-virtualenv 122 | python3 -m virtualenv .venv 123 | source .venv/bin/activate 124 | pip install pyoxipng --no-index --find-links dist --force-reinstall 125 | pip install pytest 126 | pytest 127 | - name: pytest 128 | if: ${{ !startsWith(matrix.platform.target, 'x86') }} 129 | uses: uraimo/run-on-arch-action@v2 130 | with: 131 | arch: ${{ matrix.platform.target }} 132 | distro: alpine_latest 133 | githubToken: ${{ github.token }} 134 | install: | 135 | apk add py3-virtualenv 136 | run: | 137 | set -e 138 | python3 -m virtualenv .venv 139 | source .venv/bin/activate 140 | pip install pytest 141 | pip install pyoxipng --find-links dist --force-reinstall 142 | pytest 143 | 144 | windows: 145 | runs-on: ${{ matrix.platform.runner }} 146 | strategy: 147 | matrix: 148 | platform: 149 | - runner: windows-latest 150 | target: x64 151 | - runner: windows-latest 152 | target: x86 153 | steps: 154 | - uses: actions/checkout@v4 155 | - uses: actions/setup-python@v5 156 | with: 157 | python-version: "3.13" 158 | architecture: ${{ matrix.platform.target }} 159 | - name: Build wheels 160 | uses: PyO3/maturin-action@v1 161 | with: 162 | target: ${{ matrix.platform.target }} 163 | args: --release --out dist --interpreter 3.9 3.10 3.11 3.12 3.13 164 | sccache: "true" 165 | - name: Upload wheels 166 | uses: actions/upload-artifact@v4 167 | with: 168 | name: wheels-windows-${{ matrix.platform.target }} 169 | path: dist 170 | - name: pytest 171 | if: ${{ !startsWith(matrix.platform.target, 'aarch64') }} 172 | shell: bash 173 | run: | 174 | set -e 175 | python3 -m venv .venv 176 | source .venv/Scripts/activate 177 | pip install pyoxipng --find-links dist --force-reinstall 178 | pip install pytest 179 | pytest 180 | 181 | macos: 182 | runs-on: ${{ matrix.platform.runner }} 183 | strategy: 184 | matrix: 185 | platform: 186 | - runner: macos-13 187 | target: x86_64 188 | - runner: macos-14 189 | target: aarch64 190 | steps: 191 | - uses: actions/checkout@v4 192 | - uses: actions/setup-python@v5 193 | with: 194 | python-version: "3.13" 195 | - name: Build wheels 196 | uses: PyO3/maturin-action@v1 197 | with: 198 | target: ${{ matrix.platform.target }} 199 | args: --release --out dist --interpreter 3.9 3.10 3.11 3.12 3.13 200 | sccache: "true" 201 | - name: Upload wheels 202 | uses: actions/upload-artifact@v4 203 | with: 204 | name: wheels-macos-${{ matrix.platform.target }} 205 | path: dist 206 | - name: pytest 207 | run: | 208 | set -e 209 | python3 -m venv .venv 210 | source .venv/bin/activate 211 | pip install pyoxipng --find-links dist --force-reinstall 212 | pip install pytest 213 | pytest 214 | 215 | sdist: 216 | runs-on: ubuntu-latest 217 | steps: 218 | - uses: actions/checkout@v4 219 | - name: Build sdist 220 | uses: PyO3/maturin-action@v1 221 | with: 222 | command: sdist 223 | args: --out dist 224 | - name: Upload sdist 225 | uses: actions/upload-artifact@v4 226 | with: 227 | name: wheels-sdist 228 | path: dist 229 | 230 | release: 231 | name: Release 232 | runs-on: ubuntu-latest 233 | if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} 234 | needs: [linux, musllinux, windows, macos, sdist] 235 | permissions: 236 | # Use to sign the release artifacts 237 | id-token: write 238 | # Used to upload release artifacts 239 | contents: write 240 | # Used to generate artifact attestation 241 | attestations: write 242 | steps: 243 | - uses: actions/download-artifact@v4 244 | - name: Generate artifact attestation 245 | uses: actions/attest-build-provenance@v1 246 | with: 247 | subject-path: "wheels-*/*" 248 | - name: Publish to PyPI 249 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 250 | uses: PyO3/maturin-action@v1 251 | env: 252 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_PASSWORD }} 253 | with: 254 | command: upload 255 | args: --non-interactive --skip-existing wheels-*/* 256 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "d9007a1c015781864b7343fd8f45d90b602356d41cf4a0b5c8863a3559609d8a" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "maturin": { 18 | "hashes": [ 19 | "sha256:04f90b4cc03f78a616cc7054034f0184b61756d1497e1b87e011a70ce420ebe2", 20 | "sha256:11fbd89f07053c71904ae4afdf48ee18d41740d2f2939b91c838ccc5afd02e1a", 21 | "sha256:16d2ccd0476cf81022f01c45a77bf20fb4db06c04bfbbc77a1c85b27f9973e2d", 22 | "sha256:2a62793262eea8f85218accd2f877040e4b74af356a958bb769858ee23a275fd", 23 | "sha256:2f4abdf33e87ccb0d035a838e832945c6f7374222eacde508fafc752dda8ba6e", 24 | "sha256:422355ec4bab3ce025f826b25e2bb77e31a879c6cc06bb7a6fd3c1dd16a7e4fe", 25 | "sha256:4f55a717aebff0a16abb9f207a3c25edc350f0ef12b7f98ffb53f2287f221f58", 26 | "sha256:721f5f75e3645fa25576af549e62673c45f247e4567eb3a21fbb714b16d3e741", 27 | "sha256:90e1a022b5c735170f50867b1fa46338acc695b94fbf230376dc69ff9830606b", 28 | "sha256:935892c4cfa9483eda22034413db0a6cd71069b4539ee5107d137c428a0026b6", 29 | "sha256:acae3cfa51a68e512653103efa29e3321aa1ffb73b26cdcf3450f4a9806f8e00", 30 | "sha256:cca59e78121c90d26e80af8e0be624362d14816ce28e082d876208f2d29ea8b5", 31 | "sha256:e02add08490f8246381ee3d66d35aab5d002435e2c1c0e14e45849bda5c42e39" 32 | ], 33 | "index": "pypi", 34 | "markers": "python_version >= '3.7'", 35 | "version": "==1.8.0" 36 | } 37 | }, 38 | "develop": { 39 | "atomicwrites": { 40 | "hashes": [ 41 | "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11" 42 | ], 43 | "index": "pypi", 44 | "version": "==1.4.1" 45 | }, 46 | "black": { 47 | "hashes": [ 48 | "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", 49 | "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd", 50 | "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", 51 | "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", 52 | "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", 53 | "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7", 54 | "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", 55 | "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", 56 | "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", 57 | "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", 58 | "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", 59 | "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f", 60 | "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", 61 | "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", 62 | "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", 63 | "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", 64 | "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800", 65 | "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", 66 | "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", 67 | "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", 68 | "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", 69 | "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e" 70 | ], 71 | "index": "pypi", 72 | "markers": "python_version >= '3.9'", 73 | "version": "==24.10.0" 74 | }, 75 | "click": { 76 | "hashes": [ 77 | "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", 78 | "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" 79 | ], 80 | "markers": "python_version >= '3.7'", 81 | "version": "==8.1.8" 82 | }, 83 | "exceptiongroup": { 84 | "hashes": [ 85 | "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", 86 | "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" 87 | ], 88 | "markers": "python_version < '3.11'", 89 | "version": "==1.1.3" 90 | }, 91 | "importlib-metadata": { 92 | "hashes": [ 93 | "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", 94 | "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7" 95 | ], 96 | "index": "pypi", 97 | "markers": "python_version >= '3.8'", 98 | "version": "==8.5.0" 99 | }, 100 | "iniconfig": { 101 | "hashes": [ 102 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 103 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 104 | ], 105 | "markers": "python_version >= '3.7'", 106 | "version": "==2.0.0" 107 | }, 108 | "maturin": { 109 | "hashes": [ 110 | "sha256:04f90b4cc03f78a616cc7054034f0184b61756d1497e1b87e011a70ce420ebe2", 111 | "sha256:11fbd89f07053c71904ae4afdf48ee18d41740d2f2939b91c838ccc5afd02e1a", 112 | "sha256:16d2ccd0476cf81022f01c45a77bf20fb4db06c04bfbbc77a1c85b27f9973e2d", 113 | "sha256:2a62793262eea8f85218accd2f877040e4b74af356a958bb769858ee23a275fd", 114 | "sha256:2f4abdf33e87ccb0d035a838e832945c6f7374222eacde508fafc752dda8ba6e", 115 | "sha256:422355ec4bab3ce025f826b25e2bb77e31a879c6cc06bb7a6fd3c1dd16a7e4fe", 116 | "sha256:4f55a717aebff0a16abb9f207a3c25edc350f0ef12b7f98ffb53f2287f221f58", 117 | "sha256:721f5f75e3645fa25576af549e62673c45f247e4567eb3a21fbb714b16d3e741", 118 | "sha256:90e1a022b5c735170f50867b1fa46338acc695b94fbf230376dc69ff9830606b", 119 | "sha256:935892c4cfa9483eda22034413db0a6cd71069b4539ee5107d137c428a0026b6", 120 | "sha256:acae3cfa51a68e512653103efa29e3321aa1ffb73b26cdcf3450f4a9806f8e00", 121 | "sha256:cca59e78121c90d26e80af8e0be624362d14816ce28e082d876208f2d29ea8b5", 122 | "sha256:e02add08490f8246381ee3d66d35aab5d002435e2c1c0e14e45849bda5c42e39" 123 | ], 124 | "index": "pypi", 125 | "markers": "python_version >= '3.7'", 126 | "version": "==1.8.0" 127 | }, 128 | "mypy-extensions": { 129 | "hashes": [ 130 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 131 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 132 | ], 133 | "markers": "python_version >= '3.5'", 134 | "version": "==1.0.0" 135 | }, 136 | "packaging": { 137 | "hashes": [ 138 | "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", 139 | "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" 140 | ], 141 | "markers": "python_version >= '3.8'", 142 | "version": "==24.2" 143 | }, 144 | "pathspec": { 145 | "hashes": [ 146 | "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", 147 | "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" 148 | ], 149 | "markers": "python_version >= '3.8'", 150 | "version": "==0.12.1" 151 | }, 152 | "platformdirs": { 153 | "hashes": [ 154 | "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", 155 | "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" 156 | ], 157 | "markers": "python_version >= '3.8'", 158 | "version": "==4.3.6" 159 | }, 160 | "pluggy": { 161 | "hashes": [ 162 | "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", 163 | "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" 164 | ], 165 | "markers": "python_version >= '3.8'", 166 | "version": "==1.5.0" 167 | }, 168 | "pytest": { 169 | "hashes": [ 170 | "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", 171 | "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761" 172 | ], 173 | "index": "pypi", 174 | "markers": "python_version >= '3.8'", 175 | "version": "==8.3.4" 176 | }, 177 | "ruff": { 178 | "hashes": [ 179 | "sha256:0d5f89f254836799af1615798caa5f80b7f935d7a670fad66c5007928e57ace8", 180 | "sha256:13e9ec6d6b55f6da412d59953d65d66e760d583dd3c1c72bf1f26435b5bfdbae", 181 | "sha256:552fb6d861320958ca5e15f28b20a3d071aa83b93caee33a87b471f99a6c0835", 182 | "sha256:58072f0c06080276804c6a4e21a9045a706584a958e644353603d36ca1eb8a60", 183 | "sha256:6ddf5d654ac0d44389f6bf05cee4caeefc3132a64b58ea46738111d687352296", 184 | "sha256:736272574e97157f7edbbb43b1d046125fce9e7d8d583d5d65d0c9bf2c15addf", 185 | "sha256:8ef06f66f4a05c3ddbc9121a8b0cecccd92c5bf3dd43b5472ffe40b8ca10f0f8", 186 | "sha256:9183dd615d8df50defa8b1d9a074053891ba39025cf5ae88e8bcb52edcc4bf08", 187 | "sha256:97d9aefef725348ad77d6db98b726cfdb075a40b936c7984088804dfd38268a7", 188 | "sha256:9f8402b7c4f96463f135e936d9ab77b65711fcd5d72e5d67597b543bbb43cf3f", 189 | "sha256:ab78e33325a6f5374e04c2ab924a3367d69a0da36f8c9cb6b894a62017506111", 190 | "sha256:bf197b98ed86e417412ee3b6c893f44c8864f816451441483253d5ff22c0e81e", 191 | "sha256:c41319b85faa3aadd4d30cb1cffdd9ac6b89704ff79f7664b853785b48eccdf3", 192 | "sha256:e248b1f0fa2749edd3350a2a342b67b43a2627434c059a063418e3d375cfe643", 193 | "sha256:e4e56b3baa9c23d324ead112a4fdf20db9a3f8f29eeabff1355114dd96014604", 194 | "sha256:e5fe710ab6061592521f902fca7ebcb9fabd27bc7c57c764298b1c1f15fff720", 195 | "sha256:f21a1143776f8656d7f364bd264a9d60f01b7f52243fbe90e7670c0dfe0cf65d", 196 | "sha256:ffb60904651c00a1e0b8df594591770018a0f04587f7deeb3838344fe3adabac" 197 | ], 198 | "index": "pypi", 199 | "markers": "python_version >= '3.7'", 200 | "version": "==0.8.4" 201 | }, 202 | "tomli": { 203 | "hashes": [ 204 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 205 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 206 | ], 207 | "markers": "python_version < '3.11'", 208 | "version": "==2.0.1" 209 | }, 210 | "typing-extensions": { 211 | "hashes": [ 212 | "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", 213 | "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" 214 | ], 215 | "index": "pypi", 216 | "markers": "python_version >= '3.8'", 217 | "version": "==4.12.2" 218 | }, 219 | "zipp": { 220 | "hashes": [ 221 | "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", 222 | "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931" 223 | ], 224 | "markers": "python_version >= '3.9'", 225 | "version": "==3.21.0" 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyoxipng 2 | 3 | [![CI](https://github.com/nfrasser/pyoxipng/actions/workflows/CI.yml/badge.svg)](https://github.com/nfrasser/pyoxipng/actions/workflows/CI.yml) 4 | [![PyPI](https://badgen.net/pypi/v/pyoxipng)](https://pypi.org/project/pyoxipng/) 5 | 6 | Python wrapper for multithreaded .png image file optimizer 7 | [oxipng](https://github.com/shssoichiro/oxipng) (written in Rust). Use 8 | `pyoxipng` to reduce the file size of your PNG images. 9 | 10 | Jump to a section 11 | 12 | - [Installation](#installation) 13 | - [API](#api) 14 | 15 | - [optimize](#oxipngoptimizeinput-outputnone-kwargs) 16 | - [optimize_from_memory](#oxipngoptimize_from_memorydata-kwargs) 17 | - [RawImage](#oxipngrawimage) 18 | 19 | - [Options](#options) 20 | - [filter](#filter) 21 | - [interlace](#interlace) 22 | - [strip](#strip) 23 | - [deflate](#deflate) 24 | - [Development](#development) 25 | - [License](#license) 26 | 27 | ## Installation 28 | 29 | Install from PyPI: 30 | 31 | ```sh 32 | pip install pyoxipng 33 | ``` 34 | 35 | Import in your Python code: 36 | 37 | ```py 38 | import oxipng 39 | ``` 40 | 41 | ## API 42 | 43 | ### oxipng.optimize(input, output=None, \*\*kwargs) 44 | 45 | Optimize a file on disk. 46 | 47 | **Parameters**: 48 | 49 | - **input** _(str | bytes | PathLike)_ – path to input file to optimize 50 | - **output** _(str | bytes | PathLike, optional)_ – path to optimized output result file. If not specified, overwrites input. Defaults to None 51 | - **\*\*kwargs** – [Options](#options) 52 | 53 | **Returns** 54 | 55 | - None 56 | 57 | **Raises** 58 | 59 | - **oxipng.PngError** – optimization could not be completed 60 | 61 | **Examples:** 62 | 63 | Optimize a file on disk and overwrite 64 | 65 | ```py 66 | oxipng.optimize("/path/to/image.png") 67 | ``` 68 | 69 | Optimize a file and save to a new location: 70 | 71 | ```py 72 | oxipng.optimize("/path/to/image.png", "/path/to/image-optimized.png") 73 | ``` 74 | 75 | ### oxipng.optimize_from_memory(data, \*\*kwargs) 76 | 77 | Optimize raw data from a PNG file loaded in Python as a `bytes` object: 78 | 79 | **Parameters**: 80 | 81 | - **data** _(bytes)_ – raw PNG data to optimize 82 | - **\*\*kwargs** – [Options](#options) 83 | 84 | **Returns** 85 | 86 | - _(bytes)_ – optimized raw PNG data 87 | 88 | **Raises** 89 | 90 | - **oxipng.PngError** – optimization could not be completed 91 | 92 | **Examples:** 93 | 94 | ```py 95 | data = ... # bytes of png data 96 | optimized_data = oxipng.optimize_from_memory(data) 97 | with open("/path/to/image-optimized.png", "wb") as f: 98 | f.write(optimized_data) 99 | ``` 100 | 101 | ### oxipng.RawImage 102 | 103 | Create an optimized PNG file from raw image data: 104 | 105 | ```python 106 | raw = oxipng.RawImage(data, width, height) 107 | optimized_data = raw.create_optimized_png() 108 | ``` 109 | 110 | By default, assumes the input data is 8-bit, row-major RGBA, where every 4 bytes represents one pixel with Red-Green-Blue-Alpha channels. To interpret non-RGBA data, specify a `color_type` parameter with the `oxipng.ColorType` class: 111 | 112 | | Method | Description | 113 | | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | 114 | | `oxipng.ColorType.grayscale(int \| None)` | Grayscale, with one color channel. Specify optional shade of gray that should be rendered as transparent. | 115 | | `oxipng.ColorType.rgb(tuple[int, int, int])` | RGB, with three color channels. Specify optional color value that should be rendered as transparent. | 116 | | `oxipng.ColorType.indexed(list[[tuple[int, int, int, int]])` | Indexed, with one byte per pixel representing a color from the palette. Specify palette containing the colors used, up to 256 entries. | 117 | | `oxipng.ColorType.grayscale_alpha()` | Grayscale + Alpha, with two color channels. | 118 | | `oxipng.ColorType.rgba()` | RGBA, with four color channels. | 119 | 120 | **Parameters:** 121 | 122 | - **data** _(bytes | bytearray)_ – Raw image data bytes. Format depends on `color_type` and `bit_depth` parameters 123 | - **width** _(int)_ – Width of raw image, in pixels 124 | - **height** _(int)_ – Height of raw image, in pixels 125 | - **color_type** _([oxipng.ColorType, optional)_ – Descriptor for color type used to represent this image. Optional, defaults to `oxipng.ColorType.rgba()` 126 | - **bit_depth** _(int, optional)_ – Bit depth of raw image. Optional, defaults to 8 127 | 128 | **Examples:** 129 | 130 | Save RGB image data from a JPEG file, interpreting black pixels as transparent. 131 | 132 | ```python 133 | from PIL import Image 134 | import numpy as np 135 | 136 | # Load an image file with Pillow 137 | jpg = Image.open("/path/to/image.jpg") 138 | 139 | # Convert to RGB numpy array 140 | rgb_array = np.array(jpg.convert("RGB"), dtype=np.uint8) 141 | height, width, channels = rgb_array.shape 142 | 143 | # Create raw image with sRGB color profile 144 | data = rgb_array.tobytes() 145 | color_type = oxipng.ColorType.rgb((0, 0, 0)) # black is transparent 146 | raw = oxipng.RawImage(data, width, height, color_type=color_type) 147 | raw.add_png_chunk(b"sRGB", b"\0") 148 | 149 | # Optimize and save 150 | optimized = raw.create_optimized_png(level=6) 151 | with open("/path/to/image/optimized.png", "wb") as f: 152 | f.write(optimized) 153 | ``` 154 | 155 | Save with data where bytes reference a color palette 156 | 157 | ```python 158 | data = b"\0\1\2..." # get index data 159 | palette = [[0, 0, 0, 255], [1, 23, 234, 255], ...] 160 | color_type = oxipng.ColorType.indexed(palette) 161 | raw = oxipng.RawImage(data, 100, 100, color_type=color_type) 162 | optimized = raw.create_optimized_png() 163 | ``` 164 | 165 | **Methods:** 166 | 167 | #### add_png_chunk(name, data) 168 | 169 | Add a png chunk, such as `b"iTXt"`, to be included in the output 170 | 171 | **Parameters:** 172 | 173 | - **name** _(bytes)_ – PNG chunk identifier 174 | - **data** _(bytes | bytarray)_ 175 | 176 | **Returns:** 177 | 178 | - None 179 | 180 | #### add_icc_profile(data) 181 | 182 | Add an ICC profile for the image 183 | 184 | **Parameters:** 185 | 186 | - **data** _(bytes)_ – ICC profile data 187 | 188 | **Returns:** 189 | 190 | - None 191 | 192 | #### create_optimized_png(\*\*kwargs) 193 | 194 | Create an optimized png from the raw image data using the options provided 195 | 196 | **Parameters:** 197 | 198 | - **\*\*kwargs** – [Options](#options) 199 | 200 | **Returns:** 201 | 202 | - _(bytes)_ optimized PNG image data 203 | 204 | ## Options 205 | 206 | `optimize` , `optimize_from_memory` and `RawImage.create_optimized_png` accept the following options as keyword arguments. 207 | 208 | **Example:** 209 | 210 | ```py 211 | oxipng.optimize("/path/to/image.png", level=6, fix_errors=True, interlace=oxipng.Interlacing.Adam7) 212 | ``` 213 | 214 | | Option | Description | Type | Default | 215 | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------------------------- | 216 | | `level` | Set the optimization level to an integer between 0 and 6 (inclusive) | int | `2` | 217 | | `fix_errors` | Attempt to fix errors when decoding the input file rather than throwing `PngError` | bool | `False` | 218 | | `force` | Write to output even if there was no improvement in compression | bool | `False` | 219 | | `filter` | Which filters to try on the file. Use Use enum values from `oxipng.RowFilter` | Sequence[[RowFilter](#filter)] | `[RowFilter.NoOp]` | 220 | | `interlace` | Whether to change the interlacing type of the file. `None` will not change current interlacing type | [Interlacing](#interlace) \| None | `None` | 221 | | `optimize_alpha` | Whether to allow transparent pixels to be altered to improve compression | bool | `False` | 222 | | `bit_depth_reduction` | Whether to attempt bit depth reduction | bool | `True` | 223 | | `color_type_reduction` | Whether to attempt color type reduction | bool | `True` | 224 | | `palette_reduction` | Whether to attempt palette reduction | bool | `True` | 225 | | `grayscale_reduction` | Whether to attempt grayscale reduction | bool | `True` | 226 | | `idat_recoding` | If any type of reduction is performed, IDAT recoding will be performed regardless of this setting | bool | `True` | 227 | | `scale_16` | Whether to forcibly reduce 16-bit to 8-bit by scaling | bool | `False` | 228 | | `strip` | Which headers to strip from the PNG file, if any. Specify with `oxipng.StripChunks` | [StripChunks](#strip) | `StripChunks.none()` | 229 | | `deflate` | Which DEFLATE algorithm to use. Specify with `oxipng.Deflaters` | [Deflaters](#deflate) | `Deflaters.libdeflater()` | 230 | | `fast_evaluation` | Whether to use fast evaluation to pick the best filter | bool | `False` | 231 | | `timeout` | Maximum amount of time to spend (in seconds) on optimizations. Further potential optimizations skipped if the timeout is exceeded | float \| None | `None` | 232 | 233 | ### filter 234 | 235 | Initialize a `filter` list or tuple with any of the following `oxipng.RowFilter` enum options: 236 | 237 | - `oxipng.RowFilter.NoOp` 238 | - `oxipng.RowFilter.Sub` 239 | - `oxipng.RowFilter.Up` 240 | - `oxipng.RowFilter.Average` 241 | - `oxipng.RowFilter.Paeth` 242 | - `oxipng.RowFilter.Bigrams` 243 | - `oxipng.RowFilter.BigEnt` 244 | - `oxipng.RowFilter.Brute` 245 | 246 | ### interlace 247 | 248 | Set `interlace` to `None` to keep existing interlacing or to one of following `oxipng.Interlacing` enum options: 249 | 250 | - `oxipng.Interlacing.Off` (interlace disabled) 251 | - `oxipng.Interlacing.Adam7` (interlace enabled) 252 | 253 | ### strip 254 | 255 | Initialize the `strip` option with one of the following static methods in the 256 | `oxipng.StripChunks` class. 257 | 258 | | Method | Description | 259 | | ------------------------------------------- | ------------------------------------------------------------------------------------------- | 260 | | `oxipng.StripChunks.none()` | None | 261 | | `oxipng.StripChunks.strip(Sequence[bytes])` | Strip chunks specified in the given list | 262 | | `oxipng.StripChunks.safe()` | Strip chunks that won't affect rendering (all but cICP, iCCP, sRGB, pHYs, acTL, fcTL, fdAT) | 263 | | `oxipng.StripChunks.keep(Sequence[bytes])` | Strip all non-critical chunks except those in the given list | 264 | | `oxipng.StripChunks.all()` | Strip all non-critical chunks | 265 | 266 | ### deflate 267 | 268 | Initialize the `deflate` option with one of the following static methods in the 269 | `oxipng.Deflaters` class. 270 | 271 | | Method | Description | 272 | | ----------------------------------- | ---------------------------------------------------------- | 273 | | `oxipng.Deflaters.libdeflater(int)` | Libdeflater with compression level [0-12] | 274 | | `oxipng.Deflaters.zopfli(int)` | Zopfli with number of compression iterations to do [1-255] | 275 | 276 | ## Development 277 | 278 | 1. Install [Rust](https://www.rust-lang.org/tools/install) 279 | 1. Install [Python 3.8+](https://www.python.org/downloads/) 280 | 1. Install [Pipenv](https://pipenv.pypa.io/en/latest/) 281 | 1. Clone this repository and navigate to it via command line 282 | ```sh 283 | git clone https://github.com/nfrasser/pyoxipng.git 284 | cd pyoxipng 285 | ``` 286 | 1. Install dependencies 287 | ```sh 288 | pipenv install --dev 289 | ``` 290 | 1. Activate the dev environment 291 | ``` 292 | pipenv shell 293 | ``` 294 | 1. Build 295 | ```sh 296 | maturin develop 297 | ``` 298 | 1. Run tests 299 | ``` 300 | pytest 301 | ``` 302 | 1. Format code 303 | ``` 304 | ruff check . 305 | ruff format . 306 | ``` 307 | 308 | ## License 309 | 310 | MIT 311 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.20" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.7" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.4" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 49 | dependencies = [ 50 | "anstyle", 51 | "once_cell_polyfill", 52 | "windows-sys", 53 | ] 54 | 55 | [[package]] 56 | name = "autocfg" 57 | version = "1.5.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 60 | 61 | [[package]] 62 | name = "bitflags" 63 | version = "2.9.2" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" 66 | 67 | [[package]] 68 | name = "bitvec" 69 | version = "1.0.1" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 72 | dependencies = [ 73 | "funty", 74 | "radium", 75 | "tap", 76 | "wyz", 77 | ] 78 | 79 | [[package]] 80 | name = "bumpalo" 81 | version = "3.19.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 84 | 85 | [[package]] 86 | name = "bytemuck" 87 | version = "1.23.2" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" 90 | 91 | [[package]] 92 | name = "cc" 93 | version = "1.2.33" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" 96 | dependencies = [ 97 | "shlex", 98 | ] 99 | 100 | [[package]] 101 | name = "cfg-if" 102 | version = "1.0.3" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 105 | 106 | [[package]] 107 | name = "clap" 108 | version = "4.5.45" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" 111 | dependencies = [ 112 | "clap_builder", 113 | ] 114 | 115 | [[package]] 116 | name = "clap_builder" 117 | version = "4.5.44" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" 120 | dependencies = [ 121 | "anstream", 122 | "anstyle", 123 | "clap_lex", 124 | "strsim", 125 | "terminal_size", 126 | ] 127 | 128 | [[package]] 129 | name = "clap_lex" 130 | version = "0.7.5" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 133 | 134 | [[package]] 135 | name = "colorchoice" 136 | version = "1.0.4" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 139 | 140 | [[package]] 141 | name = "crc32fast" 142 | version = "1.5.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 145 | dependencies = [ 146 | "cfg-if", 147 | ] 148 | 149 | [[package]] 150 | name = "crossbeam-channel" 151 | version = "0.5.15" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 154 | dependencies = [ 155 | "crossbeam-utils", 156 | ] 157 | 158 | [[package]] 159 | name = "crossbeam-deque" 160 | version = "0.8.6" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 163 | dependencies = [ 164 | "crossbeam-epoch", 165 | "crossbeam-utils", 166 | ] 167 | 168 | [[package]] 169 | name = "crossbeam-epoch" 170 | version = "0.9.18" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 173 | dependencies = [ 174 | "crossbeam-utils", 175 | ] 176 | 177 | [[package]] 178 | name = "crossbeam-utils" 179 | version = "0.8.21" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 182 | 183 | [[package]] 184 | name = "either" 185 | version = "1.15.0" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 188 | 189 | [[package]] 190 | name = "env_filter" 191 | version = "0.1.3" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 194 | dependencies = [ 195 | "log", 196 | ] 197 | 198 | [[package]] 199 | name = "env_logger" 200 | version = "0.11.8" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 203 | dependencies = [ 204 | "anstream", 205 | "anstyle", 206 | "env_filter", 207 | "log", 208 | ] 209 | 210 | [[package]] 211 | name = "equivalent" 212 | version = "1.0.2" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 215 | 216 | [[package]] 217 | name = "errno" 218 | version = "0.3.13" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 221 | dependencies = [ 222 | "libc", 223 | "windows-sys", 224 | ] 225 | 226 | [[package]] 227 | name = "filetime" 228 | version = "0.2.26" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" 231 | dependencies = [ 232 | "cfg-if", 233 | "libc", 234 | "libredox", 235 | "windows-sys", 236 | ] 237 | 238 | [[package]] 239 | name = "funty" 240 | version = "2.0.0" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 243 | 244 | [[package]] 245 | name = "glob" 246 | version = "0.3.3" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 249 | 250 | [[package]] 251 | name = "hashbrown" 252 | version = "0.15.5" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 255 | 256 | [[package]] 257 | name = "heck" 258 | version = "0.5.0" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 261 | 262 | [[package]] 263 | name = "indexmap" 264 | version = "2.10.0" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 267 | dependencies = [ 268 | "equivalent", 269 | "hashbrown", 270 | "rayon", 271 | ] 272 | 273 | [[package]] 274 | name = "indoc" 275 | version = "2.0.6" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 278 | 279 | [[package]] 280 | name = "is_terminal_polyfill" 281 | version = "1.70.1" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 284 | 285 | [[package]] 286 | name = "libc" 287 | version = "0.2.175" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 290 | 291 | [[package]] 292 | name = "libdeflate-sys" 293 | version = "1.24.0" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "805824325366c44599dfeb62850fe3c7d7b3e3d75f9ab46785bc7dba3676815c" 296 | dependencies = [ 297 | "cc", 298 | ] 299 | 300 | [[package]] 301 | name = "libdeflater" 302 | version = "1.24.0" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "b270bcc7e9d6dce967a504a55b1b0444f966aa9184e8605b531bc0492abb30bb" 305 | dependencies = [ 306 | "libdeflate-sys", 307 | ] 308 | 309 | [[package]] 310 | name = "libredox" 311 | version = "0.1.9" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" 314 | dependencies = [ 315 | "bitflags", 316 | "libc", 317 | "redox_syscall", 318 | ] 319 | 320 | [[package]] 321 | name = "linux-raw-sys" 322 | version = "0.9.4" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 325 | 326 | [[package]] 327 | name = "log" 328 | version = "0.4.27" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 331 | 332 | [[package]] 333 | name = "memoffset" 334 | version = "0.9.1" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 337 | dependencies = [ 338 | "autocfg", 339 | ] 340 | 341 | [[package]] 342 | name = "once_cell" 343 | version = "1.21.3" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 346 | 347 | [[package]] 348 | name = "once_cell_polyfill" 349 | version = "1.70.1" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 352 | 353 | [[package]] 354 | name = "oxipng" 355 | version = "9.1.5" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "26c613f0f566526a647c7473f6a8556dbce22c91b13485ee4b4ec7ab648e4973" 358 | dependencies = [ 359 | "bitvec", 360 | "clap", 361 | "crossbeam-channel", 362 | "env_logger", 363 | "filetime", 364 | "glob", 365 | "indexmap", 366 | "libdeflater", 367 | "log", 368 | "rayon", 369 | "rgb", 370 | "rustc-hash", 371 | "zopfli", 372 | ] 373 | 374 | [[package]] 375 | name = "portable-atomic" 376 | version = "1.11.1" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 379 | 380 | [[package]] 381 | name = "proc-macro2" 382 | version = "1.0.101" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 385 | dependencies = [ 386 | "unicode-ident", 387 | ] 388 | 389 | [[package]] 390 | name = "pyo3" 391 | version = "0.25.1" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" 394 | dependencies = [ 395 | "indoc", 396 | "libc", 397 | "memoffset", 398 | "once_cell", 399 | "portable-atomic", 400 | "pyo3-build-config", 401 | "pyo3-ffi", 402 | "pyo3-macros", 403 | "unindent", 404 | ] 405 | 406 | [[package]] 407 | name = "pyo3-build-config" 408 | version = "0.25.1" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" 411 | dependencies = [ 412 | "once_cell", 413 | "target-lexicon", 414 | ] 415 | 416 | [[package]] 417 | name = "pyo3-ffi" 418 | version = "0.25.1" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" 421 | dependencies = [ 422 | "libc", 423 | "pyo3-build-config", 424 | ] 425 | 426 | [[package]] 427 | name = "pyo3-macros" 428 | version = "0.25.1" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" 431 | dependencies = [ 432 | "proc-macro2", 433 | "pyo3-macros-backend", 434 | "quote", 435 | "syn", 436 | ] 437 | 438 | [[package]] 439 | name = "pyo3-macros-backend" 440 | version = "0.25.1" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" 443 | dependencies = [ 444 | "heck", 445 | "proc-macro2", 446 | "pyo3-build-config", 447 | "quote", 448 | "syn", 449 | ] 450 | 451 | [[package]] 452 | name = "pyoxipng" 453 | version = "9.1.1" 454 | dependencies = [ 455 | "oxipng", 456 | "pyo3", 457 | ] 458 | 459 | [[package]] 460 | name = "quote" 461 | version = "1.0.40" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 464 | dependencies = [ 465 | "proc-macro2", 466 | ] 467 | 468 | [[package]] 469 | name = "radium" 470 | version = "0.7.0" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 473 | 474 | [[package]] 475 | name = "rayon" 476 | version = "1.11.0" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" 479 | dependencies = [ 480 | "either", 481 | "rayon-core", 482 | ] 483 | 484 | [[package]] 485 | name = "rayon-core" 486 | version = "1.13.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" 489 | dependencies = [ 490 | "crossbeam-deque", 491 | "crossbeam-utils", 492 | ] 493 | 494 | [[package]] 495 | name = "redox_syscall" 496 | version = "0.5.17" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" 499 | dependencies = [ 500 | "bitflags", 501 | ] 502 | 503 | [[package]] 504 | name = "rgb" 505 | version = "0.8.52" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" 508 | dependencies = [ 509 | "bytemuck", 510 | ] 511 | 512 | [[package]] 513 | name = "rustc-hash" 514 | version = "2.1.1" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 517 | 518 | [[package]] 519 | name = "rustix" 520 | version = "1.0.8" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 523 | dependencies = [ 524 | "bitflags", 525 | "errno", 526 | "libc", 527 | "linux-raw-sys", 528 | "windows-sys", 529 | ] 530 | 531 | [[package]] 532 | name = "shlex" 533 | version = "1.3.0" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 536 | 537 | [[package]] 538 | name = "simd-adler32" 539 | version = "0.3.7" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 542 | 543 | [[package]] 544 | name = "strsim" 545 | version = "0.11.1" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 548 | 549 | [[package]] 550 | name = "syn" 551 | version = "2.0.106" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 554 | dependencies = [ 555 | "proc-macro2", 556 | "quote", 557 | "unicode-ident", 558 | ] 559 | 560 | [[package]] 561 | name = "tap" 562 | version = "1.0.1" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 565 | 566 | [[package]] 567 | name = "target-lexicon" 568 | version = "0.13.2" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" 571 | 572 | [[package]] 573 | name = "terminal_size" 574 | version = "0.4.3" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" 577 | dependencies = [ 578 | "rustix", 579 | "windows-sys", 580 | ] 581 | 582 | [[package]] 583 | name = "unicode-ident" 584 | version = "1.0.18" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 587 | 588 | [[package]] 589 | name = "unindent" 590 | version = "0.2.4" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" 593 | 594 | [[package]] 595 | name = "utf8parse" 596 | version = "0.2.2" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 599 | 600 | [[package]] 601 | name = "windows-link" 602 | version = "0.1.3" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 605 | 606 | [[package]] 607 | name = "windows-sys" 608 | version = "0.60.2" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 611 | dependencies = [ 612 | "windows-targets", 613 | ] 614 | 615 | [[package]] 616 | name = "windows-targets" 617 | version = "0.53.3" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 620 | dependencies = [ 621 | "windows-link", 622 | "windows_aarch64_gnullvm", 623 | "windows_aarch64_msvc", 624 | "windows_i686_gnu", 625 | "windows_i686_gnullvm", 626 | "windows_i686_msvc", 627 | "windows_x86_64_gnu", 628 | "windows_x86_64_gnullvm", 629 | "windows_x86_64_msvc", 630 | ] 631 | 632 | [[package]] 633 | name = "windows_aarch64_gnullvm" 634 | version = "0.53.0" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 637 | 638 | [[package]] 639 | name = "windows_aarch64_msvc" 640 | version = "0.53.0" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 643 | 644 | [[package]] 645 | name = "windows_i686_gnu" 646 | version = "0.53.0" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 649 | 650 | [[package]] 651 | name = "windows_i686_gnullvm" 652 | version = "0.53.0" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 655 | 656 | [[package]] 657 | name = "windows_i686_msvc" 658 | version = "0.53.0" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 661 | 662 | [[package]] 663 | name = "windows_x86_64_gnu" 664 | version = "0.53.0" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 667 | 668 | [[package]] 669 | name = "windows_x86_64_gnullvm" 670 | version = "0.53.0" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 673 | 674 | [[package]] 675 | name = "windows_x86_64_msvc" 676 | version = "0.53.0" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 679 | 680 | [[package]] 681 | name = "wyz" 682 | version = "0.5.1" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" 685 | dependencies = [ 686 | "tap", 687 | ] 688 | 689 | [[package]] 690 | name = "zopfli" 691 | version = "0.8.2" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" 694 | dependencies = [ 695 | "bumpalo", 696 | "crc32fast", 697 | "log", 698 | "simd-adler32", 699 | ] 700 | --------------------------------------------------------------------------------