py-img-processor 1.1.0__py3-none-any.whl → 1.3.1__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.
- imgprocessor/__init__.py +7 -1
- imgprocessor/enums.py +2 -4
- imgprocessor/main.py +2 -2
- imgprocessor/parsers/__init__.py +23 -13
- imgprocessor/parsers/alpha.py +2 -2
- imgprocessor/parsers/base.py +158 -29
- imgprocessor/parsers/blur.py +2 -2
- imgprocessor/parsers/circle.py +2 -2
- imgprocessor/parsers/crop.py +10 -10
- imgprocessor/parsers/gray.py +1 -1
- imgprocessor/parsers/merge.py +64 -27
- imgprocessor/parsers/resize.py +20 -16
- imgprocessor/parsers/rotate.py +2 -2
- imgprocessor/parsers/watermark.py +42 -23
- imgprocessor/processor.py +97 -82
- {py_img_processor-1.1.0.dist-info → py_img_processor-1.3.1.dist-info}/METADATA +19 -10
- py_img_processor-1.3.1.dist-info/RECORD +23 -0
- py_img_processor-1.1.0.dist-info/RECORD +0 -23
- {py_img_processor-1.1.0.dist-info → py_img_processor-1.3.1.dist-info}/LICENSE +0 -0
- {py_img_processor-1.1.0.dist-info → py_img_processor-1.3.1.dist-info}/WHEEL +0 -0
- {py_img_processor-1.1.0.dist-info → py_img_processor-1.3.1.dist-info}/entry_points.txt +0 -0
- {py_img_processor-1.1.0.dist-info → py_img_processor-1.3.1.dist-info}/top_level.txt +0 -0
imgprocessor/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ import importlib
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
__all__ = ["settings", "VERSION"]
|
|
8
|
-
__version__ = "1.1
|
|
8
|
+
__version__ = "1.3.1"
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
VERSION = __version__
|
|
@@ -41,6 +41,12 @@ 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 = ()
|
|
48
|
+
# 临时文件目录
|
|
49
|
+
PROCESSOR_TEMP_DIR = None
|
|
44
50
|
|
|
45
51
|
def __getattribute__(self, attr: str) -> typing.Any:
|
|
46
52
|
try:
|
imgprocessor/enums.py
CHANGED
|
@@ -31,10 +31,6 @@ class ImageOrientation(ChoiceEnum):
|
|
|
31
31
|
class OpAction(ChoiceEnum):
|
|
32
32
|
"""支持的操作类型"""
|
|
33
33
|
|
|
34
|
-
# 以下几个比较特殊,在保存文件时使用
|
|
35
|
-
FORMAT = ("format", "格式")
|
|
36
|
-
QUALITY = ("quality", "质量")
|
|
37
|
-
INTERLACE = ("interlace", "渐进显示")
|
|
38
34
|
# 其他
|
|
39
35
|
RESIZE = ("resize", "缩放")
|
|
40
36
|
CROP = ("crop", "裁剪")
|
|
@@ -62,6 +58,8 @@ class ArgType(ChoiceEnum):
|
|
|
62
58
|
STRING = ("str", "字符串")
|
|
63
59
|
INTEGER = ("int", "整数")
|
|
64
60
|
FLOAT = ("float", "浮点数")
|
|
61
|
+
URI = ("uri", "资源路径/链接")
|
|
62
|
+
ACTION = ("action", "对图像的操作")
|
|
65
63
|
|
|
66
64
|
|
|
67
65
|
class Geography(ChoiceEnum):
|
imgprocessor/main.py
CHANGED
|
@@ -7,7 +7,7 @@ import argparse
|
|
|
7
7
|
import traceback
|
|
8
8
|
|
|
9
9
|
from imgprocessor import VERSION
|
|
10
|
-
from imgprocessor.processor import ProcessParams,
|
|
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
|
-
|
|
98
|
+
process_image(file_path, param_str, out_path=out_path)
|
|
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)
|
imgprocessor/parsers/__init__.py
CHANGED
|
@@ -4,7 +4,8 @@ import typing
|
|
|
4
4
|
|
|
5
5
|
from imgprocessor import enums
|
|
6
6
|
from imgprocessor.exceptions import ParamParseException
|
|
7
|
-
|
|
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,22 +17,30 @@ from .watermark import WatermarkParser
|
|
|
16
17
|
from .merge import MergeParser
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
_ACTION_PARASER_MAP: dict[str,
|
|
20
|
-
enums.OpAction.RESIZE: ResizeParser,
|
|
21
|
-
enums.OpAction.CROP: CropParser,
|
|
22
|
-
enums.OpAction.CIRCLE: CircleParser,
|
|
23
|
-
enums.OpAction.BLUR: BlurParser,
|
|
24
|
-
enums.OpAction.ROTATE: RotateParser,
|
|
25
|
-
enums.OpAction.ALPHA: AlphaParser,
|
|
26
|
-
enums.OpAction.GRAY: GrayParser,
|
|
27
|
-
enums.OpAction.WATERMARK: WatermarkParser,
|
|
28
|
-
enums.OpAction.MERGE: MergeParser,
|
|
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
|
|
|
32
33
|
class ProcessParams(object):
|
|
33
34
|
"""图像处理输入参数"""
|
|
34
35
|
|
|
36
|
+
@classmethod
|
|
37
|
+
def init(cls, params: typing.Union["ProcessParams", dict, str]) -> "ProcessParams":
|
|
38
|
+
if isinstance(params, ProcessParams):
|
|
39
|
+
return params
|
|
40
|
+
if isinstance(params, dict):
|
|
41
|
+
return cls(**params)
|
|
42
|
+
return cls.parse_str(params)
|
|
43
|
+
|
|
35
44
|
def __init__(
|
|
36
45
|
self,
|
|
37
46
|
enable_base64: bool = False,
|
|
@@ -40,7 +49,7 @@ class ProcessParams(object):
|
|
|
40
49
|
) -> None:
|
|
41
50
|
self.save_parser: ImgSaveParser = ImgSaveParser.init(kwargs, enable_base64=enable_base64) # type: ignore
|
|
42
51
|
|
|
43
|
-
_actions = []
|
|
52
|
+
_actions: list[BaseParser] = []
|
|
44
53
|
for i in actions or []:
|
|
45
54
|
key = i.get("key")
|
|
46
55
|
cls = _ACTION_PARASER_MAP.get(key)
|
|
@@ -65,6 +74,7 @@ class ProcessParams(object):
|
|
|
65
74
|
|
|
66
75
|
save_args = [""] # 加空字符串,是为了保证解析出key
|
|
67
76
|
|
|
77
|
+
save_keys = list(ImgSaveParser.ARGS.keys())
|
|
68
78
|
for item in value.split("/"):
|
|
69
79
|
if not item:
|
|
70
80
|
continue
|
|
@@ -76,7 +86,7 @@ class ProcessParams(object):
|
|
|
76
86
|
key, param_str = info
|
|
77
87
|
if not key:
|
|
78
88
|
raise ParamParseException(f"参数必须指定操作类型 [{item}]不符合参数要求")
|
|
79
|
-
if key in
|
|
89
|
+
if key in save_keys:
|
|
80
90
|
save_args.append(f"{key}_{param_str}")
|
|
81
91
|
else:
|
|
82
92
|
action_cls = _ACTION_PARASER_MAP.get(key)
|
imgprocessor/parsers/alpha.py
CHANGED
|
@@ -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__(
|
imgprocessor/parsers/base.py
CHANGED
|
@@ -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
|
|
8
|
-
from imgprocessor
|
|
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
|
|
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:
|
|
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:
|
|
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,
|
|
251
|
-
order: int = enums.PositionOrder.BEFORE,
|
|
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 [
|
|
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,79 @@ 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
|
+
# 可能包含 %20 (空格) 等编码,需要 unquote
|
|
406
|
+
filename = os.path.basename(urllib.parse.unquote(parsed_url.path))
|
|
407
|
+
_, suffix = os.path.splitext(filename)
|
|
408
|
+
with tempfile.NamedTemporaryFile(suffix=suffix, dir=settings.PROCESSOR_TEMP_DIR) as fp:
|
|
409
|
+
# 输入值计算md5作为文件名;重复地址本地若存在不下载多次
|
|
410
|
+
urlretrieve(uri, filename=fp.name)
|
|
411
|
+
fp.seek(0)
|
|
412
|
+
|
|
413
|
+
size = os.path.getsize(fp.name)
|
|
414
|
+
if size > settings.PROCESSOR_MAX_FILE_SIZE * 1024 * 1024:
|
|
415
|
+
raise ProcessLimitException(f"图像文件大小不得超过{settings.PROCESSOR_MAX_FILE_SIZE}MB")
|
|
416
|
+
|
|
417
|
+
with Image.open(fp) as uri_im:
|
|
418
|
+
validate_ori_im(uri_im)
|
|
419
|
+
# 解决临时文件close后im对象不能正常使用得问题
|
|
420
|
+
ori_im = copy_full_img(uri_im)
|
|
421
|
+
yield ori_im
|
|
422
|
+
else:
|
|
423
|
+
size = os.path.getsize(uri)
|
|
424
|
+
if size > settings.PROCESSOR_MAX_FILE_SIZE * 1024 * 1024:
|
|
425
|
+
raise ProcessLimitException(f"图像文件大小不得超过{settings.PROCESSOR_MAX_FILE_SIZE}MB")
|
|
426
|
+
with Image.open(uri) as uri_im:
|
|
427
|
+
validate_ori_im(uri_im)
|
|
428
|
+
ori_im = uri_im
|
|
429
|
+
if use_copy:
|
|
430
|
+
ori_im = copy_full_img(ori_im)
|
|
431
|
+
yield ori_im
|
|
432
|
+
|
|
433
|
+
|
|
307
434
|
class ImgSaveParser(BaseParser):
|
|
308
435
|
KEY = ""
|
|
436
|
+
|
|
437
|
+
# 定义的key注意和枚举 `OpAction` 中的key不能重复
|
|
309
438
|
ARGS = {
|
|
310
|
-
"format": {"type": enums.ArgType.STRING, "default": None},
|
|
311
|
-
"quality": {"type": enums.ArgType.INTEGER, "default": None, "min": 1, "max": 100},
|
|
439
|
+
"format": {"type": enums.ArgType.STRING.value, "default": None},
|
|
440
|
+
"quality": {"type": enums.ArgType.INTEGER.value, "default": None, "min": 1, "max": 100},
|
|
312
441
|
# 1 表示将原图设置成渐进显示
|
|
313
|
-
"interlace": {"type": enums.ArgType.INTEGER, "default": 0, "choices": [0, 1]},
|
|
442
|
+
"interlace": {"type": enums.ArgType.INTEGER.value, "default": 0, "choices": [0, 1]},
|
|
314
443
|
}
|
|
315
444
|
|
|
316
445
|
def __init__(
|
imgprocessor/parsers/blur.py
CHANGED
|
@@ -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__(
|
imgprocessor/parsers/circle.py
CHANGED
|
@@ -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__(
|
imgprocessor/parsers/crop.py
CHANGED
|
@@ -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
|
|
imgprocessor/parsers/gray.py
CHANGED
imgprocessor/parsers/merge.py
CHANGED
|
@@ -5,65 +5,92 @@ import typing
|
|
|
5
5
|
from PIL import Image
|
|
6
6
|
|
|
7
7
|
from imgprocessor import enums, settings
|
|
8
|
-
from .
|
|
8
|
+
from imgprocessor.exceptions import ParamValidateException
|
|
9
|
+
from .base import (
|
|
10
|
+
BaseParser,
|
|
11
|
+
pre_processing,
|
|
12
|
+
compute_by_geography,
|
|
13
|
+
compute_splice_two_im,
|
|
14
|
+
trans_uri_to_im,
|
|
15
|
+
)
|
|
9
16
|
|
|
10
17
|
|
|
11
18
|
class MergeParser(BaseParser):
|
|
12
19
|
|
|
13
|
-
KEY = enums.OpAction.MERGE
|
|
20
|
+
KEY = enums.OpAction.MERGE.value
|
|
14
21
|
ARGS = {
|
|
15
22
|
# 要处理的图片
|
|
16
|
-
"image": {"type": enums.ArgType.
|
|
23
|
+
"image": {"type": enums.ArgType.URI.value, "required": True, "base64_encode": True},
|
|
17
24
|
# 对image的处理参数
|
|
18
|
-
"
|
|
19
|
-
#
|
|
20
|
-
"
|
|
25
|
+
"actions": {"type": enums.ArgType.ACTION.value, "base64_encode": True},
|
|
26
|
+
# 是否将imgae当做背景放在输入图像之下; 定义输入图像和image参数的拼接顺序
|
|
27
|
+
"bg": {"type": enums.ArgType.INTEGER.value, "default": 0, "choices": [0, 1]},
|
|
28
|
+
# 使用输入图像的大小作为参照进行缩放,bg=1按照image缩放输入图像
|
|
29
|
+
"p": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 1, "max": 1000},
|
|
21
30
|
# 对齐方式
|
|
22
|
-
"order": {"type": enums.ArgType.INTEGER, "choices": enums.PositionOrder},
|
|
23
|
-
"align": {
|
|
24
|
-
|
|
31
|
+
"order": {"type": enums.ArgType.INTEGER.value, "choices": enums.PositionOrder},
|
|
32
|
+
"align": {
|
|
33
|
+
"type": enums.ArgType.INTEGER.value,
|
|
34
|
+
"default": enums.PositionAlign.BOTTOM.value,
|
|
35
|
+
"choices": enums.PositionAlign,
|
|
36
|
+
},
|
|
37
|
+
"interval": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": 1000},
|
|
25
38
|
# 粘贴的位置
|
|
26
|
-
"g": {"type": enums.ArgType.STRING, "choices": enums.Geography},
|
|
27
|
-
"x": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
|
|
28
|
-
"y": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
|
|
29
|
-
"pf": {"type": enums.ArgType.STRING, "default": ""},
|
|
39
|
+
"g": {"type": enums.ArgType.STRING.value, "choices": enums.Geography},
|
|
40
|
+
"x": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
|
|
41
|
+
"y": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
|
|
42
|
+
"pf": {"type": enums.ArgType.STRING.value, "default": ""},
|
|
30
43
|
# 拼接后大小包含2个图像,空白区域使用color颜色填充
|
|
31
44
|
"color": {
|
|
32
|
-
"type": enums.ArgType.STRING,
|
|
33
|
-
"default": "
|
|
45
|
+
"type": enums.ArgType.STRING.value,
|
|
46
|
+
"default": "0000", # 为了保证透明背景
|
|
34
47
|
"regex": r"^([0-9a-fA-F]{6}|[0-9a-fA-F]{8}|[0-9a-fA-F]{3,4})$",
|
|
35
48
|
},
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
def __init__(
|
|
39
52
|
self,
|
|
40
|
-
image:
|
|
41
|
-
|
|
53
|
+
image: str = "",
|
|
54
|
+
actions: typing.Union[str, list] = "",
|
|
42
55
|
p: int = 0,
|
|
43
56
|
order: typing.Optional[int] = None,
|
|
44
57
|
align: int = 2,
|
|
45
58
|
interval: int = 0,
|
|
59
|
+
bg: int = 0,
|
|
46
60
|
g: typing.Optional[str] = None,
|
|
47
61
|
x: int = 0,
|
|
48
62
|
y: int = 0,
|
|
49
63
|
pf: str = "",
|
|
50
|
-
color: str = "
|
|
64
|
+
color: str = "0000",
|
|
51
65
|
**kwargs: typing.Any,
|
|
52
66
|
) -> None:
|
|
53
67
|
self.image = image
|
|
54
|
-
self.action = action
|
|
55
68
|
self.p = p
|
|
56
69
|
self.order = order
|
|
57
70
|
self.align = align
|
|
58
71
|
self.interval = interval
|
|
72
|
+
self.bg = bg
|
|
59
73
|
self.g = g
|
|
60
74
|
self.x = x
|
|
61
75
|
self.y = y
|
|
62
76
|
self.pf = pf
|
|
63
77
|
self.color = color
|
|
64
78
|
|
|
79
|
+
self.actions: list[BaseParser] = []
|
|
80
|
+
if actions:
|
|
81
|
+
from imgprocessor.processor import ProcessParams
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
if isinstance(actions, str):
|
|
85
|
+
params = ProcessParams.parse_str(actions)
|
|
86
|
+
else:
|
|
87
|
+
params = ProcessParams(actions=actions)
|
|
88
|
+
self.actions = params.actions
|
|
89
|
+
except ParamValidateException as e:
|
|
90
|
+
raise ParamValidateException(f"merage操作中actions参数校验异常,其中 {e}")
|
|
91
|
+
|
|
65
92
|
def compute(self, src_w: int, src_h: int, w2: int, h2: int) -> tuple:
|
|
66
|
-
if self.order in enums.PositionOrder:
|
|
93
|
+
if self.order in enums.PositionOrder:
|
|
67
94
|
order = typing.cast(int, self.order)
|
|
68
95
|
w, h, x1, y1, x2, y2 = compute_splice_two_im(
|
|
69
96
|
src_w,
|
|
@@ -90,20 +117,30 @@ class MergeParser(BaseParser):
|
|
|
90
117
|
|
|
91
118
|
def do_action(self, im: Image) -> Image:
|
|
92
119
|
im = pre_processing(im, use_alpha=True)
|
|
93
|
-
src_w, src_h = im.size
|
|
94
120
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
121
|
+
# 处理要合并的图像
|
|
122
|
+
with trans_uri_to_im(self.image, use_copy=True) as _im2:
|
|
123
|
+
im2 = pre_processing(_im2, use_alpha=True)
|
|
124
|
+
|
|
125
|
+
if self.actions:
|
|
126
|
+
from imgprocessor.processor import ProcessorCtr
|
|
99
127
|
|
|
100
|
-
|
|
101
|
-
|
|
128
|
+
im2 = ProcessorCtr.handle_img_actions(im2, self.actions)
|
|
129
|
+
|
|
130
|
+
# 调整拼接顺序
|
|
131
|
+
if self.bg:
|
|
132
|
+
im, im2 = im2, im
|
|
133
|
+
|
|
134
|
+
# 缩放图像
|
|
102
135
|
if self.p:
|
|
136
|
+
src_w, src_h = im.size
|
|
103
137
|
w2, h2 = round(src_w * self.p / 100), round(src_h * self.p / 100)
|
|
104
138
|
im2 = im2.resize((w2, h2), resample=Image.LANCZOS)
|
|
139
|
+
|
|
140
|
+
src_w, src_h = im.size
|
|
105
141
|
w2, h2 = im2.size
|
|
106
142
|
|
|
143
|
+
# 计算合并像素点
|
|
107
144
|
w, h, x1, y1, x2, y2 = self.compute(src_w, src_h, w2, h2)
|
|
108
145
|
out = Image.new("RGBA", (w, h), color=f"#{self.color}")
|
|
109
146
|
out.paste(im, (x1, y1), im)
|
imgprocessor/parsers/resize.py
CHANGED
|
@@ -10,25 +10,29 @@ from .base import BaseParser, pre_processing
|
|
|
10
10
|
|
|
11
11
|
class ResizeParser(BaseParser):
|
|
12
12
|
|
|
13
|
-
KEY = enums.OpAction.RESIZE
|
|
13
|
+
KEY = enums.OpAction.RESIZE.value
|
|
14
14
|
ARGS = {
|
|
15
|
-
"m": {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
15
|
+
"m": {
|
|
16
|
+
"type": enums.ArgType.STRING.value,
|
|
17
|
+
"default": enums.ResizeMode.LFIT.value,
|
|
18
|
+
"choices": enums.ResizeMode,
|
|
19
|
+
},
|
|
20
|
+
"w": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
|
|
21
|
+
"h": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
|
|
22
|
+
"l": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
|
|
23
|
+
"s": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
|
|
24
|
+
"limit": {"type": enums.ArgType.INTEGER.value, "default": 1, "choices": [0, 1]},
|
|
21
25
|
"color": {
|
|
22
|
-
"type": enums.ArgType.STRING,
|
|
23
|
-
"default": "FFFFFF",
|
|
26
|
+
"type": enums.ArgType.STRING.value,
|
|
27
|
+
"default": "FFFFFF", # 默认白色
|
|
24
28
|
"regex": r"^([0-9a-fA-F]{6}|[0-9a-fA-F]{8}|[0-9a-fA-F]{3,4})$",
|
|
25
29
|
},
|
|
26
|
-
"p": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": 1000},
|
|
30
|
+
"p": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 1, "max": 1000},
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
def __init__(
|
|
30
34
|
self,
|
|
31
|
-
m: str = enums.ResizeMode.LFIT,
|
|
35
|
+
m: str = enums.ResizeMode.LFIT.value,
|
|
32
36
|
w: int = 0,
|
|
33
37
|
h: int = 0,
|
|
34
38
|
l: int = 0, # noqa: E741
|
|
@@ -50,13 +54,13 @@ class ResizeParser(BaseParser):
|
|
|
50
54
|
def compute(self, src_w: int, src_h: int) -> tuple:
|
|
51
55
|
"""计算出`Image.resize`需要的参数"""
|
|
52
56
|
if self.w or self.h:
|
|
53
|
-
if self.m in [enums.ResizeMode.FIXED, enums.ResizeMode.PAD, enums.ResizeMode.FIT]:
|
|
57
|
+
if self.m in [enums.ResizeMode.FIXED.value, enums.ResizeMode.PAD.value, enums.ResizeMode.FIT.value]:
|
|
54
58
|
# 有可能改变原图宽高比
|
|
55
59
|
if not (self.w and self.h):
|
|
56
60
|
raise ParamValidateException(f"当m={self.m}的模式下,参数w和h都必不可少且不能为0")
|
|
57
61
|
# w,h按指定的即可,无需计算
|
|
58
62
|
w, h = self.w, self.h
|
|
59
|
-
elif self.m == enums.ResizeMode.MFIT:
|
|
63
|
+
elif self.m == enums.ResizeMode.MFIT.value:
|
|
60
64
|
# 低版本Pillow未实现 ImageOps.cover 方法,自行处理
|
|
61
65
|
# 等比缩放
|
|
62
66
|
if self.w and self.h:
|
|
@@ -70,7 +74,7 @@ class ResizeParser(BaseParser):
|
|
|
70
74
|
else:
|
|
71
75
|
w, h = round(self.h * src_w / src_h), self.h
|
|
72
76
|
else:
|
|
73
|
-
# 默认 enums.ResizeMode.LFIT
|
|
77
|
+
# 默认 enums.ResizeMode.LFIT.value
|
|
74
78
|
# 等比缩放
|
|
75
79
|
if self.w and self.h:
|
|
76
80
|
# 指定w与h的矩形内的最大图像
|
|
@@ -114,9 +118,9 @@ class ResizeParser(BaseParser):
|
|
|
114
118
|
if size == im.size:
|
|
115
119
|
# 大小没有变化直接返回
|
|
116
120
|
return im
|
|
117
|
-
if self.m == enums.ResizeMode.PAD:
|
|
121
|
+
if self.m == enums.ResizeMode.PAD.value:
|
|
118
122
|
out = ImageOps.pad(im, size, color=f"#{self.color}")
|
|
119
|
-
elif self.m == enums.ResizeMode.FIT:
|
|
123
|
+
elif self.m == enums.ResizeMode.FIT.value:
|
|
120
124
|
out = ImageOps.fit(im, size)
|
|
121
125
|
else:
|
|
122
126
|
out = im.resize(size, resample=Image.LANCZOS)
|
imgprocessor/parsers/rotate.py
CHANGED
|
@@ -10,10 +10,10 @@ from .base import BaseParser, pre_processing
|
|
|
10
10
|
|
|
11
11
|
class RotateParser(BaseParser):
|
|
12
12
|
|
|
13
|
-
KEY = enums.OpAction.ROTATE
|
|
13
|
+
KEY = enums.OpAction.ROTATE.value
|
|
14
14
|
ARGS = {
|
|
15
15
|
# 顺时针旋转的度数
|
|
16
|
-
"value": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 360},
|
|
16
|
+
"value": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": 360},
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
def __init__(
|
|
@@ -6,44 +6,63 @@ from PIL import Image, ImageFont, ImageDraw
|
|
|
6
6
|
|
|
7
7
|
from imgprocessor import enums, settings, utils
|
|
8
8
|
from imgprocessor.exceptions import ParamValidateException
|
|
9
|
-
from .base import
|
|
9
|
+
from .base import (
|
|
10
|
+
BaseParser,
|
|
11
|
+
pre_processing,
|
|
12
|
+
compute_splice_two_im,
|
|
13
|
+
compute_by_geography,
|
|
14
|
+
trans_uri_to_im,
|
|
15
|
+
)
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
class WatermarkParser(BaseParser):
|
|
13
19
|
|
|
14
|
-
KEY = enums.OpAction.WATERMARK
|
|
20
|
+
KEY = enums.OpAction.WATERMARK.value
|
|
15
21
|
ARGS = {
|
|
16
22
|
# 水印本身的不透明度,100表示完全不透明
|
|
17
|
-
"t": {"type": enums.ArgType.INTEGER, "default": 100, "min": 0, "max": 100},
|
|
18
|
-
"g": {"type": enums.ArgType.STRING, "choices": enums.Geography},
|
|
19
|
-
"x": {"type": enums.ArgType.INTEGER, "default": 10, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
|
|
20
|
-
"y": {"type": enums.ArgType.INTEGER, "default": 10, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
|
|
23
|
+
"t": {"type": enums.ArgType.INTEGER.value, "default": 100, "min": 0, "max": 100},
|
|
24
|
+
"g": {"type": enums.ArgType.STRING.value, "choices": enums.Geography},
|
|
25
|
+
"x": {"type": enums.ArgType.INTEGER.value, "default": 10, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
|
|
26
|
+
"y": {"type": enums.ArgType.INTEGER.value, "default": 10, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
|
|
21
27
|
# percent field, eg: xy
|
|
22
|
-
"pf": {"type": enums.ArgType.STRING, "default": ""},
|
|
28
|
+
"pf": {"type": enums.ArgType.STRING.value, "default": ""},
|
|
23
29
|
# 是否将图片水印或文字水印铺满原图; 值为1开启
|
|
24
|
-
"fill": {"type": enums.ArgType.INTEGER, "default": 0, "choices": [0, 1]},
|
|
25
|
-
"padx": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 4096},
|
|
26
|
-
"pady": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 4096},
|
|
30
|
+
"fill": {"type": enums.ArgType.INTEGER.value, "default": 0, "choices": [0, 1]},
|
|
31
|
+
"padx": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": 4096},
|
|
32
|
+
"pady": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": 4096},
|
|
27
33
|
# 图片水印路径
|
|
28
|
-
"image": {"type": enums.ArgType.
|
|
34
|
+
"image": {"type": enums.ArgType.URI.value, "base64_encode": True},
|
|
29
35
|
# 水印的原始设计参照尺寸,会根据原图大小缩放水印
|
|
30
|
-
"design": {"type": enums.ArgType.INTEGER, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
|
|
36
|
+
"design": {"type": enums.ArgType.INTEGER.value, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
|
|
31
37
|
# 文字
|
|
32
|
-
"text": {"type": enums.ArgType.STRING, "base64_encode": True, "max_length": 64},
|
|
33
|
-
"font": {"type": enums.ArgType.STRING, "base64_encode": True},
|
|
34
|
-
|
|
35
|
-
"
|
|
38
|
+
"text": {"type": enums.ArgType.STRING.value, "base64_encode": True, "max_length": 64},
|
|
39
|
+
"font": {"type": enums.ArgType.STRING.value, "base64_encode": True},
|
|
40
|
+
# 文字默认黑色
|
|
41
|
+
"color": {
|
|
42
|
+
"type": enums.ArgType.STRING.value,
|
|
43
|
+
"default": "000000",
|
|
44
|
+
"regex": "^([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$",
|
|
45
|
+
},
|
|
46
|
+
"size": {"type": enums.ArgType.INTEGER.value, "default": 40, "min": 1, "max": 1000},
|
|
36
47
|
# 文字水印的阴影透明度, 0表示没有阴影
|
|
37
|
-
"shadow": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 100},
|
|
48
|
+
"shadow": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": 100},
|
|
38
49
|
# 顺时针旋转角度
|
|
39
|
-
"rotate": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 360},
|
|
50
|
+
"rotate": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": 360},
|
|
40
51
|
# 图文混合水印参数
|
|
41
52
|
# 文字和图片水印的前后顺序; 0表示图片水印在前;1表示文字水印在前
|
|
42
|
-
"order": {
|
|
53
|
+
"order": {
|
|
54
|
+
"type": enums.ArgType.INTEGER.value,
|
|
55
|
+
"default": enums.PositionOrder.BEFORE.value,
|
|
56
|
+
"choices": enums.PositionOrder,
|
|
57
|
+
},
|
|
43
58
|
# 文字水印和图片水印的对齐方式; 0表示文字水印和图片水印上对齐; 1表示文字水印和图片水印中对齐; 2: 表示文字水印和图片水印下对齐
|
|
44
|
-
"align": {
|
|
59
|
+
"align": {
|
|
60
|
+
"type": enums.ArgType.INTEGER.value,
|
|
61
|
+
"default": enums.PositionAlign.BOTTOM.value,
|
|
62
|
+
"choices": enums.PositionAlign,
|
|
63
|
+
},
|
|
45
64
|
# 文字水印和图片水印间的间距
|
|
46
|
-
"interval": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 1000},
|
|
65
|
+
"interval": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 0, "max": 1000},
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
def __init__(
|
|
@@ -99,8 +118,8 @@ class WatermarkParser(BaseParser):
|
|
|
99
118
|
w1, h1, w2, h2 = 0, 0, 0, 0
|
|
100
119
|
icon = None
|
|
101
120
|
if self.image:
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
with trans_uri_to_im(self.image, use_copy=True) as _icon:
|
|
122
|
+
icon = pre_processing(_icon, use_alpha=True)
|
|
104
123
|
if not self.text:
|
|
105
124
|
# 没有文字,直接返回
|
|
106
125
|
return icon
|
imgprocessor/processor.py
CHANGED
|
@@ -1,75 +1,71 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# coding=utf-8
|
|
3
3
|
import typing
|
|
4
|
-
import os
|
|
5
4
|
import tempfile
|
|
6
5
|
import colorsys
|
|
7
6
|
|
|
8
7
|
from PIL import Image, ImageOps
|
|
9
8
|
|
|
10
|
-
from imgprocessor import
|
|
11
|
-
from imgprocessor.exceptions import ProcessLimitException
|
|
9
|
+
from imgprocessor import enums, settings
|
|
12
10
|
from imgprocessor.parsers import BaseParser, ProcessParams
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
11
|
+
from imgprocessor.parsers.base import trans_uri_to_im
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProcessorCtr(object):
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def handle_img_actions(cls, ori_im: Image, actions: list[BaseParser]) -> Image:
|
|
18
|
+
im = ori_im
|
|
19
|
+
# 解决旋转问题
|
|
20
|
+
im = ImageOps.exif_transpose(im)
|
|
21
|
+
for parser in actions:
|
|
22
|
+
im = parser.do_action(im)
|
|
23
|
+
return im
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def save_img_to_file(
|
|
27
|
+
cls,
|
|
28
|
+
im: Image,
|
|
29
|
+
out_path: typing.Optional[str] = None,
|
|
30
|
+
**kwargs: typing.Any,
|
|
31
|
+
) -> typing.Optional[typing.ByteString]:
|
|
32
|
+
fmt = kwargs.get("format") or im.format
|
|
33
|
+
|
|
34
|
+
if fmt and fmt.upper() == enums.ImageFormat.JPEG.value and im.mode == "RGBA":
|
|
35
|
+
im = im.convert("RGB")
|
|
36
|
+
|
|
37
|
+
if not kwargs.get("quality"):
|
|
38
|
+
if fmt and fmt.upper() == enums.ImageFormat.JPEG.value and im.format == enums.ImageFormat.JPEG.value:
|
|
39
|
+
kwargs["quality"] = "keep"
|
|
40
|
+
else:
|
|
41
|
+
kwargs["quality"] = settings.PROCESSOR_DEFAULT_QUALITY
|
|
42
|
+
|
|
43
|
+
if out_path:
|
|
44
|
+
# icc_profile 是为解决色域的问题
|
|
45
|
+
im.save(out_path, **kwargs)
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
# 没有传递保存的路径,返回文件内容
|
|
49
|
+
suffix = fmt or "png"
|
|
50
|
+
with tempfile.NamedTemporaryFile(suffix=f".{suffix}", dir=settings.PROCESSOR_TEMP_DIR) as fp:
|
|
51
|
+
im.save(fp.name, **kwargs)
|
|
52
|
+
fp.seek(0)
|
|
53
|
+
content = fp.read()
|
|
54
|
+
return content
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def process_image(
|
|
58
|
+
input_uri: str,
|
|
59
|
+
params: typing.Union[ProcessParams, dict, str],
|
|
35
60
|
out_path: typing.Optional[str] = None,
|
|
36
61
|
**kwargs: typing.Any,
|
|
37
|
-
) -> typing.Optional[typing.ByteString]:
|
|
38
|
-
fmt = kwargs.get("format") or im.format
|
|
39
|
-
kwargs["format"] = fmt
|
|
40
|
-
|
|
41
|
-
if fmt.upper() == enums.ImageFormat.JPEG and im.mode == "RGBA":
|
|
42
|
-
im = im.convert("RGB")
|
|
43
|
-
|
|
44
|
-
if not kwargs.get("quality"):
|
|
45
|
-
if fmt.upper() == enums.ImageFormat.JPEG and im.format == enums.ImageFormat.JPEG:
|
|
46
|
-
kwargs["quality"] = "keep"
|
|
47
|
-
else:
|
|
48
|
-
kwargs["quality"] = settings.PROCESSOR_DEFAULT_QUALITY
|
|
49
|
-
|
|
50
|
-
if out_path:
|
|
51
|
-
# icc_profile 是为解决色域的问题
|
|
52
|
-
im.save(out_path, **kwargs)
|
|
53
|
-
return None
|
|
54
|
-
|
|
55
|
-
# 没有传递保存的路径,返回文件内容
|
|
56
|
-
suffix = fmt or "png"
|
|
57
|
-
with tempfile.NamedTemporaryFile(suffix=f".{suffix}") as fp:
|
|
58
|
-
im.save(fp.name, **kwargs)
|
|
59
|
-
fp.seek(0)
|
|
60
|
-
content = fp.read()
|
|
61
|
-
return content
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def process_image_by_path(
|
|
65
|
-
input_path: str, out_path: str, params: typing.Union[ProcessParams, dict, str]
|
|
66
62
|
) -> typing.Optional[typing.ByteString]:
|
|
67
63
|
"""处理图像
|
|
68
64
|
|
|
69
65
|
Args:
|
|
70
|
-
|
|
71
|
-
out_path: 输出图像保存路径
|
|
66
|
+
input_uri: 输入图像路径
|
|
72
67
|
params: 图像处理参数
|
|
68
|
+
out_path: 输出图像保存路径
|
|
73
69
|
|
|
74
70
|
Raises:
|
|
75
71
|
ProcessLimitException: 超过处理限制会抛出异常
|
|
@@ -77,21 +73,40 @@ def process_image_by_path(
|
|
|
77
73
|
Returns:
|
|
78
74
|
默认输出直接存储无返回,仅当out_path为空时会返回处理后图像的二进制内容
|
|
79
75
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
# 初始化输入
|
|
77
|
+
params_obj: ProcessParams = ProcessParams.init(params)
|
|
78
|
+
with trans_uri_to_im(input_uri) as ori_im:
|
|
79
|
+
# 处理图像
|
|
80
|
+
im = ProcessorCtr.handle_img_actions(ori_im, params_obj.actions)
|
|
81
|
+
# 输出、保存
|
|
82
|
+
_kwargs = params_obj.save_parser.compute(ori_im, im)
|
|
83
|
+
_kwargs.update(kwargs)
|
|
84
|
+
ret = ProcessorCtr.save_img_to_file(im, out_path=out_path, **_kwargs)
|
|
85
|
+
return ret
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def process_image_obj(
|
|
89
|
+
ori_im: Image,
|
|
90
|
+
params: typing.Union[ProcessParams, dict, str],
|
|
91
|
+
out_path: typing.Optional[str] = None,
|
|
92
|
+
**kwargs: typing.Any,
|
|
93
|
+
) -> Image:
|
|
94
|
+
"""处理图像
|
|
88
95
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
Args:
|
|
97
|
+
ori_im: 输入图像为Image对象
|
|
98
|
+
params: 图像处理参数
|
|
99
|
+
out_path: 输出图像保存路径
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
Returns:
|
|
102
|
+
默认输出直接存储无返回,仅当out_path为空时会返回处理后图像的二进制内容
|
|
103
|
+
"""
|
|
104
|
+
params_obj: ProcessParams = ProcessParams.init(params)
|
|
105
|
+
im = ProcessorCtr.handle_img_actions(ori_im, params_obj.actions)
|
|
106
|
+
_kwargs = params_obj.save_parser.compute(ori_im, im)
|
|
107
|
+
_kwargs.update(kwargs)
|
|
108
|
+
ret = ProcessorCtr.save_img_to_file(im, out_path=out_path, **_kwargs)
|
|
109
|
+
return ret
|
|
95
110
|
|
|
96
111
|
|
|
97
112
|
def extract_main_color(img_path: str, delta_h: float = 0.3) -> str:
|
|
@@ -105,20 +120,20 @@ def extract_main_color(img_path: str, delta_h: float = 0.3) -> str:
|
|
|
105
120
|
颜色值,eg: FFFFFF
|
|
106
121
|
"""
|
|
107
122
|
r, g, b = 0, 0, 0
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
with Image.open(img_path) as im:
|
|
124
|
+
if im.mode != "RGB":
|
|
125
|
+
im = im.convert("RGB")
|
|
126
|
+
# 转换成HSV即 色相(Hue)、饱和度(Saturation)、明度(alue),取值范围[0,1]
|
|
127
|
+
# 取H计算平均色相
|
|
128
|
+
all_h = [colorsys.rgb_to_hsv(*im.getpixel((x, y)))[0] for x in range(im.size[0]) for y in range(im.size[1])]
|
|
129
|
+
avg_h = sum(all_h) / (im.size[0] * im.size[1])
|
|
130
|
+
# 取与平均色相相近的像素色值rgb用于计算,像素值取值范围[0,255]
|
|
131
|
+
beyond = list(
|
|
132
|
+
filter(
|
|
133
|
+
lambda x: abs(colorsys.rgb_to_hsv(*x)[0] - avg_h) < delta_h,
|
|
134
|
+
[im.getpixel((x, y)) for x in range(im.size[0]) for y in range(im.size[1])],
|
|
135
|
+
)
|
|
120
136
|
)
|
|
121
|
-
)
|
|
122
137
|
if len(beyond):
|
|
123
138
|
r = int(sum(e[0] for e in beyond) / len(beyond))
|
|
124
139
|
g = int(sum(e[1] for e in beyond) / len(beyond))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: py-img-processor
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.3.1
|
|
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 >=
|
|
24
|
+
Requires-Dist: py-enum >=2.1.1
|
|
25
25
|
Requires-Dist: Pillow >=8
|
|
26
26
|
|
|
27
27
|
# py-img-processor
|
|
@@ -71,6 +71,9 @@ 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匹配),默认无限制 | `()` |
|
|
76
|
+
| PROCESSOR_TEMP_DIR | str | tmpfile使用的临时目录,不设置默认使用系统tmp目录 | `None` |
|
|
74
77
|
|
|
75
78
|
> `注意`:`PROCESSOR_TEXT_FONT` 字体的设置是文字水印必要参数,需保证系统已安装该字体。默认值 `Arial Unicode.ttf` 是MacOS系统存在的字体,建议设置字体文件路径。
|
|
76
79
|
|
|
@@ -82,13 +85,19 @@ Image editor using Python and Pillow.
|
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
### 处理函数
|
|
85
|
-
|
|
88
|
+
```python
|
|
89
|
+
from imgprocessor.processor import process_image, process_image_obj
|
|
90
|
+
|
|
91
|
+
process_image(input_uri, params, out_path=out_path)
|
|
92
|
+
# 或者
|
|
93
|
+
process_image_obj(im, params, out_path=out_path)
|
|
94
|
+
```
|
|
86
95
|
|
|
87
96
|
参数说明:
|
|
88
97
|
|
|
89
|
-
- `
|
|
90
|
-
- `out_path` str, 输出图像保存路径
|
|
98
|
+
- `input_uri` str,输入图像文件路径或者链接地址
|
|
91
99
|
- `params` str or json,图像处理参数,参数说明详见 [Reference.md](https://github.com/SkylerHu/py-img-processor/blob/master/docs/Reference.md)
|
|
100
|
+
- `out_path` str, 输出图像保存路径, 默认为空,为空时返回二进制内容
|
|
92
101
|
|
|
93
102
|
|
|
94
103
|
### 图像处理参数为字符串
|
|
@@ -100,13 +109,13 @@ Image editor using Python and Pillow.
|
|
|
100
109
|
|
|
101
110
|
```python
|
|
102
111
|
from imgprocessor.utils import base64url_encode
|
|
103
|
-
from imgprocessor.processor import
|
|
112
|
+
from imgprocessor.processor import process_image
|
|
104
113
|
|
|
105
|
-
|
|
114
|
+
process_image(
|
|
106
115
|
"docs/imgs/lenna-400x225.jpg",
|
|
107
|
-
"/tmp/output.png",
|
|
108
116
|
# 对图片缩放、裁剪、生成圆角、并转成png存储
|
|
109
117
|
f"resize,s_200/crop,w_200,h_200,g_center/watermark,text_{base64url_encode('Hello 世界')},color_FFF,size_20/circle,r_10/format,png",
|
|
118
|
+
out_path="/tmp/output.png",
|
|
110
119
|
)
|
|
111
120
|
```
|
|
112
121
|
|
|
@@ -120,9 +129,8 @@ process_image_by_path(
|
|
|
120
129
|
- 其他参数都放在 `actions` 数组中;
|
|
121
130
|
|
|
122
131
|
```python
|
|
123
|
-
|
|
132
|
+
process_image(
|
|
124
133
|
"docs/imgs/lenna-400x225.jpg",
|
|
125
|
-
"/tmp/output.png",
|
|
126
134
|
{
|
|
127
135
|
"actions": [
|
|
128
136
|
{"key": "resize", "s": 200},
|
|
@@ -133,6 +141,7 @@ process_image_by_path(
|
|
|
133
141
|
],
|
|
134
142
|
"format": "png",
|
|
135
143
|
},
|
|
144
|
+
out_path="/tmp/output.png",
|
|
136
145
|
)
|
|
137
146
|
```
|
|
138
147
|
该操作与上述字符串示例参数等效。
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
imgprocessor/__init__.py,sha256=qtGcx7kDmNawsCwjn2HY7nJoQibl63Mrwdk8mD4kEds,2540
|
|
2
|
+
imgprocessor/enums.py,sha256=FEkNzclXiu9Ne0DXuWxlqG3m6q7yENSmlcObnfpDFus,2955
|
|
3
|
+
imgprocessor/exceptions.py,sha256=kj9win_XjCbZnXr93sYs4Cfg1hAJX2fn5hgr1YMNoHQ,384
|
|
4
|
+
imgprocessor/main.py,sha256=oBDQJAyx9iC_7xNllsJ6qlSE91Tks0LNJ6O1MzryMoY,4126
|
|
5
|
+
imgprocessor/processor.py,sha256=ocXllGWPiDiSnXEPZqRIU8qNFwomZTJ6_L0m_gTk-fY,4833
|
|
6
|
+
imgprocessor/utils.py,sha256=KBRQJaN8FuuRD2QQPYuuHQ7S-2ML-ctz4rstQTrEdog,1202
|
|
7
|
+
imgprocessor/parsers/__init__.py,sha256=NdXhxC_3a74lUDlFelQ3oGAeK_7tJ9_OwfCOmEd-ZvA,3192
|
|
8
|
+
imgprocessor/parsers/alpha.py,sha256=PudXZBu-s5Fzj6fB7Aae4yxJkmdfd_l_oAFSk7279Zk,877
|
|
9
|
+
imgprocessor/parsers/base.py,sha256=5wVUWLDcch2FyFBCYQCORR_2RHYUghCjqtcgpphfGSE,17231
|
|
10
|
+
imgprocessor/parsers/blur.py,sha256=twVLORVV4nXfvHBaqi353X6odOnRi414sye3AOGG5bY,673
|
|
11
|
+
imgprocessor/parsers/circle.py,sha256=VXWgKIkz9emQHWOoO2jbkUFrEbXb1s6ZM-R8BRxSi4c,1665
|
|
12
|
+
imgprocessor/parsers/crop.py,sha256=FfPPmN63XUQZCgRkKuuycj9SLdS-0hnLAU5mRrj-4Uc,3524
|
|
13
|
+
imgprocessor/parsers/gray.py,sha256=qMPid9X2tJko3pt5O-RNpXBR5drTowwOHTlfEFksiuQ,402
|
|
14
|
+
imgprocessor/parsers/merge.py,sha256=4UIeWwDVg9rJRbTVRvNTyv-mxTA8W5TIgWAvWYXjvhA,5211
|
|
15
|
+
imgprocessor/parsers/resize.py,sha256=31iJws5YVsqJZN4O-HG9OjGBtKFW_mGE5536pYvWchU,5175
|
|
16
|
+
imgprocessor/parsers/rotate.py,sha256=Vb23OJuGQrdGRkJWpDJeeVkZUF9bbsRD2e58P5Q1b1k,734
|
|
17
|
+
imgprocessor/parsers/watermark.py,sha256=MTlRVvtVMR5IyJODk5DMFe01KQKGAulaaNy7f23NvME,8862
|
|
18
|
+
py_img_processor-1.3.1.dist-info/LICENSE,sha256=Sr4UxtHWGmoO_enyTUeAN276LkBqJpse0guCcb6f9BQ,1066
|
|
19
|
+
py_img_processor-1.3.1.dist-info/METADATA,sha256=FslQFBULB1AewqEkX80vfFjkacVAngSLAWWWfpq96lw,7810
|
|
20
|
+
py_img_processor-1.3.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
21
|
+
py_img_processor-1.3.1.dist-info/entry_points.txt,sha256=MLeLjzpkH7DkDMgICWCQ99D9ElqAvTBVBGA8yjg4dhQ,57
|
|
22
|
+
py_img_processor-1.3.1.dist-info/top_level.txt,sha256=5Pm26oHcqZoihGGxc5N6qQJ2LuVa2i4au_uqHBMqehI,13
|
|
23
|
+
py_img_processor-1.3.1.dist-info/RECORD,,
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
imgprocessor/__init__.py,sha256=2ebw4pYHInmKQKQEbkwQ-fHx0uM7ED026wgR04y7EYo,2243
|
|
2
|
-
imgprocessor/enums.py,sha256=-eA9pv5g-0Xm23jrHv5aJf2g56t95-hUgzSz4xlx5Go,3042
|
|
3
|
-
imgprocessor/exceptions.py,sha256=kj9win_XjCbZnXr93sYs4Cfg1hAJX2fn5hgr1YMNoHQ,384
|
|
4
|
-
imgprocessor/main.py,sha256=QDkz__RJ_iIjrb1p56bhPzZ7yLbtzhXvvKpLmvkJEkI,4133
|
|
5
|
-
imgprocessor/processor.py,sha256=4f6DTNeIRMTAlvtEn7UQQFuNUxMQJSB_dfkzERjvUQ4,4388
|
|
6
|
-
imgprocessor/utils.py,sha256=KBRQJaN8FuuRD2QQPYuuHQ7S-2ML-ctz4rstQTrEdog,1202
|
|
7
|
-
imgprocessor/parsers/__init__.py,sha256=3aSxKTflK2hZxaiYtaX3HsFvLwg9QE7YLIp4gdTf7T8,3004
|
|
8
|
-
imgprocessor/parsers/alpha.py,sha256=grOnmZSYhO0wI2pJfGz6CfTwOTc8kfcxspmfM0nzAzA,865
|
|
9
|
-
imgprocessor/parsers/base.py,sha256=80VTehgA8LwwVety3_IGSJ_vsX01mWAZNb-c6v3AT2o,11832
|
|
10
|
-
imgprocessor/parsers/blur.py,sha256=Y9BeTK7e-y8HPNeLQxgtqiquOVf2cVLXhXn1bv9072g,661
|
|
11
|
-
imgprocessor/parsers/circle.py,sha256=z0IZZSv_zuY2jzB6kN01sB1jGhBure33DPt7JAEeNsk,1653
|
|
12
|
-
imgprocessor/parsers/crop.py,sha256=P2kqNVn9tiorq_kgBXLxygthL2EKulNkP6aRh1FrmVw,3464
|
|
13
|
-
imgprocessor/parsers/gray.py,sha256=ZDBHZlBfI91TePWWBU0feSHmtD1ekre5-uV2BDLFTKA,396
|
|
14
|
-
imgprocessor/parsers/merge.py,sha256=JwJKIVlmeKxEQIeuYBKT_i5E6jBBOM2m4fqKWpPfuhs,4049
|
|
15
|
-
imgprocessor/parsers/resize.py,sha256=Zeh9LDLQVaEqijcOsljKGDNTQ2rUmijzQ0ziAgYsSoI,5020
|
|
16
|
-
imgprocessor/parsers/rotate.py,sha256=EDKvlxTgZZu7vAkSS9cuZ296dbAhe5Aq6huH9Hjtvy0,722
|
|
17
|
-
imgprocessor/parsers/watermark.py,sha256=y5ddCw9lRS1fwMjHtI0lIGpWcCIFSQA6Q0l7gpcNS7g,8488
|
|
18
|
-
py_img_processor-1.1.0.dist-info/LICENSE,sha256=Sr4UxtHWGmoO_enyTUeAN276LkBqJpse0guCcb6f9BQ,1066
|
|
19
|
-
py_img_processor-1.1.0.dist-info/METADATA,sha256=bEzbYrVLFqjJBP6pbvt-55Tq789Yo6e_L3Pgyi--RCY,7286
|
|
20
|
-
py_img_processor-1.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
21
|
-
py_img_processor-1.1.0.dist-info/entry_points.txt,sha256=MLeLjzpkH7DkDMgICWCQ99D9ElqAvTBVBGA8yjg4dhQ,57
|
|
22
|
-
py_img_processor-1.1.0.dist-info/top_level.txt,sha256=5Pm26oHcqZoihGGxc5N6qQJ2LuVa2i4au_uqHBMqehI,13
|
|
23
|
-
py_img_processor-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|