py-img-processor 1.1.0__tar.gz → 1.2.2__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.

Potentially problematic release.


This version of py-img-processor might be problematic. Click here for more details.

Files changed (33) hide show
  1. {py_img_processor-1.1.0/py_img_processor.egg-info → py_img_processor-1.2.2}/PKG-INFO +13 -7
  2. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/README.md +11 -5
  3. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/__init__.py +5 -1
  4. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/enums.py +2 -0
  5. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/main.py +2 -2
  6. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/parsers/__init__.py +14 -13
  7. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/parsers/alpha.py +2 -2
  8. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/parsers/base.py +153 -29
  9. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/parsers/blur.py +2 -2
  10. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/parsers/circle.py +2 -2
  11. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/parsers/crop.py +10 -10
  12. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/parsers/gray.py +1 -1
  13. py_img_processor-1.2.2/imgprocessor/parsers/merge.py +148 -0
  14. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/parsers/resize.py +20 -16
  15. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/parsers/rotate.py +2 -2
  16. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/parsers/watermark.py +42 -23
  17. py_img_processor-1.2.2/imgprocessor/processor.py +130 -0
  18. {py_img_processor-1.1.0 → py_img_processor-1.2.2/py_img_processor.egg-info}/PKG-INFO +13 -7
  19. py_img_processor-1.2.2/py_img_processor.egg-info/requires.txt +2 -0
  20. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/setup.py +1 -1
  21. py_img_processor-1.1.0/imgprocessor/parsers/merge.py +0 -111
  22. py_img_processor-1.1.0/imgprocessor/processor.py +0 -128
  23. py_img_processor-1.1.0/py_img_processor.egg-info/requires.txt +0 -2
  24. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/LICENSE +0 -0
  25. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/MANIFEST.in +0 -0
  26. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/exceptions.py +0 -0
  27. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/imgprocessor/utils.py +0 -0
  28. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/py_img_processor.egg-info/SOURCES.txt +0 -0
  29. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/py_img_processor.egg-info/dependency_links.txt +0 -0
  30. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/py_img_processor.egg-info/entry_points.txt +0 -0
  31. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/py_img_processor.egg-info/not-zip-safe +0 -0
  32. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/py_img_processor.egg-info/top_level.txt +0 -0
  33. {py_img_processor-1.1.0 → py_img_processor-1.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: py-img-processor
3
- Version: 1.1.0
3
+ Version: 1.2.2
4
4
  Summary: Image editor using Python and Pillow.
5
5
  Home-page: https://github.com/SkylerHu/py-img-processor.git
6
6
  Author: SkylerHu
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
21
21
  Requires-Python: >=3.9
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
- Requires-Dist: py-enum>=1.1.1
24
+ Requires-Dist: py-enum>=2.1.1
25
25
  Requires-Dist: Pillow>=8
26
26
 
27
27
  # py-img-processor
@@ -71,6 +71,8 @@ Image editor using Python and Pillow.
71
71
  | PROCESSOR_MAX_PIXEL | int | width x height总像素3亿,处理前后的值都被此配置限制 | 300000000 |
72
72
  | PROCESSOR_DEFAULT_QUALITY | int | 图像处理后的默认质量 | 75 |
73
73
  | PROCESSOR_TEXT_FONT | str | 默认字体文件,默认从系统中寻找;也可以直接传递字体文件路径 | Arial Unicode.ttf |
74
+ | PROCESSOR_WORKSPACES | tuple | 限制水印等资源路径 (startswith匹配), 默认无限制 | `()` |
75
+ | PROCESSOR_ALLOW_DOMAINS | tuple | 限制链接地址域名 (endswith匹配),默认无限制 | `()` |
74
76
 
75
77
  > `注意`:`PROCESSOR_TEXT_FONT` 字体的设置是文字水印必要参数,需保证系统已安装该字体。默认值 `Arial Unicode.ttf` 是MacOS系统存在的字体,建议设置字体文件路径。
76
78
 
@@ -82,11 +84,15 @@ Image editor using Python and Pillow.
82
84
 
83
85
 
84
86
  ### 处理函数
85
- `process_image_by_path(input_path, out_path, params)`
87
+ ```python
88
+ from imgprocessor.processor import process_image
89
+
90
+ process_image(input_uri, out_path, params)
91
+ ```
86
92
 
87
93
  参数说明:
88
94
 
89
- - `input_path` str,输入图像文件路径
95
+ - `input_uri` str,输入图像文件路径或者链接地址
90
96
  - `out_path` str, 输出图像保存路径
91
97
  - `params` str or json,图像处理参数,参数说明详见 [Reference.md](https://github.com/SkylerHu/py-img-processor/blob/master/docs/Reference.md)
92
98
 
@@ -100,9 +106,9 @@ Image editor using Python and Pillow.
100
106
 
101
107
  ```python
102
108
  from imgprocessor.utils import base64url_encode
103
- from imgprocessor.processor import process_image_by_path
109
+ from imgprocessor.processor import process_image
104
110
 
105
- process_image_by_path(
111
+ process_image(
106
112
  "docs/imgs/lenna-400x225.jpg",
107
113
  "/tmp/output.png",
108
114
  # 对图片缩放、裁剪、生成圆角、并转成png存储
@@ -120,7 +126,7 @@ process_image_by_path(
120
126
  - 其他参数都放在 `actions` 数组中;
121
127
 
122
128
  ```python
123
- process_image_by_path(
129
+ process_image(
124
130
  "docs/imgs/lenna-400x225.jpg",
125
131
  "/tmp/output.png",
126
132
  {
@@ -45,6 +45,8 @@ Image editor using Python and Pillow.
45
45
  | PROCESSOR_MAX_PIXEL | int | width x height总像素3亿,处理前后的值都被此配置限制 | 300000000 |
46
46
  | PROCESSOR_DEFAULT_QUALITY | int | 图像处理后的默认质量 | 75 |
47
47
  | PROCESSOR_TEXT_FONT | str | 默认字体文件,默认从系统中寻找;也可以直接传递字体文件路径 | Arial Unicode.ttf |
48
+ | PROCESSOR_WORKSPACES | tuple | 限制水印等资源路径 (startswith匹配), 默认无限制 | `()` |
49
+ | PROCESSOR_ALLOW_DOMAINS | tuple | 限制链接地址域名 (endswith匹配),默认无限制 | `()` |
48
50
 
49
51
  > `注意`:`PROCESSOR_TEXT_FONT` 字体的设置是文字水印必要参数,需保证系统已安装该字体。默认值 `Arial Unicode.ttf` 是MacOS系统存在的字体,建议设置字体文件路径。
50
52
 
@@ -56,11 +58,15 @@ Image editor using Python and Pillow.
56
58
 
57
59
 
58
60
  ### 处理函数
59
- `process_image_by_path(input_path, out_path, params)`
61
+ ```python
62
+ from imgprocessor.processor import process_image
63
+
64
+ process_image(input_uri, out_path, params)
65
+ ```
60
66
 
61
67
  参数说明:
62
68
 
63
- - `input_path` str,输入图像文件路径
69
+ - `input_uri` str,输入图像文件路径或者链接地址
64
70
  - `out_path` str, 输出图像保存路径
65
71
  - `params` str or json,图像处理参数,参数说明详见 [Reference.md](./docs/Reference.md)
66
72
 
@@ -74,9 +80,9 @@ Image editor using Python and Pillow.
74
80
 
75
81
  ```python
76
82
  from imgprocessor.utils import base64url_encode
77
- from imgprocessor.processor import process_image_by_path
83
+ from imgprocessor.processor import process_image
78
84
 
79
- process_image_by_path(
85
+ process_image(
80
86
  "docs/imgs/lenna-400x225.jpg",
81
87
  "/tmp/output.png",
82
88
  # 对图片缩放、裁剪、生成圆角、并转成png存储
@@ -94,7 +100,7 @@ process_image_by_path(
94
100
  - 其他参数都放在 `actions` 数组中;
95
101
 
96
102
  ```python
97
- process_image_by_path(
103
+ process_image(
98
104
  "docs/imgs/lenna-400x225.jpg",
99
105
  "/tmp/output.png",
100
106
  {
@@ -5,7 +5,7 @@ import importlib
5
5
 
6
6
 
7
7
  __all__ = ["settings", "VERSION"]
8
- __version__ = "1.1.0"
8
+ __version__ = "1.2.2"
9
9
 
10
10
 
11
11
  VERSION = __version__
@@ -41,6 +41,10 @@ class SettingsProxy(object):
41
41
  PROCESSOR_DEFAULT_QUALITY = 75
42
42
  # 默认字体文件; 默认配置了MacOS系统中的字体
43
43
  PROCESSOR_TEXT_FONT = "Arial Unicode.ttf"
44
+ # 工作目录列表:例如水印文件必须限制在设定的目录下,避免恶意访问文件
45
+ PROCESSOR_WORKSPACES = ()
46
+ # 当资源文件uri使用链接地址时,限制地址域名来源
47
+ PROCESSOR_ALLOW_DOMAINS = ()
44
48
 
45
49
  def __getattribute__(self, attr: str) -> typing.Any:
46
50
  try:
@@ -62,6 +62,8 @@ class ArgType(ChoiceEnum):
62
62
  STRING = ("str", "字符串")
63
63
  INTEGER = ("int", "整数")
64
64
  FLOAT = ("float", "浮点数")
65
+ URI = ("uri", "资源路径/链接")
66
+ ACTION = ("action", "对图像的操作")
65
67
 
66
68
 
67
69
  class Geography(ChoiceEnum):
@@ -7,7 +7,7 @@ import argparse
7
7
  import traceback
8
8
 
9
9
  from imgprocessor import VERSION
10
- from imgprocessor.processor import ProcessParams, process_image_by_path
10
+ from imgprocessor.processor import ProcessParams, process_image
11
11
 
12
12
 
13
13
  def main(argv: typing.Optional[list[str]] = None) -> int:
@@ -95,7 +95,7 @@ def main(argv: typing.Optional[list[str]] = None) -> int:
95
95
  if not os.path.exists(cur_out_dir):
96
96
  os.makedirs(cur_out_dir)
97
97
  try:
98
- process_image_by_path(file_path, out_path, param_str)
98
+ process_image(file_path, out_path, param_str)
99
99
  print(f"{tag}\t 成功", flush=True)
100
100
  except Exception as e:
101
101
  print(f"{tag}\t \033[31m失败:{e}\033[0m", file=sys.stderr, flush=True)
@@ -4,7 +4,8 @@ import typing
4
4
 
5
5
  from imgprocessor import enums
6
6
  from imgprocessor.exceptions import ParamParseException
7
- from .base import BaseParser, ImgSaveParser # noqa: F401
7
+
8
+ from .base import BaseParser, ImgSaveParser
8
9
  from .resize import ResizeParser
9
10
  from .crop import CropParser
10
11
  from .circle import CircleParser
@@ -16,16 +17,16 @@ from .watermark import WatermarkParser
16
17
  from .merge import MergeParser
17
18
 
18
19
 
19
- _ACTION_PARASER_MAP: dict[str, BaseParser] = {
20
- enums.OpAction.RESIZE: ResizeParser, # type: ignore
21
- enums.OpAction.CROP: CropParser, # type: ignore
22
- enums.OpAction.CIRCLE: CircleParser, # type: ignore
23
- enums.OpAction.BLUR: BlurParser, # type: ignore
24
- enums.OpAction.ROTATE: RotateParser, # type: ignore
25
- enums.OpAction.ALPHA: AlphaParser, # type: ignore
26
- enums.OpAction.GRAY: GrayParser, # type: ignore
27
- enums.OpAction.WATERMARK: WatermarkParser, # type: ignore
28
- enums.OpAction.MERGE: MergeParser, # type: ignore
20
+ _ACTION_PARASER_MAP: dict[str, typing.Any] = {
21
+ enums.OpAction.RESIZE.value: ResizeParser,
22
+ enums.OpAction.CROP.value: CropParser,
23
+ enums.OpAction.CIRCLE.value: CircleParser,
24
+ enums.OpAction.BLUR.value: BlurParser,
25
+ enums.OpAction.ROTATE.value: RotateParser,
26
+ enums.OpAction.ALPHA.value: AlphaParser,
27
+ enums.OpAction.GRAY.value: GrayParser,
28
+ enums.OpAction.WATERMARK.value: WatermarkParser,
29
+ enums.OpAction.MERGE.value: MergeParser,
29
30
  }
30
31
 
31
32
 
@@ -40,7 +41,7 @@ class ProcessParams(object):
40
41
  ) -> None:
41
42
  self.save_parser: ImgSaveParser = ImgSaveParser.init(kwargs, enable_base64=enable_base64) # type: ignore
42
43
 
43
- _actions = []
44
+ _actions: list[BaseParser] = []
44
45
  for i in actions or []:
45
46
  key = i.get("key")
46
47
  cls = _ACTION_PARASER_MAP.get(key)
@@ -76,7 +77,7 @@ class ProcessParams(object):
76
77
  key, param_str = info
77
78
  if not key:
78
79
  raise ParamParseException(f"参数必须指定操作类型 [{item}]不符合参数要求")
79
- if key in [enums.OpAction.FORMAT, enums.OpAction.QUALITY, enums.OpAction.INTERLACE]:
80
+ if key in [enums.OpAction.FORMAT.value, enums.OpAction.QUALITY.value, enums.OpAction.INTERLACE.value]:
80
81
  save_args.append(f"{key}_{param_str}")
81
82
  else:
82
83
  action_cls = _ACTION_PARASER_MAP.get(key)
@@ -10,10 +10,10 @@ from .base import BaseParser, pre_processing
10
10
 
11
11
  class AlphaParser(BaseParser):
12
12
 
13
- KEY = enums.OpAction.ALPHA
13
+ KEY = enums.OpAction.ALPHA.value
14
14
  ARGS = {
15
15
  # 不透明度, 为100时,完全不透明,即原图; 为0时,完全透明
16
- "value": {"type": enums.ArgType.INTEGER, "default": 100, "min": 0, "max": 100},
16
+ "value": {"type": enums.ArgType.INTEGER.value, "default": 100, "min": 0, "max": 100},
17
17
  }
18
18
 
19
19
  def __init__(
@@ -1,11 +1,22 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf-8
1
3
  import typing
2
4
 
5
+ import os
3
6
  import re
7
+ import tempfile
8
+ import urllib.parse
9
+ from urllib.request import urlretrieve
10
+ from contextlib import contextmanager
4
11
 
5
12
  from PIL import Image, ImageOps
6
13
 
7
- from imgprocessor import enums, utils
8
- from imgprocessor.exceptions import ParamValidateException, ParamParseException
14
+ from py_enum import ChoiceEnum
15
+ from imgprocessor import settings, enums, utils
16
+ from imgprocessor.exceptions import ParamValidateException, ParamParseException, ProcessLimitException
17
+
18
+
19
+ _ALLOW_SCHEMES = ("http", "https")
9
20
 
10
21
 
11
22
  class BaseParser(object):
@@ -58,16 +69,23 @@ class BaseParser(object):
58
69
  else:
59
70
  value = kwargs.get(key)
60
71
  try:
61
- if _type == enums.ArgType.INTEGER:
72
+ if _type == enums.ArgType.INTEGER.value:
62
73
  value = cls._validate_number(value, **config)
63
- elif _type == enums.ArgType.FLOAT:
74
+ elif _type == enums.ArgType.FLOAT.value:
64
75
  value = cls._validate_number(value, use_float=True, **config)
65
- elif _type == enums.ArgType.STRING:
76
+ elif _type == enums.ArgType.STRING.value:
66
77
  value = cls._validate_str(value, enable_base64=enable_base64, **config)
78
+ elif _type == enums.ArgType.URI.value:
79
+ value = cls._validate_uri(value, enable_base64=enable_base64, **config)
80
+ elif _type == enums.ArgType.ACTION.value:
81
+ if value and isinstance(value, str):
82
+ value = cls._validate_str(value, enable_base64=enable_base64, **config)
67
83
 
68
84
  choices = config.get("choices")
85
+ if isinstance(choices, ChoiceEnum):
86
+ choices = choices.values
69
87
  if choices and value not in choices:
70
- raise ParamValidateException(f"{key}枚举值只能是其中之一 {choices.values}")
88
+ raise ParamValidateException(f"{key}枚举值只能是其中之一 {choices}")
71
89
  except ParamValidateException as e:
72
90
  raise ParamValidateException(f"参数 {key}={value} 不符合要求:{e}")
73
91
  data[key] = value
@@ -82,7 +100,7 @@ class BaseParser(object):
82
100
  regex: typing.Optional[str] = None,
83
101
  base64_encode: bool = False,
84
102
  max_length: typing.Optional[int] = None,
85
- **kwargs: dict,
103
+ **kwargs: typing.Any,
86
104
  ) -> str:
87
105
  if not isinstance(value, str):
88
106
  raise ParamValidateException("参数类型不符合要求,必须是字符串类型")
@@ -101,7 +119,7 @@ class BaseParser(object):
101
119
  min: typing.Optional[int] = None,
102
120
  max: typing.Optional[int] = None,
103
121
  use_float: bool = False,
104
- **kwargs: dict,
122
+ **kwargs: typing.Any,
105
123
  ) -> typing.Union[int, float]:
106
124
  if isinstance(value, int) or (use_float and isinstance(value, (int, float))):
107
125
  v = value
@@ -125,6 +143,47 @@ class BaseParser(object):
125
143
 
126
144
  return v
127
145
 
146
+ @classmethod
147
+ def _validate_uri(cls, value: typing.Any, **kwargs: typing.Any) -> str:
148
+ """校验输入的资源,转换为本地绝对路径
149
+
150
+ Args:
151
+ value: 输入值
152
+ workspace: 限制资源路径,可传递空字符串忽略校验. Defaults to None.
153
+ allow_domains: 限制资源地址的域名,可传递空数组忽略校验. Defaults to None.
154
+
155
+ Raises:
156
+ ParamValidateException: 参数校验异常
157
+
158
+ Returns:
159
+ 系统文件绝对路径
160
+ """
161
+ # 首先是字符串
162
+ value = cls._validate_str(value, **kwargs)
163
+ ori_value = value
164
+ # 判断是否是链接
165
+ parsed_url = urllib.parse.urlparse(value)
166
+ if parsed_url.scheme not in _ALLOW_SCHEMES:
167
+ value = os.path.realpath(os.fspath(value))
168
+ if not os.path.isfile(value):
169
+ raise ParamValidateException(f"系统文件不存在: {ori_value}")
170
+
171
+ workspaces: tuple = settings.PROCESSOR_WORKSPACES or ()
172
+ _workspace = [os.path.realpath(os.fspath(ws)) for ws in workspaces]
173
+ if _workspace and not value.startswith(tuple(_workspace)):
174
+ raise ParamValidateException(f"文件必须在 PROCESSOR_WORKSPACES={workspaces} 目录下: {ori_value}")
175
+ else:
176
+ # 是链接地址
177
+ domain = parsed_url.netloc
178
+ if not domain:
179
+ raise ParamValidateException(f"链接未解析出域名: {ori_value}")
180
+ allow_domains = settings.PROCESSOR_ALLOW_DOMAINS
181
+ if allow_domains and not parsed_url.netloc.endswith(tuple(allow_domains)):
182
+ raise ParamValidateException(
183
+ f"域名不合法, {parsed_url.netloc} 不在 {allow_domains} 范围内: {ori_value}"
184
+ )
185
+ return value
186
+
128
187
  @classmethod
129
188
  def parse_str(cls, param_str: str) -> dict:
130
189
  """将字符串参数转化为json格式数据
@@ -186,23 +245,23 @@ def compute_by_geography(
186
245
  src_w: int, src_h: int, x: int, y: int, w: int, h: int, g: typing.Optional[str], pf: str
187
246
  ) -> tuple[int, int]:
188
247
  """计算 大小(w,h)的图像相对于(src_w, src_h)图像的原点(x,y)位置"""
189
- if g == enums.Geography.NW:
248
+ if g == enums.Geography.NW.value:
190
249
  x, y = 0, 0
191
- elif g == enums.Geography.NORTH:
250
+ elif g == enums.Geography.NORTH.value:
192
251
  x, y = int(src_w / 2 - w / 2), 0
193
- elif g == enums.Geography.NE:
252
+ elif g == enums.Geography.NE.value:
194
253
  x, y = src_w - w, 0
195
- elif g == enums.Geography.WEST:
254
+ elif g == enums.Geography.WEST.value:
196
255
  x, y = 0, int(src_h / 2 - h / 2)
197
- elif g == enums.Geography.CENTER:
256
+ elif g == enums.Geography.CENTER.value:
198
257
  x, y = int(src_w / 2 - w / 2), int(src_h / 2 - h / 2)
199
- elif g == enums.Geography.EAST:
258
+ elif g == enums.Geography.EAST.value:
200
259
  x, y = src_w - w, int(src_h / 2 - h / 2)
201
- elif g == enums.Geography.SW:
260
+ elif g == enums.Geography.SW.value:
202
261
  x, y = 0, src_h - h
203
- elif g == enums.Geography.SOUTH:
262
+ elif g == enums.Geography.SOUTH.value:
204
263
  x, y = int(src_w / 2 - w / 2), src_h - h
205
- elif g == enums.Geography.SE:
264
+ elif g == enums.Geography.SE.value:
206
265
  x, y = src_w - w, src_h - h
207
266
  elif pf:
208
267
  if "x" in pf:
@@ -247,8 +306,8 @@ def compute_splice_two_im(
247
306
  h1: int,
248
307
  w2: int,
249
308
  h2: int,
250
- align: int = enums.PositionAlign.VERTIAL_CENTER, # type: ignore
251
- order: int = enums.PositionOrder.BEFORE, # type: ignore
309
+ align: int = enums.PositionAlign.VERTIAL_CENTER.value,
310
+ order: int = enums.PositionOrder.BEFORE.value,
252
311
  interval: int = 0,
253
312
  ) -> tuple:
254
313
  """拼接2个图像,计算整体大小和元素原点位置;数值单位都是像素
@@ -270,33 +329,37 @@ def compute_splice_two_im(
270
329
  第2个元素的原点位置x2
271
330
  第2个元素的原点位置y2
272
331
  """
273
- if align in [enums.PositionAlign.TOP, enums.PositionAlign.HORIZONTAL_CENTER, enums.PositionAlign.BOTTOM]:
332
+ if align in [
333
+ enums.PositionAlign.TOP.value,
334
+ enums.PositionAlign.HORIZONTAL_CENTER.value,
335
+ enums.PositionAlign.BOTTOM.value,
336
+ ]:
274
337
  # 水平顺序
275
338
  # 计算整体占位大小w,h
276
339
  w, h = w1 + w2 + interval, max(h1, h2)
277
340
 
278
- if align == enums.PositionAlign.TOP:
341
+ if align == enums.PositionAlign.TOP.value:
279
342
  y1, y2 = 0, 0
280
- elif align == enums.PositionAlign.BOTTOM:
343
+ elif align == enums.PositionAlign.BOTTOM.value:
281
344
  y1, y2 = h - h1, h - h2
282
345
  else:
283
346
  y1, y2 = int((h - h1) / 2), int((h - h2) / 2)
284
347
 
285
- if order == enums.PositionOrder.BEFORE:
348
+ if order == enums.PositionOrder.BEFORE.value:
286
349
  x1, x2 = 0, w1 + interval
287
350
  else:
288
351
  x1, x2 = w2 + interval, 0
289
352
  else:
290
353
  # 垂直
291
354
  w, h = max(w1, w2), h1 + h2 + interval
292
- if align == enums.PositionAlign.LEFT:
355
+ if align == enums.PositionAlign.LEFT.value:
293
356
  x1, x2 = 0, 0
294
- elif align == enums.PositionAlign.RIGHT:
357
+ elif align == enums.PositionAlign.RIGHT.value:
295
358
  x1, x2 = w - w1, w - w2
296
359
  else:
297
360
  x1, x2 = int((w - w1) / 2), int((w - w2) / 2)
298
361
 
299
- if order == enums.PositionOrder.BEFORE:
362
+ if order == enums.PositionOrder.BEFORE.value:
300
363
  y1, y2 = 0, h1 + interval
301
364
  else:
302
365
  y1, y2 = h2 + interval, 0
@@ -304,13 +367,74 @@ def compute_splice_two_im(
304
367
  return w, h, x1, y1, x2, y2
305
368
 
306
369
 
370
+ def validate_ori_im(ori_im: Image) -> None:
371
+ src_w, src_h = ori_im.size
372
+ if src_w > settings.PROCESSOR_MAX_W_H or src_h > settings.PROCESSOR_MAX_W_H:
373
+ raise ProcessLimitException(
374
+ f"图像宽和高单边像素不能超过{settings.PROCESSOR_MAX_W_H}像素,输入图像({src_w}, {src_h})"
375
+ )
376
+ if src_w * src_h > settings.PROCESSOR_MAX_PIXEL:
377
+ raise ProcessLimitException(f"图像总像素不可超过{settings.PROCESSOR_MAX_PIXEL}像素,输入图像({src_w}, {src_h})")
378
+
379
+
380
+ def copy_full_img(ori_im: Image) -> Image:
381
+ out_im = ori_im.copy()
382
+ # 复制格式信息
383
+ out_im.format = ori_im.format
384
+ # 复制info中的元数据(包括ICC配置文件等)
385
+ out_im.info = ori_im.info.copy()
386
+ return out_im
387
+
388
+
389
+ @contextmanager
390
+ def trans_uri_to_im(uri: str, use_copy: bool = False) -> typing.Generator:
391
+ """将输入资源转换成Image对象
392
+
393
+ Args:
394
+ uri: 文件路径 或者 可下载的链接地址
395
+ use_copy: 是否复制图像,使其不依赖打开的文件
396
+
397
+ Raises:
398
+ ProcessLimitException: 处理图像大小/像素限制
399
+
400
+ Returns:
401
+ Image对象
402
+ """
403
+ parsed_url = urllib.parse.urlparse(uri)
404
+ if parsed_url.scheme in _ALLOW_SCHEMES:
405
+ with tempfile.NamedTemporaryFile() as fp:
406
+ # 输入值计算md5作为文件名;重复地址本地若存在不下载多次
407
+ urlretrieve(uri, filename=fp.name)
408
+ fp.seek(0)
409
+
410
+ size = os.path.getsize(fp.name)
411
+ if size > settings.PROCESSOR_MAX_FILE_SIZE * 1024 * 1024:
412
+ raise ProcessLimitException(f"图像文件大小不得超过{settings.PROCESSOR_MAX_FILE_SIZE}MB")
413
+
414
+ with Image.open(fp) as uri_im:
415
+ validate_ori_im(uri_im)
416
+ # 解决临时文件close后im对象不能正常使用得问题
417
+ ori_im = copy_full_img(uri_im)
418
+ yield ori_im
419
+ else:
420
+ size = os.path.getsize(uri)
421
+ if size > settings.PROCESSOR_MAX_FILE_SIZE * 1024 * 1024:
422
+ raise ProcessLimitException(f"图像文件大小不得超过{settings.PROCESSOR_MAX_FILE_SIZE}MB")
423
+ with Image.open(uri) as uri_im:
424
+ validate_ori_im(uri_im)
425
+ ori_im = uri_im
426
+ if use_copy:
427
+ ori_im = copy_full_img(ori_im)
428
+ yield ori_im
429
+
430
+
307
431
  class ImgSaveParser(BaseParser):
308
432
  KEY = ""
309
433
  ARGS = {
310
- "format": {"type": enums.ArgType.STRING, "default": None},
311
- "quality": {"type": enums.ArgType.INTEGER, "default": None, "min": 1, "max": 100},
434
+ "format": {"type": enums.ArgType.STRING.value, "default": None},
435
+ "quality": {"type": enums.ArgType.INTEGER.value, "default": None, "min": 1, "max": 100},
312
436
  # 1 表示将原图设置成渐进显示
313
- "interlace": {"type": enums.ArgType.INTEGER, "default": 0, "choices": [0, 1]},
437
+ "interlace": {"type": enums.ArgType.INTEGER.value, "default": 0, "choices": [0, 1]},
314
438
  }
315
439
 
316
440
  def __init__(
@@ -10,10 +10,10 @@ from .base import BaseParser, pre_processing
10
10
 
11
11
  class BlurParser(BaseParser):
12
12
 
13
- KEY = enums.OpAction.BLUR
13
+ KEY = enums.OpAction.BLUR.value
14
14
  ARGS = {
15
15
  # 模糊半径,值越大,图片越模糊
16
- "r": {"type": enums.ArgType.INTEGER, "required": True, "min": 1, "max": 50},
16
+ "r": {"type": enums.ArgType.INTEGER.value, "required": True, "min": 1, "max": 50},
17
17
  }
18
18
 
19
19
  def __init__(
@@ -10,9 +10,9 @@ from .base import BaseParser, pre_processing
10
10
 
11
11
  class CircleParser(BaseParser):
12
12
 
13
- KEY = enums.OpAction.CIRCLE
13
+ KEY = enums.OpAction.CIRCLE.value
14
14
  ARGS = {
15
- "r": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
15
+ "r": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
16
16
  }
17
17
 
18
18
  def __init__(
@@ -9,20 +9,20 @@ from .base import BaseParser, pre_processing, compute_by_geography, compute_by_r
9
9
 
10
10
  class CropParser(BaseParser):
11
11
 
12
- KEY = enums.OpAction.CROP
12
+ KEY = enums.OpAction.CROP.value
13
13
  ARGS = {
14
- "w": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
15
- "h": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
16
- "ratio": {"type": enums.ArgType.STRING, "regex": r"^\d+:\d+$"},
17
- "x": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
18
- "y": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
19
- "g": {"type": enums.ArgType.STRING, "choices": enums.Geography},
14
+ "w": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
15
+ "h": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
16
+ "ratio": {"type": enums.ArgType.STRING.value, "regex": r"^\d+:\d+$"},
17
+ "x": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
18
+ "y": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
19
+ "g": {"type": enums.ArgType.STRING.value, "choices": enums.Geography},
20
20
  # percent field, eg: xywh
21
- "pf": {"type": enums.ArgType.STRING, "default": ""},
21
+ "pf": {"type": enums.ArgType.STRING.value, "default": ""},
22
22
  # padding right
23
- "padr": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
23
+ "padr": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
24
24
  # padding bottom
25
- "padb": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
25
+ "padb": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
26
26
  # 左和上通过x,y控制
27
27
  }
28
28
 
@@ -10,7 +10,7 @@ from .base import BaseParser
10
10
 
11
11
  class GrayParser(BaseParser):
12
12
 
13
- KEY = enums.OpAction.GRAY
13
+ KEY = enums.OpAction.GRAY.value
14
14
  ARGS = {}
15
15
 
16
16
  def __init__(