imgproxykit 0.1.0__py3-none-any.whl

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.
Files changed (62) hide show
  1. imgproxykit/__init__.py +2 -0
  2. imgproxykit/builders.py +132 -0
  3. imgproxykit/client.py +167 -0
  4. imgproxykit/internal/__init__.py +0 -0
  5. imgproxykit/internal/constants.py +8 -0
  6. imgproxykit/internal/empty.py +8 -0
  7. imgproxykit/internal/encoding.py +13 -0
  8. imgproxykit/internal/paths.py +21 -0
  9. imgproxykit/internal/serialization.py +34 -0
  10. imgproxykit/options/__init__.py +62 -0
  11. imgproxykit/options/auto_rotate.py +39 -0
  12. imgproxykit/options/background.py +46 -0
  13. imgproxykit/options/base.py +41 -0
  14. imgproxykit/options/blur.py +38 -0
  15. imgproxykit/options/cache_buster.py +40 -0
  16. imgproxykit/options/crop.py +61 -0
  17. imgproxykit/options/dpr.py +40 -0
  18. imgproxykit/options/enforce_thumbnail.py +39 -0
  19. imgproxykit/options/enlarge.py +38 -0
  20. imgproxykit/options/expires.py +38 -0
  21. imgproxykit/options/extend.py +50 -0
  22. imgproxykit/options/extend_aspect_ratio.py +50 -0
  23. imgproxykit/options/filename.py +41 -0
  24. imgproxykit/options/flip.py +44 -0
  25. imgproxykit/options/format_quality.py +38 -0
  26. imgproxykit/options/gravity.py +48 -0
  27. imgproxykit/options/height.py +39 -0
  28. imgproxykit/options/keep_copyright.py +38 -0
  29. imgproxykit/options/max_animation_frame_resolution.py +39 -0
  30. imgproxykit/options/max_animation_frames.py +39 -0
  31. imgproxykit/options/max_bytes.py +39 -0
  32. imgproxykit/options/max_result_dimension.py +39 -0
  33. imgproxykit/options/max_src_file_size.py +39 -0
  34. imgproxykit/options/max_src_resolution.py +39 -0
  35. imgproxykit/options/min_height.py +38 -0
  36. imgproxykit/options/min_width.py +38 -0
  37. imgproxykit/options/padding.py +50 -0
  38. imgproxykit/options/pixelate.py +38 -0
  39. imgproxykit/options/preserve_hdr.py +38 -0
  40. imgproxykit/options/quality.py +38 -0
  41. imgproxykit/options/raw.py +39 -0
  42. imgproxykit/options/resize.py +55 -0
  43. imgproxykit/options/resizing_type.py +39 -0
  44. imgproxykit/options/return_attachment.py +39 -0
  45. imgproxykit/options/rotate.py +38 -0
  46. imgproxykit/options/sharpen.py +38 -0
  47. imgproxykit/options/size.py +45 -0
  48. imgproxykit/options/skip_processing.py +39 -0
  49. imgproxykit/options/strip_color_profile.py +39 -0
  50. imgproxykit/options/strip_metadata.py +38 -0
  51. imgproxykit/options/trim.py +52 -0
  52. imgproxykit/options/watermark.py +60 -0
  53. imgproxykit/options/width.py +39 -0
  54. imgproxykit/options/zoom.py +45 -0
  55. imgproxykit/py.typed +0 -0
  56. imgproxykit/signing.py +114 -0
  57. imgproxykit/transform.py +54 -0
  58. imgproxykit/types.py +95 -0
  59. imgproxykit-0.1.0.dist-info/METADATA +150 -0
  60. imgproxykit-0.1.0.dist-info/RECORD +62 -0
  61. imgproxykit-0.1.0.dist-info/WHEEL +4 -0
  62. imgproxykit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,39 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar, final, override
3
+
4
+ from imgproxykit.options.base import Option
5
+ from imgproxykit.types import Argument, FormatType, OptionType
6
+
7
+
8
+ @final
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class SkipProcessing(Option):
11
+ """
12
+ Skip processing listed formats when source and output formats match.
13
+
14
+ Processing is skipped only when the requested output format matches
15
+ the source format. Video thumbnail processing cannot be skipped.
16
+
17
+ Attributes:
18
+ formats: Source formats eligible for skipping processing.
19
+
20
+ Usage:
21
+
22
+ >>> option = SkipProcessing(formats=('svg', 'gif'))
23
+ >>> option.arguments
24
+ ('svg', 'gif')
25
+
26
+ See `imgproxy skip processing
27
+ <https://docs.imgproxy.net/usage/processing#skip-processing>`_.
28
+
29
+ """
30
+
31
+ formats: tuple[FormatType, ...]
32
+
33
+ _name: ClassVar[OptionType] = 'skp'
34
+
35
+ @property
36
+ @override
37
+ def arguments(self) -> tuple[Argument, ...]:
38
+ """Positional arguments in imgproxy protocol order."""
39
+ return self.formats
@@ -0,0 +1,39 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar, final, override
3
+
4
+ from imgproxykit.options.base import Option
5
+ from imgproxykit.types import Argument, OptionType
6
+
7
+
8
+ @final
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class StripColorProfile(Option):
11
+ """
12
+ Convert the embedded profile to sRGB and remove it.
13
+
14
+ When enabled, imgproxy converts the embedded ICC profile to sRGB
15
+ and removes it. Otherwise, imgproxy tries to preserve the profile.
16
+
17
+ Attributes:
18
+ value: Whether to convert and remove the embedded color profile.
19
+
20
+ Usage:
21
+
22
+ >>> option = StripColorProfile(value=True)
23
+ >>> option.arguments
24
+ (True,)
25
+
26
+ See `imgproxy strip color profile
27
+ <https://docs.imgproxy.net/usage/processing#strip-color-profile>`_.
28
+
29
+ """
30
+
31
+ value: bool
32
+
33
+ _name: ClassVar[OptionType] = 'scp'
34
+
35
+ @property
36
+ @override
37
+ def arguments(self) -> tuple[Argument, ...]:
38
+ """Positional arguments in imgproxy protocol order."""
39
+ return (self.value,)
@@ -0,0 +1,38 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar, final, override
3
+
4
+ from imgproxykit.options.base import Option
5
+ from imgproxykit.types import Argument, OptionType
6
+
7
+
8
+ @final
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class StripMetadata(Option):
11
+ """
12
+ Remove image metadata.
13
+
14
+ Overrides the server metadata stripping setting for this request.
15
+
16
+ Attributes:
17
+ value: Whether to remove EXIF, IPTC, and other image metadata.
18
+
19
+ Usage:
20
+
21
+ >>> option = StripMetadata(value=True)
22
+ >>> option.arguments
23
+ (True,)
24
+
25
+ See `imgproxy strip metadata
26
+ <https://docs.imgproxy.net/usage/processing#strip-metadata>`_.
27
+
28
+ """
29
+
30
+ value: bool
31
+
32
+ _name: ClassVar[OptionType] = 'sm'
33
+
34
+ @property
35
+ @override
36
+ def arguments(self) -> tuple[Argument, ...]:
37
+ """Positional arguments in imgproxy protocol order."""
38
+ return (self.value,)
@@ -0,0 +1,52 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar, final, override
3
+
4
+ from imgproxykit.options.base import Option
5
+ from imgproxykit.types import EMPTY, Argument, OptionType
6
+
7
+
8
+ @final
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class Trim(Option):
11
+ """
12
+ Trim the surrounding background with optional color and symmetry.
13
+
14
+ Removes the surrounding background before resizing. Trimming loads
15
+ the full image into memory and is unsupported for animated images.
16
+ Omitted settings remain :data:`~imgproxykit.types.EMPTY`.
17
+
18
+ Attributes:
19
+ threshold: Color similarity tolerance.
20
+ color: Optional hex background color; ``FF00FF`` selects
21
+ transparent backgrounds. Omission enables automatic detection.
22
+ equal_hor: Trim equal amounts from the left and right sides.
23
+ equal_ver: Trim equal amounts from the top and bottom sides.
24
+
25
+ Usage:
26
+
27
+ >>> option = Trim(threshold=10, color='ffffff', equal_hor=True)
28
+ >>> option.arguments
29
+ (10, 'ffffff', True, EMPTY)
30
+
31
+ See `imgproxy trim
32
+ <https://docs.imgproxy.net/usage/processing#trim>`_.
33
+
34
+ """
35
+
36
+ threshold: float
37
+ color: str | EMPTY = EMPTY
38
+ equal_hor: bool | EMPTY = EMPTY
39
+ equal_ver: bool | EMPTY = EMPTY
40
+
41
+ _name: ClassVar[OptionType] = 't'
42
+
43
+ @property
44
+ @override
45
+ def arguments(self) -> tuple[Argument, ...]:
46
+ """Positional arguments in imgproxy protocol order."""
47
+ return (
48
+ self.threshold,
49
+ self.color,
50
+ self.equal_hor,
51
+ self.equal_ver,
52
+ )
@@ -0,0 +1,60 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar, final, override
3
+
4
+ from imgproxykit.options.base import Option
5
+ from imgproxykit.types import EMPTY, Argument, OptionType, WatermarkPosition
6
+
7
+
8
+ @final
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class Watermark(Option):
11
+ """
12
+ Apply the watermark image configured on the imgproxy server.
13
+
14
+ Omitted settings remain :data:`~imgproxykit.types.EMPTY` and are
15
+ resolved by the server.
16
+
17
+ Attributes:
18
+ opacity: Multiplier applied to the configured base opacity.
19
+ position: Watermark anchor, or ``re`` to repeat it across the image.
20
+ offset_x: Horizontal offset, or horizontal spacing when repeating.
21
+ offset_y: Vertical offset, or vertical spacing when repeating.
22
+ scale: Size relative to the resulting image. Zero or omission
23
+ preserves the watermark size.
24
+
25
+ Usage:
26
+
27
+ >>> option = Watermark(
28
+ ... opacity=0.5, position='soea', offset_x=10, offset_y=10
29
+ ... )
30
+ >>> option.arguments
31
+ (0.5, 'soea', 10, 10, EMPTY)
32
+
33
+ See `imgproxy watermark
34
+ <https://docs.imgproxy.net/usage/processing#watermark>`_
35
+ and `watermark image configuration
36
+ <https://docs.imgproxy.net/features/watermark#specifying-watermark-image>`_.
37
+
38
+ """
39
+
40
+ # Public API:
41
+ opacity: float
42
+ position: WatermarkPosition | EMPTY = EMPTY
43
+ offset_x: float | EMPTY = EMPTY
44
+ offset_y: float | EMPTY = EMPTY
45
+ scale: float | EMPTY = EMPTY
46
+
47
+ # Private API:
48
+ _name: ClassVar[OptionType] = 'wm'
49
+
50
+ @property
51
+ @override
52
+ def arguments(self) -> tuple[Argument, ...]:
53
+ """Positional arguments in imgproxy protocol order."""
54
+ return (
55
+ self.opacity,
56
+ self.position,
57
+ self.offset_x,
58
+ self.offset_y,
59
+ self.scale,
60
+ )
@@ -0,0 +1,39 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar, final, override
3
+
4
+ from imgproxykit.options.base import Option
5
+ from imgproxykit.types import Argument, OptionType
6
+
7
+
8
+ @final
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class Width(Option):
11
+ """
12
+ Set the target width in pixels.
13
+
14
+ Zero lets imgproxy calculate width from height and aspect ratio.
15
+ With force resizing, zero preserves the source width.
16
+
17
+ Attributes:
18
+ value: Target width in pixels.
19
+
20
+ Usage:
21
+
22
+ >>> option = Width(value=300)
23
+ >>> option.arguments
24
+ (300,)
25
+
26
+ See `imgproxy width
27
+ <https://docs.imgproxy.net/usage/processing#width>`_.
28
+
29
+ """
30
+
31
+ value: int
32
+
33
+ _name: ClassVar[OptionType] = 'w'
34
+
35
+ @property
36
+ @override
37
+ def arguments(self) -> tuple[Argument, ...]:
38
+ """Positional arguments in imgproxy protocol order."""
39
+ return (self.value,)
@@ -0,0 +1,45 @@
1
+ from dataclasses import dataclass
2
+ from typing import ClassVar, final, override
3
+
4
+ from imgproxykit.options.base import Option
5
+ from imgproxykit.types import EMPTY, Argument, OptionType
6
+
7
+
8
+ @final
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class Zoom(Option):
11
+ """
12
+ Multiply dimensions by one factor or separate X and Y factors.
13
+
14
+ Unlike DPR, zoom does not scale gravity offsets, watermark offsets,
15
+ or padding. Factors must be greater than zero.
16
+
17
+ Attributes:
18
+ factor_x: Horizontal multiplier, also used vertically when
19
+ ``factor_y`` is omitted.
20
+ factor_y: Optional vertical multiplier.
21
+
22
+ Usage:
23
+
24
+ >>> option = Zoom(factor_x=2)
25
+ >>> option.arguments
26
+ (2, EMPTY)
27
+ >>> option = Zoom(factor_x=2, factor_y=1.5)
28
+ >>> option.arguments
29
+ (2, 1.5)
30
+
31
+ See `imgproxy zoom
32
+ <https://docs.imgproxy.net/usage/processing#zoom>`_.
33
+
34
+ """
35
+
36
+ factor_x: float
37
+ factor_y: float | EMPTY = EMPTY
38
+
39
+ _name: ClassVar[OptionType] = 'z'
40
+
41
+ @property
42
+ @override
43
+ def arguments(self) -> tuple[Argument, ...]:
44
+ """Positional arguments in imgproxy protocol order."""
45
+ return (self.factor_x, self.factor_y)
imgproxykit/py.typed ADDED
File without changes
imgproxykit/signing.py ADDED
@@ -0,0 +1,114 @@
1
+ import contextlib
2
+ import hmac
3
+
4
+ from imgproxykit.internal.constants import SIGNATURE_DIGEST, UNSAFE
5
+ from imgproxykit.internal.encoding import encode_base64
6
+ from imgproxykit.types import EMPTY
7
+
8
+
9
+ class URLSigner:
10
+ """
11
+ Sign imgproxy paths using a reusable key and salt pair.
12
+
13
+ Used by :class:`~imgproxykit.Client` after path construction.
14
+ Credentials are decoded once during initialization and reused for
15
+ subsequent signatures. Omitting both credentials enables unsigned mode.
16
+
17
+ Subclass this signer and override :meth:`__call__` to customize signing.
18
+ Set the subclass on :attr:`imgproxykit.Client.signer_cls` to use it
19
+ in a client.
20
+
21
+ """
22
+
23
+ __slots__ = ('_credentials',)
24
+
25
+ # Private API:
26
+ _credentials: tuple[bytes, bytes] | None
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ key: str | EMPTY = EMPTY,
32
+ salt: str | EMPTY = EMPTY,
33
+ ) -> None:
34
+ """
35
+ Configure signing credentials or enable unsigned mode.
36
+
37
+ Supply both credentials to generate signatures. Omit both to return
38
+ the ``unsafe`` marker, for servers with signature checking disabled.
39
+
40
+ Args:
41
+ key: Nonempty hex-encoded signing key matching the server.
42
+ salt: Nonempty hex-encoded signing salt matching the server.
43
+
44
+ Raises:
45
+ ValueError: If only one credential is supplied, or either
46
+ credential is empty or contains invalid hexadecimal data.
47
+
48
+ Usage:
49
+
50
+ >>> from imgproxykit.signing import URLSigner
51
+ >>> unsigned = URLSigner()
52
+ >>> signed = URLSigner(
53
+ ... key='736563726574',
54
+ ... salt='73616c74',
55
+ ... )
56
+
57
+ These credentials are examples for local development.
58
+
59
+ """
60
+ self._credentials = None
61
+
62
+ # Omitting both credentials selects unsigned mode.
63
+ if key is EMPTY and salt is EMPTY:
64
+ return
65
+
66
+ # Signing requires a complete key and salt pair.
67
+ if key is EMPTY or salt is EMPTY:
68
+ raise ValueError('key and salt must be supplied together.')
69
+
70
+ self._credentials = (
71
+ self._decode_hex(key),
72
+ self._decode_hex(salt),
73
+ )
74
+
75
+ def __call__(self, path: str) -> str:
76
+ """
77
+ Sign the exact supplied path or return the unsigned marker.
78
+
79
+ The signature is an HMAC-SHA256 digest of the salt followed by
80
+ the UTF-8 encoded path. The path is neither normalized nor modified.
81
+
82
+ Args:
83
+ path: Finalized path beginning with ``/``, including processing
84
+ options, the encoded source, and any output extension.
85
+ Excludes the server address and signature segment.
86
+
87
+ Returns:
88
+ URL-safe Base64 signature without padding, or ``unsafe`` when
89
+ signing credentials were omitted.
90
+
91
+ """
92
+ if self._credentials is None:
93
+ return UNSAFE
94
+
95
+ key, salt = self._credentials
96
+ signature = hmac.digest(
97
+ key,
98
+ salt + path.encode(),
99
+ SIGNATURE_DIGEST,
100
+ )
101
+ return encode_base64(signature)
102
+
103
+ def _decode_hex(self, value: str) -> bytes:
104
+ """
105
+ Decode a nonempty hexadecimal credential.
106
+
107
+ Invalid and empty values raise :class:`ValueError`.
108
+ """
109
+ with contextlib.suppress(ValueError):
110
+ decoded = bytes.fromhex(value)
111
+ if decoded:
112
+ return decoded
113
+
114
+ raise ValueError('Signing credentials must be nonempty hex strings.')
@@ -0,0 +1,54 @@
1
+ from dataclasses import dataclass
2
+
3
+ from imgproxykit.options import Option
4
+ from imgproxykit.types import EMPTY, FormatType
5
+
6
+
7
+ @dataclass(frozen=True, slots=True, kw_only=True)
8
+ class Transform:
9
+ """
10
+ Define reusable image processing options and an optional output format.
11
+
12
+ Instances are immutable and can be reused for multiple source images
13
+ and clients. Pass a transform to :meth:`imgproxykit.Client.url` to
14
+ include its processing options and output format in the generated URL.
15
+
16
+ Options are serialized in the supplied order. imgproxy determines the
17
+ image processing pipeline and resolves omitted option arguments using
18
+ its defaults. Creating a transform does not process an image or make
19
+ a network request.
20
+
21
+ Attributes:
22
+ options: Tuple of typed processing options. Defaults to an empty
23
+ tuple, which adds no processing option segments to the URL.
24
+ format: Output image format, such as ``webp`` or ``avif``.
25
+ Defaults to :data:`~imgproxykit.types.EMPTY`, leaving format
26
+ selection to imgproxy.
27
+
28
+ Usage:
29
+
30
+ >>> from imgproxykit import Client, Transform
31
+ >>> from imgproxykit.options import Quality, Resize
32
+ >>> thumbnail = Transform(
33
+ ... options=(
34
+ ... Resize(width=300, height=300, mode='fill'),
35
+ ... Quality(85),
36
+ ... ),
37
+ ... format='webp',
38
+ ... )
39
+ >>> client = Client('https://images.example.com')
40
+ >>> first_url = client.url(
41
+ ... 'https://example.com/first.jpg',
42
+ ... transform=thumbnail,
43
+ ... )
44
+ >>> second_url = client.url(
45
+ ... 'https://example.com/second.jpg',
46
+ ... transform=thumbnail,
47
+ ... )
48
+
49
+ """
50
+
51
+ # TODO: Add immutable composition; define duplicate option handling,
52
+ # format overrides, and explicitly clearing an inherited format.
53
+ options: tuple[Option, ...] = ()
54
+ format: FormatType | EMPTY = EMPTY
imgproxykit/types.py ADDED
@@ -0,0 +1,95 @@
1
+ from typing import Literal
2
+
3
+ from imgproxykit.internal.empty import EMPTY as EMPTY # noqa: PLC0414
4
+
5
+ type Argument = str | int | float | bool | EMPTY
6
+ type FormatType = Literal[
7
+ 'png',
8
+ 'jpg',
9
+ 'jxl',
10
+ 'webp',
11
+ 'avif',
12
+ 'gif',
13
+ 'ico',
14
+ 'svg',
15
+ 'heic',
16
+ 'bmp',
17
+ 'tiff',
18
+ ]
19
+ type OptionType = Literal[
20
+ 'w',
21
+ 'h',
22
+ 'mw',
23
+ 'mh',
24
+ 'rt',
25
+ 'el',
26
+ 'ar',
27
+ 'rot',
28
+ 'bl',
29
+ 'sh',
30
+ 'pix',
31
+ 'sm',
32
+ 'kcr',
33
+ 'scp',
34
+ 'ph',
35
+ 'eth',
36
+ 'mb',
37
+ 'raw',
38
+ 'exp',
39
+ 'att',
40
+ 'msr',
41
+ 'msfs',
42
+ 'maf',
43
+ 'mafr',
44
+ 'mrd',
45
+ 's',
46
+ 'z',
47
+ 'ex',
48
+ 'exar',
49
+ 'c',
50
+ 't',
51
+ 'pd',
52
+ 'fl',
53
+ 'bg',
54
+ 'fq',
55
+ 'skp',
56
+ 'fn',
57
+ 'cb',
58
+ 'rs',
59
+ 'g',
60
+ 'dpr',
61
+ 'q',
62
+ 'wm',
63
+ ]
64
+ type WatermarkPosition = Literal[
65
+ 'ce',
66
+ 'no',
67
+ 'so',
68
+ 'ea',
69
+ 'we',
70
+ 'noea',
71
+ 'now',
72
+ 'soea',
73
+ 'sow',
74
+ 're',
75
+ ]
76
+ type ResizeType = Literal[
77
+ 'fit',
78
+ 'fill',
79
+ 'fill-down',
80
+ 'force',
81
+ 'auto',
82
+ ]
83
+ type Position = Literal[
84
+ 'no',
85
+ 'so',
86
+ 'ea',
87
+ 'we',
88
+ 'noea',
89
+ 'now',
90
+ 'soea',
91
+ 'sow',
92
+ 'ce',
93
+ 'sm',
94
+ 'fp',
95
+ ]