Skip to content

Commit cebba0e

Browse files
authored
Merge pull request #61 from LeanderCS/ruff
Add IsImageValidator, ToBase64ImageFilter and ToImageFilter
2 parents c6eda9d + 41dc77e commit cebba0e

22 files changed

+605
-50
lines changed

docs/source/changelog.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@ Changelog
33

44
All notable changes to this project will be documented in this file.
55

6+
[0.6.2] - 2025-07-03
7+
--------------------
8+
9+
Added
10+
^^^^^
11+
- Added IsImageValidator, ToBase64ImageFilter and ToImageFilter.
12+
13+
14+
[0.6.1] - 2025-07-02
15+
--------------------
16+
17+
Changed
18+
^^^^^^^
19+
- Fixed issue with ``__init__.py`` for compiled versions.
20+
21+
622
[0.6.0] - 2025-06-30
723
--------------------
824

flask_inputfilter/_input_filter.pyx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,11 @@ cdef class InputFilter:
184184
dict[str, Any] validated_data
185185

186186
validated_data, errors = DataMixin.validate_with_conditions(
187-
self.fields, data, self.global_filters, self.global_validators, self.conditions
187+
self.fields,
188+
data,
189+
self.global_filters,
190+
self.global_validators,
191+
self.conditions,
188192
)
189193

190194
if errors:
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import warnings
24

35
warnings.warn(
@@ -7,5 +9,3 @@
79
DeprecationWarning,
810
stacklevel=2,
911
)
10-
11-
from flask_inputfilter.models import BaseCondition

flask_inputfilter/filters/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .string_slugify_filter import StringSlugifyFilter
1010
from .string_trim_filter import StringTrimFilter
1111
from .to_alpha_numeric_filter import ToAlphaNumericFilter
12+
from .to_base64_image_filter import ToBase64ImageFilter
1213
from .to_boolean_filter import ToBooleanFilter
1314
from .to_camel_case_filter import ToCamelCaseFilter
1415
from .to_dataclass_filter import ToDataclassFilter
@@ -17,6 +18,7 @@
1718
from .to_digits_filter import ToDigitsFilter
1819
from .to_enum_filter import ToEnumFilter
1920
from .to_float_filter import ToFloatFilter
21+
from .to_image_filter import ToImageFilter
2022
from .to_integer_filter import ToIntegerFilter
2123
from .to_iso_filter import ToIsoFilter
2224
from .to_lower_filter import ToLowerFilter
@@ -42,6 +44,7 @@
4244
"StringSlugifyFilter",
4345
"StringTrimFilter",
4446
"ToAlphaNumericFilter",
47+
"ToBase64ImageFilter",
4548
"ToBooleanFilter",
4649
"ToCamelCaseFilter",
4750
"ToDataclassFilter",
@@ -50,6 +53,7 @@
5053
"ToDigitsFilter",
5154
"ToEnumFilter",
5255
"ToFloatFilter",
56+
"ToImageFilter",
5357
"ToIntegerFilter",
5458
"ToIsoFilter",
5559
"ToLowerFilter",
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import warnings
24

35
warnings.warn(
@@ -7,5 +9,3 @@
79
DeprecationWarning,
810
stacklevel=2,
911
)
10-
11-
from flask_inputfilter.models import BaseFilter

flask_inputfilter/filters/string_slugify_filter.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,16 @@ def apply(self, value: Any) -> Union[Optional[str], Any]:
3838

3939
value_without_accents = "".join(
4040
char
41-
for char in unicodedata.normalize(UnicodeFormEnum.NFD.value, value)
41+
for char in unicodedata.normalize(
42+
UnicodeFormEnum.NFD.value,
43+
value,
44+
)
4245
if unicodedata.category(char) != "Mn"
4346
)
4447

4548
value = unicodedata.normalize(
46-
UnicodeFormEnum.NFKD.value, value_without_accents
49+
UnicodeFormEnum.NFKD.value,
50+
value_without_accents,
4751
)
4852
value = value.encode("ascii", "ignore").decode("ascii")
4953

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from __future__ import annotations
2+
3+
import base64
4+
import io
5+
from typing import Any, Optional
6+
7+
from PIL import Image
8+
9+
from flask_inputfilter.enums import ImageFormatEnum
10+
from flask_inputfilter.models import BaseFilter
11+
12+
13+
class ToBase64ImageFilter(BaseFilter):
14+
"""
15+
Converts an image to a base64 encoded string. Supports various input
16+
formats including file paths, bytes, or PIL Image objects.
17+
18+
**Parameters:**
19+
20+
- **format** (*ImageFormatEnum*, default: ``ImageFormatEnum.PNG``):
21+
The output image format for the base64 encoding.
22+
- **quality** (*int*, default: ``85``): The image quality (1-100) for
23+
lossy formats like JPEG. Higher values mean better quality.
24+
25+
**Expected Behavior:**
26+
27+
Converts the input image to a base64 encoded string:
28+
- If input is a PIL Image object, converts it directly
29+
- If input is a string, tries to open it as a file path
30+
- If input is bytes, tries to open as image data
31+
- If input is already a base64 string, validates and returns it
32+
- Returns the original value if conversion fails
33+
34+
**Example Usage:**
35+
36+
.. code-block:: python
37+
38+
class ImageFilter(InputFilter):
39+
def __init__(self):
40+
super().__init__()
41+
42+
self.add('image', filters=[
43+
ToBase64ImageFilter(format=ImageFormatEnum.JPEG)
44+
])
45+
"""
46+
47+
__slots__ = ("format", "quality")
48+
49+
def __init__(
50+
self,
51+
format: Optional[ImageFormatEnum] = None,
52+
quality: int = 85,
53+
) -> None:
54+
self.format = format if format else ImageFormatEnum.PNG
55+
self.quality = quality
56+
57+
def apply(self, value: Any) -> Any:
58+
if isinstance(value, Image.Image):
59+
return self._image_to_base64(value)
60+
61+
# Try to open as file path
62+
if isinstance(value, str):
63+
try:
64+
with Image.open(value) as img:
65+
return self._image_to_base64(img)
66+
except OSError:
67+
pass
68+
69+
# Try to decode as base64
70+
try:
71+
Image.open(io.BytesIO(base64.b64decode(value))).verify()
72+
return value
73+
except Exception:
74+
pass
75+
76+
# Try to open as raw bytes
77+
if isinstance(value, bytes):
78+
try:
79+
img = Image.open(io.BytesIO(value))
80+
return self._image_to_base64(img)
81+
except OSError:
82+
pass
83+
84+
return value
85+
86+
def _image_to_base64(self, image: Image.Image) -> str:
87+
"""Convert a PIL Image to base64 encoded string."""
88+
if image.mode in ("RGBA", "P"):
89+
image = image.convert("RGB")
90+
91+
buffered = io.BytesIO()
92+
93+
save_options = {"format": self.format.value}
94+
95+
if self.format in (ImageFormatEnum.JPEG, ImageFormatEnum.WEBP):
96+
save_options["quality"] = self.quality
97+
save_options["optimize"] = True
98+
99+
image.save(buffered, **save_options)
100+
101+
return base64.b64encode(buffered.getvalue()).decode("ascii")
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
import base64
4+
import io
5+
from typing import Any
6+
7+
from PIL import Image
8+
9+
from flask_inputfilter.models import BaseFilter
10+
11+
12+
class ToImageFilter(BaseFilter):
13+
"""
14+
Converts various input formats to a PIL Image object. Supports file paths,
15+
base64 encoded strings, and bytes.
16+
17+
**Expected Behavior:**
18+
19+
Converts the input to a PIL Image object:
20+
- If input is already a PIL Image object, returns it as-is
21+
- If input is a string, tries to open it as a file path or decode as base64
22+
- If input is bytes, tries to open as image data
23+
- Returns the original value if conversion fails
24+
25+
**Example Usage:**
26+
27+
.. code-block:: python
28+
29+
class ImageFilter(InputFilter):
30+
def __init__(self):
31+
super().__init__()
32+
33+
self.add('image', filters=[
34+
ToImageFilter()
35+
])
36+
"""
37+
38+
__slots__ = ()
39+
40+
def apply(self, value: Any) -> Any:
41+
if isinstance(value, Image.Image):
42+
return value
43+
44+
if isinstance(value, str):
45+
# Try to open as file path
46+
try:
47+
return Image.open(value)
48+
except OSError:
49+
pass
50+
51+
# Try to decode as base64
52+
try:
53+
return Image.open(io.BytesIO(base64.b64decode(value)))
54+
except Exception:
55+
pass
56+
57+
# Try to open as raw bytes
58+
if isinstance(value, bytes):
59+
try:
60+
return Image.open(io.BytesIO(value))
61+
except OSError:
62+
pass
63+
64+
return value

flask_inputfilter/filters/to_normalized_unicode_filter.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,14 @@ def apply(self, value: Any) -> Union[str, Any]:
5959

6060
value_without_accents = "".join(
6161
char
62-
for char in unicodedata.normalize(UnicodeFormEnum.NFD.value, value)
62+
for char in unicodedata.normalize(
63+
UnicodeFormEnum.NFD.value,
64+
value,
65+
)
6366
if unicodedata.category(char) != "Mn"
6467
)
6568

66-
return unicodedata.normalize(self.form.value, value_without_accents)
69+
return unicodedata.normalize(
70+
self.form.value,
71+
value_without_accents,
72+
)

flask_inputfilter/input_filter.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,9 @@ def set_data(self, data: dict[str, Any]) -> None:
235235
data to be filtered and stored.
236236
"""
237237
self.data = DataMixin.filter_data(
238-
data, self.fields, self.global_filters
238+
data,
239+
self.fields,
240+
self.global_filters,
239241
)
240242

241243
def get_value(self, name: str) -> Any:

0 commit comments

Comments
 (0)