Skip to content

Commit dfac233

Browse files
migration to class based design
1 parent 1f116af commit dfac233

File tree

3 files changed

+193
-1
lines changed

3 files changed

+193
-1
lines changed

‎.gitignore‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
py_resizer/__pycache__
1+
__pycache__
22
.vscode
33
.DS_Store
44
py_resizer/__pycache__/exceptions.cpython-310.pyc
5+
code-intensive.jpeg
6+
main.py
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from pathlib import Path
2+
from threading import Thread
3+
from typing import Tuple, Union
4+
from uuid import uuid4
5+
6+
from PIL import Image
7+
8+
from image_utils.config import settings
9+
from image_utils.image_resizer.base import IImageResizer
10+
from image_utils.core.utils import print_if_verbose
11+
from image_utils.validators.base import IValidator
12+
13+
14+
class ImageResizer(IImageResizer):
15+
def __init__(self, validator: IValidator) -> None:
16+
self.validator = validator()
17+
18+
def resize_image(self, img_path: Union[Path, str],
19+
*, extension: str = None,
20+
img_height: int = 200, img_width: int = 200) -> Path:
21+
"""
22+
Resizes an image file to the new height and width as well as
23+
a new extension type if extension is provided
24+
25+
* `args`:
26+
`img_path`:`Path` | `str` -> Path to the image file to be processed
27+
28+
* `kwargs`:
29+
`extension`:`str` -> Defaults to `None`, If it is provided the
30+
image's file extension is changed to this type
31+
32+
`img_height`:`int` -> Defaults to `200`, If provided the image is
33+
resized to this `height`
34+
35+
`img_width`:`int` -> Defaults to `200`, If provided the image is
36+
resized to this `width`
37+
38+
* `returns` : `bool` -> `True` if the image was resized else `False`
39+
"""
40+
img_path = self.validator.validate_file(img_path)
41+
image = Image.open(img_path)
42+
43+
if hasattr(settings, 'DEFAULT_EXTENSION'):
44+
extension = settings.DEFAULT_EXTENSION
45+
46+
resized_image = image.resize(
47+
(img_width, img_height), Image.ANTIALIAS)
48+
49+
if extension:
50+
extension = extension.lower() if extension.startswith(
51+
'.') else '.' + extension.lower()
52+
img_path = img_path.with_suffix(extension)
53+
54+
# add a new prefix image file before saving
55+
previous_file_name = img_path.name
56+
resized_image_name = F'resized_{ previous_file_name }'
57+
img_format = img_path.suffix.upper()[1:]
58+
img_path = img_path.with_name(resized_image_name)
59+
60+
if img_path.exists():
61+
print_if_verbose(F'Duplicate resized image found at { img_path }')
62+
63+
if not settings.SKIP_EXISTING_FILES:
64+
print_if_verbose(F'Skipped resizing { img_path.name }\n')
65+
return img_path
66+
67+
if not settings.OVERRIDE_EXISTING_FILES:
68+
uuid_appended_name = '-'.join((img_path.stem, uuid4().hex))
69+
img_path = img_path.with_stem(uuid_appended_name)
70+
try:
71+
resized_image.save(img_path, img_format, quality=90)
72+
except Exception as e:
73+
print(F"{e}")
74+
print_if_verbose(
75+
F'Resized image - { previous_file_name } ({ img_width }x{ img_height }) (saved at { img_path }) in { img_format } format\n')
76+
return img_path
77+
78+
def resize_bulk_images(self, *,
79+
img_paths: Union[Tuple[Path], Tuple[str]] = (),
80+
img_dir: Union[Path, str] = None, save_as='png',
81+
extensions: str = None, img_height: int = 200,
82+
img_width: int = 200, recursive: bool = False) -> None:
83+
"""
84+
Resizes image file to the new height and width as well as
85+
a new extension type if extension is provided
86+
87+
`img_paths`:`Tuple[Path]` | `Tuple[str]` -> Tuple containing paths
88+
or string representation of the image paths to be processed
89+
90+
`img_dir`:`Path | str` -> Folder containing images to be processed
91+
92+
`extensions`:`str` -> Defaults to `None`, If it is provided the
93+
images with file extension type(s) are resized
94+
95+
`img_height`:`int` -> Defaults to `200`, If provided the image is
96+
resized to this `height`
97+
98+
`img_width`:`int` -> Defaults to `200`, If provided the image is
99+
resized to this `width`
100+
101+
`returns` : `int` -> Total number of images successfully resized
102+
"""
103+
104+
self.validator.validate_path_args(img_paths=img_paths, img_dir=img_dir)
105+
106+
# we use the img_paths argument if it is provided,
107+
# use the img_dir if the img_paths is not
108+
image_paths = img_paths or self.get_paths_from_dir(
109+
img_dir, extensions=extensions or settings.SUPPORTED_EXTENSIONS, recursive=recursive)
110+
111+
kwargs = {
112+
'extension': save_as,
113+
'img_height': img_height,
114+
'img_width': img_width
115+
}
116+
117+
threads: list[Thread] = []
118+
thread: Thread
119+
120+
for img_path in image_paths:
121+
thread = Thread(target=self.resize_image,
122+
args=(img_path,), kwargs=kwargs)
123+
thread.start()
124+
threads.append(thread)
125+
126+
for thread in threads:
127+
thread.join()
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from pathlib import Path
2+
from typing import Tuple, Union
3+
4+
from image_utils.core.exceptions import *
5+
from image_utils.config import settings
6+
from image_utils.validators.base import IValidator
7+
from image_utils.core.utils import get_supported_extensions_as_str
8+
9+
10+
class Validator(IValidator):
11+
def validate_file_extensions(self, file_: Path) -> None:
12+
if not file_.suffix.lower()[1:] in settings.SUPPORTED_EXTENSIONS:
13+
raise InvalidImageTypeError(
14+
'The image file extension is not supported, '
15+
F'supported extensions must be one of the following { get_supported_extensions_as_str() }')
16+
17+
def validate_path(self, path: Union[Path, str]) -> Path:
18+
""" Validates the existence of the provided path """
19+
20+
path = Path(path)
21+
22+
if not path.exists():
23+
raise FileNotFoundError(
24+
F'{ path } was not found in your path, kindly review the path provided')
25+
26+
return path
27+
28+
def validate_file(self, path: Union[Path, str]) -> Path:
29+
""" Carries out validation on the file provided """
30+
31+
path = self.validate_path(path)
32+
33+
if not path.is_file():
34+
raise NotAFileError(
35+
F'{ path } exists but is not a file, kindly pass a valid file path')
36+
37+
self.validate_file_extensions(path)
38+
39+
return path
40+
41+
42+
def validate_directory(self, path: Union[Path, str]) -> Path:
43+
""" Carries out validation on the directory provided """
44+
45+
path = self.validate_path(path)
46+
47+
if not path.is_dir():
48+
raise NotADirectoryError(
49+
F'{ path } exists but is not a directory, kindly pass a valid directory path')
50+
51+
return path
52+
53+
54+
def validate_path_args(self, *,
55+
img_paths: Union[Tuple[Path], Tuple[str]] = (),
56+
img_dir: Union[Path, str]) -> None:
57+
if img_paths and img_dir:
58+
raise MutualExclusionError('img_paths and img_dir are mutually exclusive'
59+
'only one of these may be provided')
60+
61+
if not (img_paths or img_dir):
62+
raise MissingRequiredPathError(
63+
'img_paths or img_dir must be provided')

0 commit comments

Comments
 (0)