pyverter 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyverter-0.1.1/CHANGES.rst +48 -0
- pyverter-0.1.1/LICENSE +21 -0
- pyverter-0.1.1/PKG-INFO +31 -0
- pyverter-0.1.1/README.rst +8 -0
- pyverter-0.1.1/pyproject.toml +162 -0
- pyverter-0.1.1/pyproject.toml.orig +162 -0
- pyverter-0.1.1/pyverter/__init__.py +3 -0
- pyverter-0.1.1/pyverter/cli.py +41 -0
- pyverter-0.1.1/pyverter/resize.py +171 -0
- pyverter-0.1.1/pyverter/stat.py +50 -0
- pyverter-0.1.1/pyverter/tools.py +10 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
CHANGELOG
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
.. towncrier release notes start
|
|
5
|
+
|
|
6
|
+
pyverter 0.1.1 (2026-08-23)
|
|
7
|
+
===========================
|
|
8
|
+
|
|
9
|
+
Miscellaneous
|
|
10
|
+
-------------
|
|
11
|
+
|
|
12
|
+
- Update license configuration in pyproject.toml to use SPDX expression and license-files.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
pyverter 0.1.0 (2026-08-23)
|
|
16
|
+
===========================
|
|
17
|
+
|
|
18
|
+
Features
|
|
19
|
+
--------
|
|
20
|
+
|
|
21
|
+
- Add tests for resizer (`#2 <https://github.com/fizyk/pyverter/issues/2>`_)
|
|
22
|
+
- Implemented multiprocessing - now images will be converted faster. (`#4 <https://github.com/fizyk/pyverter/issues/4>`_)
|
|
23
|
+
- Build a package structure and define an entrypoint script. This will make the resizer properly installable. (`#9 <https://github.com/fizyk/pyverter/issues/9>`_)
|
|
24
|
+
- List images and error encountered while processing them (`#27 <https://github.com/fizyk/pyverter/issues/27>`_)
|
|
25
|
+
- Print size change in summary (`#28 <https://github.com/fizyk/pyverter/issues/28>`_)
|
|
26
|
+
- Migrate to python 3.12 (`#40 <https://github.com/fizyk/pyverter/issues/40>`_)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Miscellaneous
|
|
30
|
+
-------------
|
|
31
|
+
|
|
32
|
+
- Lint and typecheck code. (`#1 <https://github.com/fizyk/pyverter/issues/1>`_)
|
|
33
|
+
- Added dependabot configuration and automerge workflow (`#3 <https://github.com/fizyk/pyverter/issues/3>`_)
|
|
34
|
+
- Add towncrier and tbump configuration and dependency to (`#5 <https://github.com/fizyk/pyverter/issues/5>`_)
|
|
35
|
+
- Drop dpi related code as they do not really matter. (`#10 <https://github.com/fizyk/pyverter/issues/10>`_)
|
|
36
|
+
- Removed resizer editable installation from Pipfile. It breaks dependabot. (`#15 <https://github.com/fizyk/pyverter/issues/15>`_)
|
|
37
|
+
- Freeze Pillow and click requirements in Pipfile (`#19 <https://github.com/fizyk/pyverter/issues/19>`_)
|
|
38
|
+
- Adjust progressbar typing for mypy and new click. (`#38 <https://github.com/fizyk/pyverter/issues/38>`_)
|
|
39
|
+
- Update code formatting with black 24.1 (`#55 <https://github.com/fizyk/pyverter/issues/55>`_)
|
|
40
|
+
- Extend pre-commit with pyproject-fmt to format pyproject.toml (`#337 <https://github.com/fizyk/pyverter/issues/337>`_)
|
|
41
|
+
- Migrate development environment and CI to uv (`#338 <https://github.com/fizyk/pyverter/issues/338>`_)
|
|
42
|
+
- Add release-schedule workflow. (`#343 <https://github.com/fizyk/pyverter/issues/343>`_)
|
|
43
|
+
- Migrated the Automerge workflow to `fizyk/actions-reuse` version 5.4.1. (`#348 <https://github.com/fizyk/pyverter/issues/348>`_)
|
|
44
|
+
- Configure Dependabot to update pre-commit dependencies. (`#351 <https://github.com/fizyk/pyverter/issues/351>`_)
|
|
45
|
+
- `#352 <https://github.com/fizyk/pyverter/issues/352>`_
|
|
46
|
+
- Settle on pyverter as the package name
|
|
47
|
+
- Update workflows for actions-reuse 3
|
|
48
|
+
- Use pre-commit for maintaining code style and linting
|
pyverter-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Grzegorz Śliwiński
|
|
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.
|
pyverter-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyverter
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: (Batch) Image resizer cli command based on Pillow
|
|
5
|
+
Keywords: cli,image,processing
|
|
6
|
+
Author: Grzegorz Śliwiński
|
|
7
|
+
Author-email: Grzegorz Śliwiński <fizyk+pypi@fizyk.dev>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Dist: click
|
|
17
|
+
Requires-Dist: pillow
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Project-URL: Source, https://github.com/fizyk/pyverter
|
|
20
|
+
Project-URL: Bug Tracker, https://github.com/fizyk/pyverter/issues
|
|
21
|
+
Project-URL: Changelog, https://github.com/fizyk/pyverter/blob/v0.1.1/CHANGES.rst
|
|
22
|
+
Description-Content-Type: text/x-rst
|
|
23
|
+
|
|
24
|
+
A (batch) File converter
|
|
25
|
+
========================
|
|
26
|
+
|
|
27
|
+
Program that will help you convert a whole collection of files in a batch.
|
|
28
|
+
|
|
29
|
+
At this stage, it can resize Images with Pillow
|
|
30
|
+
|
|
31
|
+
Plan is to allow more and provide a plugin system to be able to allow even more.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
A (batch) File converter
|
|
2
|
+
========================
|
|
3
|
+
|
|
4
|
+
Program that will help you convert a whole collection of files in a batch.
|
|
5
|
+
|
|
6
|
+
At this stage, it can resize Images with Pillow
|
|
7
|
+
|
|
8
|
+
Plan is to allow more and provide a plugin system to be able to allow even more.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "uv_build"
|
|
3
|
+
requires = ["uv-build>=0.12.5,<0.13"]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyverter"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "(Batch) Image resizer cli command based on Pillow"
|
|
9
|
+
readme = "README.rst"
|
|
10
|
+
keywords = [
|
|
11
|
+
"cli",
|
|
12
|
+
"image",
|
|
13
|
+
"processing",
|
|
14
|
+
]
|
|
15
|
+
license = "MIT"
|
|
16
|
+
license-files = ["LICENSE"]
|
|
17
|
+
requires-python = ">=3.12"
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Natural Language :: English",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"click",
|
|
28
|
+
"pillow",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[[project.authors]]
|
|
32
|
+
name = "Grzegorz Śliwiński"
|
|
33
|
+
email = "fizyk+pypi@fizyk.dev"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Source = "https://github.com/fizyk/pyverter"
|
|
37
|
+
"Bug Tracker" = "https://github.com/fizyk/pyverter/issues"
|
|
38
|
+
Changelog = "https://github.com/fizyk/pyverter/blob/v0.1.1/CHANGES.rst"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
pyverter = "pyverter.cli:pyverter"
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
dev = [
|
|
45
|
+
"click==8.4.2",
|
|
46
|
+
"mypy==2.3.1",
|
|
47
|
+
"pdbpp==0.12.1",
|
|
48
|
+
"pillow==12.3.0",
|
|
49
|
+
"pytest==9.1.1",
|
|
50
|
+
"pytest-cov==7.1.0",
|
|
51
|
+
"towncrier==25.8.0",
|
|
52
|
+
"types-pillow==10.2.0.20240822",
|
|
53
|
+
]
|
|
54
|
+
release = ["tbump==6.11.0"]
|
|
55
|
+
|
|
56
|
+
[tool.uv.build-backend]
|
|
57
|
+
module-name = "pyverter"
|
|
58
|
+
module-root = ""
|
|
59
|
+
source-include = ["CHANGES.rst"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
target-version = "py311"
|
|
63
|
+
line-length = 120
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint]
|
|
66
|
+
select = [
|
|
67
|
+
"E",
|
|
68
|
+
"F",
|
|
69
|
+
"I",
|
|
70
|
+
"D",
|
|
71
|
+
"RUF",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.pyproject-fmt]
|
|
75
|
+
column_width = 120
|
|
76
|
+
indent = 4
|
|
77
|
+
keep_full_version = true
|
|
78
|
+
generate_python_version_classifiers = false
|
|
79
|
+
table_format = "long"
|
|
80
|
+
sub_table_spacing = """
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
[tool.mypy]
|
|
85
|
+
exclude = ["build/*"]
|
|
86
|
+
|
|
87
|
+
[tool.pytest.ini_options]
|
|
88
|
+
xfail_strict = true
|
|
89
|
+
addopts = "--showlocals --verbose --cov"
|
|
90
|
+
testpaths = "tests"
|
|
91
|
+
|
|
92
|
+
[[tool.tbump.before_commit]]
|
|
93
|
+
name = "Build changelog"
|
|
94
|
+
cmd = "uv run towncrier build --version {new_version} --yes"
|
|
95
|
+
|
|
96
|
+
[[tool.tbump.field]]
|
|
97
|
+
name = "extra"
|
|
98
|
+
default = ""
|
|
99
|
+
|
|
100
|
+
[[tool.tbump.file]]
|
|
101
|
+
src = "pyverter/__init__.py"
|
|
102
|
+
|
|
103
|
+
[[tool.tbump.file]]
|
|
104
|
+
src = "pyproject.toml"
|
|
105
|
+
search = 'version = "{current_version}"'
|
|
106
|
+
|
|
107
|
+
[[tool.tbump.file]]
|
|
108
|
+
src = "pyproject.toml"
|
|
109
|
+
search = 'Changelog = "https://github.com/fizyk/pyverter/blob/v{current_version}/CHANGES.rst"'
|
|
110
|
+
|
|
111
|
+
[tool.tbump.git]
|
|
112
|
+
message_template = "Release {new_version}"
|
|
113
|
+
tag_template = "v{new_version}"
|
|
114
|
+
|
|
115
|
+
[tool.tbump.version]
|
|
116
|
+
current = "0.1.1"
|
|
117
|
+
regex = '''
|
|
118
|
+
(?P<major>\d+)
|
|
119
|
+
\.
|
|
120
|
+
(?P<minor>\d+)
|
|
121
|
+
\.
|
|
122
|
+
(?P<patch>\d+)
|
|
123
|
+
(\-
|
|
124
|
+
(?P<extra>.+)
|
|
125
|
+
)?
|
|
126
|
+
'''
|
|
127
|
+
|
|
128
|
+
[tool.towncrier]
|
|
129
|
+
directory = "newsfragments"
|
|
130
|
+
filename = "CHANGES.rst"
|
|
131
|
+
issue_format = "`#{issue} <https://github.com/fizyk/pyverter/issues/{issue}>`_"
|
|
132
|
+
single_file = true
|
|
133
|
+
|
|
134
|
+
[[tool.towncrier.type]]
|
|
135
|
+
directory = "break"
|
|
136
|
+
name = "Breaking changes"
|
|
137
|
+
showcontent = true
|
|
138
|
+
|
|
139
|
+
[[tool.towncrier.type]]
|
|
140
|
+
directory = "depr"
|
|
141
|
+
name = "Deprecations"
|
|
142
|
+
showcontent = true
|
|
143
|
+
|
|
144
|
+
[[tool.towncrier.type]]
|
|
145
|
+
directory = "feature"
|
|
146
|
+
name = "Features"
|
|
147
|
+
showcontent = true
|
|
148
|
+
|
|
149
|
+
[[tool.towncrier.type]]
|
|
150
|
+
directory = "bugfix"
|
|
151
|
+
name = "Bugfixes"
|
|
152
|
+
showcontent = true
|
|
153
|
+
|
|
154
|
+
[[tool.towncrier.type]]
|
|
155
|
+
directory = "docs"
|
|
156
|
+
name = "Documentation"
|
|
157
|
+
showcontent = true
|
|
158
|
+
|
|
159
|
+
[[tool.towncrier.type]]
|
|
160
|
+
directory = "misc"
|
|
161
|
+
name = "Miscellaneous"
|
|
162
|
+
showcontent = true
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "uv_build"
|
|
3
|
+
requires = [ "uv-build>=0.12.5,<0.13" ]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyverter"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "(Batch) Image resizer cli command based on Pillow"
|
|
9
|
+
readme = "README.rst"
|
|
10
|
+
keywords = [ "cli", "image", "processing" ]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = [ "LICENSE" ]
|
|
13
|
+
requires-python = ">=3.12"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Natural Language :: English",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"click",
|
|
24
|
+
"pillow",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[[project.authors]]
|
|
28
|
+
name = "Grzegorz Śliwiński"
|
|
29
|
+
email = "fizyk+pypi@fizyk.dev"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Source = "https://github.com/fizyk/pyverter"
|
|
33
|
+
"Bug Tracker" = "https://github.com/fizyk/pyverter/issues"
|
|
34
|
+
Changelog = "https://github.com/fizyk/pyverter/blob/v0.1.1/CHANGES.rst"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
pyverter = "pyverter.cli:pyverter"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"click==8.4.2",
|
|
42
|
+
"mypy==2.3.1",
|
|
43
|
+
"pdbpp==0.12.1",
|
|
44
|
+
"pillow==12.3.0",
|
|
45
|
+
"pytest==9.1.1",
|
|
46
|
+
"pytest-cov==7.1.0",
|
|
47
|
+
"towncrier==25.8.0",
|
|
48
|
+
"types-pillow==10.2.0.20240822",
|
|
49
|
+
]
|
|
50
|
+
release = [
|
|
51
|
+
"tbump==6.11.0",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.uv.build-backend]
|
|
55
|
+
module-name = "pyverter"
|
|
56
|
+
module-root = ""
|
|
57
|
+
source-include = [ "CHANGES.rst" ]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
target-version = "py311"
|
|
61
|
+
line-length = 120
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
select = [
|
|
65
|
+
"E", # pycodestyle
|
|
66
|
+
"F", # pyflakes
|
|
67
|
+
"I", # isort
|
|
68
|
+
"D", # pydocstyle
|
|
69
|
+
"RUF", # Ruff specific
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[tool.pyproject-fmt]
|
|
73
|
+
column_width = 120
|
|
74
|
+
indent = 4
|
|
75
|
+
keep_full_version = true
|
|
76
|
+
generate_python_version_classifiers = false
|
|
77
|
+
table_format = "long"
|
|
78
|
+
sub_table_spacing = "\n"
|
|
79
|
+
|
|
80
|
+
[tool.mypy]
|
|
81
|
+
exclude = [
|
|
82
|
+
"build/*",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[tool.pytest.ini_options]
|
|
86
|
+
xfail_strict = true
|
|
87
|
+
addopts = "--showlocals --verbose --cov"
|
|
88
|
+
testpaths = "tests"
|
|
89
|
+
|
|
90
|
+
[tool.tbump]
|
|
91
|
+
|
|
92
|
+
[[tool.tbump.before_commit]]
|
|
93
|
+
name = "Build changelog"
|
|
94
|
+
cmd = "uv run towncrier build --version {new_version} --yes"
|
|
95
|
+
|
|
96
|
+
[[tool.tbump.field]]
|
|
97
|
+
name = "extra"
|
|
98
|
+
default = ""
|
|
99
|
+
|
|
100
|
+
[[tool.tbump.file]]
|
|
101
|
+
src = "pyverter/__init__.py"
|
|
102
|
+
|
|
103
|
+
[[tool.tbump.file]]
|
|
104
|
+
src = "pyproject.toml"
|
|
105
|
+
search = 'version = "{current_version}"'
|
|
106
|
+
|
|
107
|
+
[[tool.tbump.file]]
|
|
108
|
+
src = "pyproject.toml"
|
|
109
|
+
search = 'Changelog = "https://github.com/fizyk/pyverter/blob/v{current_version}/CHANGES.rst"'
|
|
110
|
+
|
|
111
|
+
[tool.tbump.git]
|
|
112
|
+
message_template = "Release {new_version}"
|
|
113
|
+
tag_template = "v{new_version}"
|
|
114
|
+
|
|
115
|
+
[tool.tbump.version]
|
|
116
|
+
current = "0.1.1"
|
|
117
|
+
regex = '''
|
|
118
|
+
(?P<major>\d+)
|
|
119
|
+
\.
|
|
120
|
+
(?P<minor>\d+)
|
|
121
|
+
\.
|
|
122
|
+
(?P<patch>\d+)
|
|
123
|
+
(\-
|
|
124
|
+
(?P<extra>.+)
|
|
125
|
+
)?
|
|
126
|
+
'''
|
|
127
|
+
|
|
128
|
+
[tool.towncrier]
|
|
129
|
+
directory = "newsfragments"
|
|
130
|
+
filename = "CHANGES.rst"
|
|
131
|
+
issue_format = "`#{issue} <https://github.com/fizyk/pyverter/issues/{issue}>`_"
|
|
132
|
+
single_file = true
|
|
133
|
+
|
|
134
|
+
[[tool.towncrier.type]]
|
|
135
|
+
directory = "break"
|
|
136
|
+
name = "Breaking changes"
|
|
137
|
+
showcontent = true
|
|
138
|
+
|
|
139
|
+
[[tool.towncrier.type]]
|
|
140
|
+
directory = "depr"
|
|
141
|
+
name = "Deprecations"
|
|
142
|
+
showcontent = true
|
|
143
|
+
|
|
144
|
+
[[tool.towncrier.type]]
|
|
145
|
+
directory = "feature"
|
|
146
|
+
name = "Features"
|
|
147
|
+
showcontent = true
|
|
148
|
+
|
|
149
|
+
[[tool.towncrier.type]]
|
|
150
|
+
directory = "bugfix"
|
|
151
|
+
name = "Bugfixes"
|
|
152
|
+
showcontent = true
|
|
153
|
+
|
|
154
|
+
[[tool.towncrier.type]]
|
|
155
|
+
directory = "docs"
|
|
156
|
+
name = "Documentation"
|
|
157
|
+
showcontent = true
|
|
158
|
+
|
|
159
|
+
[[tool.towncrier.type]]
|
|
160
|
+
directory = "misc"
|
|
161
|
+
name = "Miscellaneous"
|
|
162
|
+
showcontent = true
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Main pyverter module."""
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from click import Context
|
|
7
|
+
|
|
8
|
+
from pyverter.resize import Resizer
|
|
9
|
+
from pyverter.stat import image_stats, list_images
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
@click.argument("path", type=click.Path(exists=True, file_okay=False, path_type=pathlib.Path))
|
|
14
|
+
@click.pass_context
|
|
15
|
+
def pyverter(ctx: Context, path: pathlib.Path) -> None:
|
|
16
|
+
"""Pyverter command group."""
|
|
17
|
+
ctx.ensure_object(dict)
|
|
18
|
+
ctx.obj["path"] = path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pyverter.command()
|
|
22
|
+
@click.pass_context
|
|
23
|
+
def stats(ctx: Context) -> None:
|
|
24
|
+
"""Print path's images stats."""
|
|
25
|
+
ctx.ensure_object(dict)
|
|
26
|
+
path = ctx.obj["path"]
|
|
27
|
+
click.echo(f"Reading images to process from {path.absolute()}")
|
|
28
|
+
count = image_stats(list_images(path))
|
|
29
|
+
click.echo(f"There's a total of {count} images.")
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pyverter.command()
|
|
34
|
+
@click.option("--max-size", type=int)
|
|
35
|
+
@click.pass_context
|
|
36
|
+
def resize(ctx: Context, max_size: int) -> None:
|
|
37
|
+
"""Resize images in given path."""
|
|
38
|
+
ctx.ensure_object(dict)
|
|
39
|
+
path = ctx.obj["path"]
|
|
40
|
+
resizer = Resizer(max_size, path, path)
|
|
41
|
+
resizer()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Resize algorithm."""
|
|
2
|
+
|
|
3
|
+
import multiprocessing
|
|
4
|
+
import pathlib
|
|
5
|
+
import signal
|
|
6
|
+
from multiprocessing import Process, Queue
|
|
7
|
+
from multiprocessing.synchronize import Event
|
|
8
|
+
from queue import Empty
|
|
9
|
+
from typing import Any, Dict, List, NamedTuple
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from PIL import Image
|
|
13
|
+
|
|
14
|
+
from pyverter.stat import list_images
|
|
15
|
+
from pyverter.tools import sizeof_fmt
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProcessedImages(NamedTuple):
|
|
19
|
+
"""Processed image message."""
|
|
20
|
+
|
|
21
|
+
image_path: pathlib.Path
|
|
22
|
+
width: int
|
|
23
|
+
height: int
|
|
24
|
+
size_before: int
|
|
25
|
+
size_after: int
|
|
26
|
+
processed: bool
|
|
27
|
+
error: Exception | None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NonInteruptableProcess(Process):
|
|
31
|
+
"""Catch signal interrupt for processes."""
|
|
32
|
+
|
|
33
|
+
def run(self) -> None:
|
|
34
|
+
"""Run process but immune to SIGINT."""
|
|
35
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
36
|
+
super().run()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Resizer:
|
|
40
|
+
"""Resizer algorithm."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, max_size: int, source: pathlib.Path, destination: pathlib.Path) -> None:
|
|
43
|
+
"""Initialize resizer."""
|
|
44
|
+
self.cpu_cores: int = multiprocessing.cpu_count()
|
|
45
|
+
self.max_size = max_size
|
|
46
|
+
self.source = source
|
|
47
|
+
self.destination = destination
|
|
48
|
+
|
|
49
|
+
def __call__(self) -> None:
|
|
50
|
+
"""Run resizer."""
|
|
51
|
+
stop_event: Event = multiprocessing.Event()
|
|
52
|
+
process_no: int = int(self.cpu_cores / 2)
|
|
53
|
+
|
|
54
|
+
# initial shard queue
|
|
55
|
+
images_to_process: Queue = Queue()
|
|
56
|
+
images_processed: Queue = Queue()
|
|
57
|
+
|
|
58
|
+
processes: List[NonInteruptableProcess] = []
|
|
59
|
+
for _ in range(process_no):
|
|
60
|
+
process = NonInteruptableProcess(
|
|
61
|
+
target=self.process_images,
|
|
62
|
+
args=(images_to_process, images_processed, stop_event),
|
|
63
|
+
)
|
|
64
|
+
processes.append(process)
|
|
65
|
+
process.start()
|
|
66
|
+
|
|
67
|
+
click.echo(f"Reading images to process from {self.source.absolute()}")
|
|
68
|
+
image_counter: int = 0
|
|
69
|
+
for image in list_images(self.source):
|
|
70
|
+
images_to_process.put(image)
|
|
71
|
+
image_counter += 1
|
|
72
|
+
click.echo(f"Found {image_counter} image files.")
|
|
73
|
+
|
|
74
|
+
converted: List[ProcessedImages] = []
|
|
75
|
+
skipped: List[ProcessedImages] = []
|
|
76
|
+
errored: List[ProcessedImages] = []
|
|
77
|
+
size_before = 0
|
|
78
|
+
size_after = 0
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
items: List[ProcessedImages] = []
|
|
82
|
+
with click.progressbar(
|
|
83
|
+
items,
|
|
84
|
+
show_percent=True,
|
|
85
|
+
show_pos=True,
|
|
86
|
+
length=image_counter,
|
|
87
|
+
label="Processing images",
|
|
88
|
+
) as bar:
|
|
89
|
+
processed_counter: int = 0
|
|
90
|
+
while True:
|
|
91
|
+
try:
|
|
92
|
+
image_processed: ProcessedImages = images_processed.get(timeout=5)
|
|
93
|
+
except Empty:
|
|
94
|
+
break
|
|
95
|
+
processed_counter += 1
|
|
96
|
+
bar.update(1)
|
|
97
|
+
size_before += image_processed.size_before
|
|
98
|
+
size_after += image_processed.size_after
|
|
99
|
+
if image_processed.processed:
|
|
100
|
+
converted.append(image_processed)
|
|
101
|
+
else:
|
|
102
|
+
skipped.append(image_processed)
|
|
103
|
+
if image_processed.error:
|
|
104
|
+
errored.append(image_processed)
|
|
105
|
+
if processed_counter >= image_counter:
|
|
106
|
+
break
|
|
107
|
+
except KeyboardInterrupt:
|
|
108
|
+
click.echo("Process stopped")
|
|
109
|
+
finally:
|
|
110
|
+
click.echo("Finished")
|
|
111
|
+
stop_event.set()
|
|
112
|
+
for process in processes:
|
|
113
|
+
process.join()
|
|
114
|
+
click.echo(f" {len(converted)} of images got converted and {len(skipped)} got skipped.")
|
|
115
|
+
click.echo(
|
|
116
|
+
f" Original size of images was {sizeof_fmt(size_before)} and got reduced to {sizeof_fmt(size_after)}"
|
|
117
|
+
)
|
|
118
|
+
if errored:
|
|
119
|
+
click.echo(f" There were {len(errored)} errors.")
|
|
120
|
+
for errored_image in errored:
|
|
121
|
+
click.echo(f" {errored_image.image_path} errored with f{errored_image.error}")
|
|
122
|
+
|
|
123
|
+
def process_images(self, to_process: Queue, processed: Queue, stop: Event) -> None:
|
|
124
|
+
"""Process images, one at a time."""
|
|
125
|
+
while True:
|
|
126
|
+
if stop.is_set():
|
|
127
|
+
break
|
|
128
|
+
try:
|
|
129
|
+
image_file: pathlib.Path = to_process.get(timeout=3)
|
|
130
|
+
except Empty:
|
|
131
|
+
continue
|
|
132
|
+
image_file.relative_to(self.source)
|
|
133
|
+
destination = self.destination / image_file.relative_to(self.source)
|
|
134
|
+
try:
|
|
135
|
+
processed.put(self.resize_image(image_file, destination))
|
|
136
|
+
except OSError as ex:
|
|
137
|
+
size = image_file.stat().st_size
|
|
138
|
+
processed.put(ProcessedImages(image_file, 0, 0, size, size, True, ex))
|
|
139
|
+
|
|
140
|
+
def resize_image(self, image_file: pathlib.Path, destination: pathlib.Path) -> ProcessedImages:
|
|
141
|
+
"""Resize image, return true if the resize had been performed."""
|
|
142
|
+
with Image.open(image_file) as image:
|
|
143
|
+
old_size = image_file.stat().st_size
|
|
144
|
+
if image.width <= self.max_size and image.height <= self.max_size:
|
|
145
|
+
return ProcessedImages(
|
|
146
|
+
destination,
|
|
147
|
+
image.width,
|
|
148
|
+
image.height,
|
|
149
|
+
old_size,
|
|
150
|
+
old_size,
|
|
151
|
+
False,
|
|
152
|
+
None,
|
|
153
|
+
)
|
|
154
|
+
if image.width > image.height:
|
|
155
|
+
height = int(image.height * self.max_size / image.width)
|
|
156
|
+
width = self.max_size
|
|
157
|
+
else:
|
|
158
|
+
width = int(image.width * self.max_size / image.height)
|
|
159
|
+
height = self.max_size
|
|
160
|
+
new_image = image.resize((int(width), int(height)), Image.Resampling.LANCZOS)
|
|
161
|
+
kwargs: Dict[str, Any] = {"quality": 75}
|
|
162
|
+
if exif := image.info.get("exif"):
|
|
163
|
+
kwargs["exif"] = exif
|
|
164
|
+
try:
|
|
165
|
+
new_image.save(destination, **kwargs)
|
|
166
|
+
new_size = destination.stat().st_size
|
|
167
|
+
return ProcessedImages(destination, width, height, old_size, new_size, True, None)
|
|
168
|
+
except ValueError:
|
|
169
|
+
return ProcessedImages(destination, width, height, old_size, old_size, False, None)
|
|
170
|
+
finally:
|
|
171
|
+
new_image.close()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Statistic gathering methods."""
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from queue import SimpleQueue
|
|
6
|
+
from typing import Dict, Generator
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from PIL import Image
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def list_images(path: pathlib.Path) -> Generator[pathlib.Path, None, None]:
|
|
13
|
+
"""List all images from path."""
|
|
14
|
+
directories: SimpleQueue[pathlib.Path] = SimpleQueue()
|
|
15
|
+
directories.put(path)
|
|
16
|
+
while not directories.empty():
|
|
17
|
+
current_directory = directories.get()
|
|
18
|
+
for children_path in current_directory.iterdir():
|
|
19
|
+
if children_path.is_dir():
|
|
20
|
+
directories.put(children_path)
|
|
21
|
+
else:
|
|
22
|
+
try:
|
|
23
|
+
with Image.open(children_path):
|
|
24
|
+
yield children_path
|
|
25
|
+
except IOError:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def count_images(path):
|
|
30
|
+
"""Count all images in a path."""
|
|
31
|
+
return sum(1 for _ in list_images(path))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def image_stats(images: Generator[pathlib.Path, None, None]) -> int:
|
|
35
|
+
"""Actual print all images statistics."""
|
|
36
|
+
size_stat: Dict = defaultdict(int)
|
|
37
|
+
images_count = 0
|
|
38
|
+
for image_file in images:
|
|
39
|
+
with Image.open(image_file) as image:
|
|
40
|
+
if image.width > image.height:
|
|
41
|
+
size = (image.width, image.height)
|
|
42
|
+
else:
|
|
43
|
+
size = (image.height, image.width)
|
|
44
|
+
size_stat[size] += 1
|
|
45
|
+
images_count += 1
|
|
46
|
+
click.echo("Sizes:")
|
|
47
|
+
for size, count in size_stat.items():
|
|
48
|
+
click.echo(f" {size[0]:5}x{size[1]:<5}: {count:6}")
|
|
49
|
+
|
|
50
|
+
return images_count
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Additional tools to use."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def sizeof_fmt(num, suffix="B"):
|
|
5
|
+
"""Format sizes with proper unit."""
|
|
6
|
+
for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"):
|
|
7
|
+
if abs(num) < 1024.0:
|
|
8
|
+
return f"{num:3.1f}{unit}{suffix}"
|
|
9
|
+
num /= 1024.0
|
|
10
|
+
return f"{num:.1f}Yi{suffix}"
|