py-img-processor 1.0.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.
Potentially problematic release.
This version of py-img-processor might be problematic. Click here for more details.
- imgprocessor/__init__.py +68 -0
- imgprocessor/enums.py +96 -0
- imgprocessor/exceptions.py +26 -0
- imgprocessor/main.py +106 -0
- imgprocessor/parsers/__init__.py +89 -0
- imgprocessor/parsers/alpha.py +32 -0
- imgprocessor/parsers/base.py +353 -0
- imgprocessor/parsers/blur.py +29 -0
- imgprocessor/parsers/circle.py +53 -0
- imgprocessor/parsers/crop.py +100 -0
- imgprocessor/parsers/gray.py +24 -0
- imgprocessor/parsers/merge.py +111 -0
- imgprocessor/parsers/resize.py +122 -0
- imgprocessor/parsers/rotate.py +31 -0
- imgprocessor/parsers/watermark.py +218 -0
- imgprocessor/processor.py +128 -0
- imgprocessor/utils.py +54 -0
- py_img_processor-1.0.0.dist-info/LICENSE +21 -0
- py_img_processor-1.0.0.dist-info/METADATA +164 -0
- py_img_processor-1.0.0.dist-info/RECORD +23 -0
- py_img_processor-1.0.0.dist-info/WHEEL +5 -0
- py_img_processor-1.0.0.dist-info/entry_points.txt +2 -0
- py_img_processor-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
# python3.11只后
|
|
5
|
+
from typing import Self # type: ignore
|
|
6
|
+
except Exception:
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from PIL import Image, ImageOps
|
|
12
|
+
|
|
13
|
+
from imgprocessor import enums, utils
|
|
14
|
+
from imgprocessor.exceptions import ParamValidateException, ParamParseException
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseParser(object):
|
|
18
|
+
# 用来定义参数
|
|
19
|
+
KEY: typing.Any = ""
|
|
20
|
+
ARGS: dict = {}
|
|
21
|
+
|
|
22
|
+
def __init__(self, **kwargs: typing.Any) -> None:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def init(cls, data: dict, enable_base64: bool = False) -> Self:
|
|
27
|
+
params = cls.validate_args(enable_base64=enable_base64, **data)
|
|
28
|
+
ins = cls(**params)
|
|
29
|
+
ins.validate()
|
|
30
|
+
return ins
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def init_by_str(cls, param_str: str) -> Self:
|
|
34
|
+
data = cls.parse_str(param_str)
|
|
35
|
+
return cls.init(data, enable_base64=True)
|
|
36
|
+
|
|
37
|
+
def validate(self) -> None:
|
|
38
|
+
"""由子类继承实现各类实例的数据校验"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def do_action(self, im: Image) -> Image:
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict:
|
|
45
|
+
data = {}
|
|
46
|
+
for k in self.ARGS.keys():
|
|
47
|
+
if k in self.__dict__:
|
|
48
|
+
data[k] = self.__dict__.get(k)
|
|
49
|
+
return data
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def validate_args(cls, enable_base64: bool = False, **kwargs: typing.Any) -> dict:
|
|
53
|
+
data = {}
|
|
54
|
+
for key, config in cls.ARGS.items():
|
|
55
|
+
_type = config["type"]
|
|
56
|
+
_default = config.get("default")
|
|
57
|
+
if key not in kwargs:
|
|
58
|
+
required = config.get("required")
|
|
59
|
+
if required:
|
|
60
|
+
raise ParamValidateException(f"缺少必要参数{key}")
|
|
61
|
+
# 配置的default仅当在没有传递值的时候才生效
|
|
62
|
+
if _default is not None:
|
|
63
|
+
data[key] = _default
|
|
64
|
+
else:
|
|
65
|
+
value = kwargs.get(key)
|
|
66
|
+
try:
|
|
67
|
+
if _type == enums.ArgType.INTEGER:
|
|
68
|
+
value = cls._validate_number(value, **config)
|
|
69
|
+
elif _type == enums.ArgType.FLOAT:
|
|
70
|
+
value = cls._validate_number(value, use_float=True, **config)
|
|
71
|
+
elif _type == enums.ArgType.STRING:
|
|
72
|
+
value = cls._validate_str(value, enable_base64=enable_base64, **config)
|
|
73
|
+
|
|
74
|
+
choices = config.get("choices")
|
|
75
|
+
if choices and value not in choices:
|
|
76
|
+
raise ParamValidateException(f"{key}枚举值只能是其中之一 {choices.values}")
|
|
77
|
+
except ParamValidateException as e:
|
|
78
|
+
raise ParamValidateException(f"参数 {key}={value} 不符合要求:{e}")
|
|
79
|
+
data[key] = value
|
|
80
|
+
|
|
81
|
+
return data
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def _validate_str(
|
|
85
|
+
cls,
|
|
86
|
+
value: typing.Any,
|
|
87
|
+
enable_base64: bool = False,
|
|
88
|
+
regex: typing.Optional[str] = None,
|
|
89
|
+
base64_encode: bool = False,
|
|
90
|
+
max_length: typing.Optional[int] = None,
|
|
91
|
+
**kwargs: dict,
|
|
92
|
+
) -> str:
|
|
93
|
+
if not isinstance(value, str):
|
|
94
|
+
raise ParamValidateException("参数类型不符合要求,必须是字符串类型")
|
|
95
|
+
if enable_base64 and base64_encode:
|
|
96
|
+
value = utils.base64url_decode(value)
|
|
97
|
+
if max_length is not None and len(value) > max_length:
|
|
98
|
+
raise ParamValidateException(f"长度不允许超过{max_length}个字符")
|
|
99
|
+
if regex and not re.match(regex, value):
|
|
100
|
+
raise ParamValidateException(f"不符合格式要求,需符合正则:{regex}")
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def _validate_number(
|
|
105
|
+
cls,
|
|
106
|
+
value: typing.Any,
|
|
107
|
+
min: typing.Optional[int] = None,
|
|
108
|
+
max: typing.Optional[int] = None,
|
|
109
|
+
use_float: bool = False,
|
|
110
|
+
**kwargs: dict,
|
|
111
|
+
) -> typing.Union[int, float]:
|
|
112
|
+
if isinstance(value, int) or (use_float and isinstance(value, (int, float))):
|
|
113
|
+
v = value
|
|
114
|
+
elif isinstance(value, str):
|
|
115
|
+
if not value.isdigit():
|
|
116
|
+
if use_float:
|
|
117
|
+
try:
|
|
118
|
+
v = float(value)
|
|
119
|
+
except Exception:
|
|
120
|
+
raise ParamValidateException("参数类型不符合要求,必须是数值")
|
|
121
|
+
else:
|
|
122
|
+
raise ParamValidateException("参数类型不符合要求,必须是整数")
|
|
123
|
+
else:
|
|
124
|
+
v = int(value)
|
|
125
|
+
else:
|
|
126
|
+
raise ParamValidateException("必须是整数")
|
|
127
|
+
if min is not None and v < min:
|
|
128
|
+
raise ParamValidateException(f"参数不在取值范围内,最小值为{min}")
|
|
129
|
+
if max is not None and v > max:
|
|
130
|
+
raise ParamValidateException(f"参数不在取值范围内,最大值为{max}")
|
|
131
|
+
|
|
132
|
+
return v
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def parse_str(cls, param_str: str) -> dict:
|
|
136
|
+
"""将字符串参数转化为json格式数据
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
param_str: 字符串参数,示例:`resize,h_100,m_lfit`
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
exceptions.ParseParamException: 解析参数不符合预期会抛出异常
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
输出json格式参数,例如返回`{"key": "resize", "h": "100", "m": "lfit"}`
|
|
146
|
+
"""
|
|
147
|
+
params = {}
|
|
148
|
+
info = param_str.split(",")
|
|
149
|
+
key = info[0]
|
|
150
|
+
if key != cls.KEY:
|
|
151
|
+
raise ParamParseException(f"解析出来的key={key}与{cls.__name__}.KEY={cls.KEY}不匹配")
|
|
152
|
+
for item in info[1:]:
|
|
153
|
+
info = item.split("_", 1)
|
|
154
|
+
if len(info) == 2:
|
|
155
|
+
k, v = info
|
|
156
|
+
params[k] = v
|
|
157
|
+
else:
|
|
158
|
+
params["value"] = info[0]
|
|
159
|
+
|
|
160
|
+
params["key"] = key
|
|
161
|
+
return params
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def pre_processing(im: Image, use_alpha: bool = False) -> Image:
|
|
165
|
+
"""预处理图像,默认转成`RGB`,若为`use_alpha=True`转为`RGBA`
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
im: 输入图像
|
|
169
|
+
use_alpha: 是否处理透明度
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
输出图像
|
|
173
|
+
"""
|
|
174
|
+
# 去掉方向信息
|
|
175
|
+
orientation = im.getexif().get(0x0112)
|
|
176
|
+
if orientation and 2 <= orientation <= 8:
|
|
177
|
+
im = ImageOps.exif_transpose(im)
|
|
178
|
+
|
|
179
|
+
if im.mode not in ["RGB", "RGBA"]:
|
|
180
|
+
# 统一处理成RGBA进行操作:
|
|
181
|
+
# 1. 像rotate/resize操作需要RGB模式;
|
|
182
|
+
# 2. 像水印操作需要RGBA;
|
|
183
|
+
im = im.convert("RGBA")
|
|
184
|
+
|
|
185
|
+
if use_alpha and im.mode != "RGBA":
|
|
186
|
+
im = im.convert("RGBA")
|
|
187
|
+
|
|
188
|
+
return im
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def compute_by_geography(
|
|
192
|
+
src_w: int, src_h: int, x: int, y: int, w: int, h: int, g: typing.Optional[str], pf: str
|
|
193
|
+
) -> tuple[int, int]:
|
|
194
|
+
"""计算 大小(w,h)的图像相对于(src_w, src_h)图像的原点(x,y)位置"""
|
|
195
|
+
if g == enums.Geography.NW:
|
|
196
|
+
x, y = 0, 0
|
|
197
|
+
elif g == enums.Geography.NORTH:
|
|
198
|
+
x, y = int(src_w / 2 - w / 2), 0
|
|
199
|
+
elif g == enums.Geography.NE:
|
|
200
|
+
x, y = src_w - w, 0
|
|
201
|
+
elif g == enums.Geography.WEST:
|
|
202
|
+
x, y = 0, int(src_h / 2 - h / 2)
|
|
203
|
+
elif g == enums.Geography.CENTER:
|
|
204
|
+
x, y = int(src_w / 2 - w / 2), int(src_h / 2 - h / 2)
|
|
205
|
+
elif g == enums.Geography.EAST:
|
|
206
|
+
x, y = src_w - w, int(src_h / 2 - h / 2)
|
|
207
|
+
elif g == enums.Geography.SW:
|
|
208
|
+
x, y = 0, src_h - h
|
|
209
|
+
elif g == enums.Geography.SOUTH:
|
|
210
|
+
x, y = int(src_w / 2 - w / 2), src_h - h
|
|
211
|
+
elif g == enums.Geography.SE:
|
|
212
|
+
x, y = src_w - w, src_h - h
|
|
213
|
+
elif pf:
|
|
214
|
+
if "x" in pf:
|
|
215
|
+
if x < 0 or x > 100:
|
|
216
|
+
raise ParamValidateException(f"pf={pf}包含了x,所以x作为百分比取值范围为[0,100]")
|
|
217
|
+
x = int(src_w * x / 100)
|
|
218
|
+
if "y" in pf:
|
|
219
|
+
if y < 0 or y > 100:
|
|
220
|
+
raise ParamValidateException(f"pf={pf}包含了y,所以y作为百分比取值范围为[0,100]")
|
|
221
|
+
y = int(src_h * y / 100)
|
|
222
|
+
return x, y
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def compute_by_ratio(src_w: int, src_h: int, ratio: str) -> tuple[int, int]:
|
|
226
|
+
"""根据输入宽高,按照比例比计算出最大区域
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
src_w: 输入宽度
|
|
230
|
+
src_h: 输入高度
|
|
231
|
+
ratio: 比例字符串,eg "4:3"
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
计算后的宽高
|
|
235
|
+
"""
|
|
236
|
+
w_r, h_r = ratio.split(":")
|
|
237
|
+
wr, hr = int(w_r), int(h_r)
|
|
238
|
+
if src_w * hr > src_h * wr:
|
|
239
|
+
# 相对于目标比例,宽长了
|
|
240
|
+
w = int(src_h * wr / hr)
|
|
241
|
+
h = src_h
|
|
242
|
+
elif src_w * hr < src_h * wr:
|
|
243
|
+
w = src_w
|
|
244
|
+
h = int(src_w * hr / wr)
|
|
245
|
+
else:
|
|
246
|
+
# 刚好符合比例
|
|
247
|
+
w, h = src_w, src_h
|
|
248
|
+
return w, h
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def compute_splice_two_im(
|
|
252
|
+
w1: int,
|
|
253
|
+
h1: int,
|
|
254
|
+
w2: int,
|
|
255
|
+
h2: int,
|
|
256
|
+
align: int = enums.PositionAlign.VERTIAL_CENTER, # type: ignore
|
|
257
|
+
order: int = enums.PositionOrder.BEFORE, # type: ignore
|
|
258
|
+
interval: int = 0,
|
|
259
|
+
) -> tuple:
|
|
260
|
+
"""拼接2个图像,计算整体大小和元素原点位置;数值单位都是像素
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
w1: 第1个元素的宽
|
|
264
|
+
h1: 第1个元素的高
|
|
265
|
+
w2: 第2个元素的宽
|
|
266
|
+
h2: 第2个元素的高
|
|
267
|
+
align: 对齐方式 see enums.PositionAlign
|
|
268
|
+
order: 排序 see enums.PositionOrder
|
|
269
|
+
interval: 元素之间的间隔
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
整体占位w宽度
|
|
273
|
+
整体占位y宽度
|
|
274
|
+
第1个元素的原点位置x1
|
|
275
|
+
第1个元素的原点位置y1
|
|
276
|
+
第2个元素的原点位置x2
|
|
277
|
+
第2个元素的原点位置y2
|
|
278
|
+
"""
|
|
279
|
+
if align in [enums.PositionAlign.TOP, enums.PositionAlign.HORIZONTAL_CENTER, enums.PositionAlign.BOTTOM]:
|
|
280
|
+
# 水平顺序
|
|
281
|
+
# 计算整体占位大小w,h
|
|
282
|
+
w, h = w1 + w2 + interval, max(h1, h2)
|
|
283
|
+
|
|
284
|
+
if align == enums.PositionAlign.TOP:
|
|
285
|
+
y1, y2 = 0, 0
|
|
286
|
+
elif align == enums.PositionAlign.BOTTOM:
|
|
287
|
+
y1, y2 = h - h1, h - h2
|
|
288
|
+
else:
|
|
289
|
+
y1, y2 = int((h - h1) / 2), int((h - h2) / 2)
|
|
290
|
+
|
|
291
|
+
if order == enums.PositionOrder.BEFORE:
|
|
292
|
+
x1, x2 = 0, w1 + interval
|
|
293
|
+
else:
|
|
294
|
+
x1, x2 = w2 + interval, 0
|
|
295
|
+
else:
|
|
296
|
+
# 垂直
|
|
297
|
+
w, h = max(w1, w2), h1 + h2 + interval
|
|
298
|
+
if align == enums.PositionAlign.LEFT:
|
|
299
|
+
x1, x2 = 0, 0
|
|
300
|
+
elif align == enums.PositionAlign.RIGHT:
|
|
301
|
+
x1, x2 = w - w1, w - w2
|
|
302
|
+
else:
|
|
303
|
+
x1, x2 = int((w - w1) / 2), int((w - w2) / 2)
|
|
304
|
+
|
|
305
|
+
if order == enums.PositionOrder.BEFORE:
|
|
306
|
+
y1, y2 = 0, h1 + interval
|
|
307
|
+
else:
|
|
308
|
+
y1, y2 = h2 + interval, 0
|
|
309
|
+
|
|
310
|
+
return w, h, x1, y1, x2, y2
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class ImgSaveParser(BaseParser):
|
|
314
|
+
KEY = ""
|
|
315
|
+
ARGS = {
|
|
316
|
+
"format": {"type": enums.ArgType.STRING, "default": None},
|
|
317
|
+
"quality": {"type": enums.ArgType.INTEGER, "default": None, "min": 1, "max": 100},
|
|
318
|
+
# 1 表示将原图设置成渐进显示
|
|
319
|
+
"interlace": {"type": enums.ArgType.INTEGER, "default": 0, "choices": [0, 1]},
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
def __init__(
|
|
323
|
+
self,
|
|
324
|
+
format: typing.Optional[str] = None,
|
|
325
|
+
quality: typing.Optional[int] = None,
|
|
326
|
+
interlace: int = 0,
|
|
327
|
+
**kwargs: typing.Any,
|
|
328
|
+
) -> None:
|
|
329
|
+
self.format = format
|
|
330
|
+
self.quality = quality
|
|
331
|
+
self.interlace = interlace
|
|
332
|
+
|
|
333
|
+
def validate(self) -> None:
|
|
334
|
+
super().validate()
|
|
335
|
+
if self.format:
|
|
336
|
+
fmt_values = [v.lower() for v in enums.ImageFormat.values]
|
|
337
|
+
if self.format not in fmt_values:
|
|
338
|
+
raise ParamValidateException(f"参数 format 只能是其中之一:{fmt_values}")
|
|
339
|
+
|
|
340
|
+
def compute(self, in_im: Image, out_im: Image) -> dict:
|
|
341
|
+
kwargs = {
|
|
342
|
+
"format": self.format or in_im.format,
|
|
343
|
+
# png 和 gif 格式的选项是 interlace(一般翻译成交错),jpeg(jpg) 的选项则是 progressive (翻译成 渐进)
|
|
344
|
+
"progressive": True if self.interlace else False,
|
|
345
|
+
"interlace": self.interlace,
|
|
346
|
+
}
|
|
347
|
+
# 为了解决色域问题
|
|
348
|
+
icc_profile = in_im.info.get("icc_profile")
|
|
349
|
+
if icc_profile:
|
|
350
|
+
kwargs["icc_profile"] = icc_profile
|
|
351
|
+
if self.quality:
|
|
352
|
+
kwargs["quality"] = self.quality
|
|
353
|
+
return kwargs
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from PIL import Image, ImageFilter
|
|
6
|
+
|
|
7
|
+
from imgprocessor import enums
|
|
8
|
+
from .base import BaseParser, pre_processing
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BlurParser(BaseParser):
|
|
12
|
+
|
|
13
|
+
KEY = enums.OpAction.BLUR
|
|
14
|
+
ARGS = {
|
|
15
|
+
# 模糊半径,值越大,图片越模糊
|
|
16
|
+
"r": {"type": enums.ArgType.INTEGER, "required": True, "min": 1, "max": 50},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
r: int = 0,
|
|
22
|
+
**kwargs: typing.Any,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.r = r
|
|
25
|
+
|
|
26
|
+
def do_action(self, im: Image) -> Image:
|
|
27
|
+
im = pre_processing(im)
|
|
28
|
+
im = im.filter(ImageFilter.GaussianBlur(radius=self.r))
|
|
29
|
+
return im
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from PIL import Image, ImageDraw
|
|
6
|
+
|
|
7
|
+
from imgprocessor import enums, settings
|
|
8
|
+
from .base import BaseParser, pre_processing
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CircleParser(BaseParser):
|
|
12
|
+
|
|
13
|
+
KEY = enums.OpAction.CIRCLE
|
|
14
|
+
ARGS = {
|
|
15
|
+
"r": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
r: int = 0,
|
|
21
|
+
**kwargs: typing.Any,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.r = r
|
|
24
|
+
|
|
25
|
+
def compute(self, src_w: int, src_h: int) -> int:
|
|
26
|
+
r = self.r
|
|
27
|
+
|
|
28
|
+
min_s = int(min(src_w, src_h) / 2)
|
|
29
|
+
if not r or r > min_s:
|
|
30
|
+
# 没有设置或是超过最大内切圆的半径,按照最大内切圆的半径处理
|
|
31
|
+
r = min_s
|
|
32
|
+
|
|
33
|
+
return r
|
|
34
|
+
|
|
35
|
+
def do_action(self, im: Image) -> Image:
|
|
36
|
+
im = pre_processing(im, use_alpha=True)
|
|
37
|
+
|
|
38
|
+
src_w, src_h = im.size
|
|
39
|
+
rad = self.compute(*im.size)
|
|
40
|
+
# 放大倍数,解决圆角锯齿问题
|
|
41
|
+
expand = 6
|
|
42
|
+
new_rad = rad * expand
|
|
43
|
+
circle = Image.new("L", (new_rad * 2, new_rad * 2), 0)
|
|
44
|
+
ImageDraw.Draw(circle).ellipse((0, 0, new_rad * 2, new_rad * 2), fill=255)
|
|
45
|
+
circle = circle.resize((rad * 2, rad * 2), resample=Image.LANCZOS)
|
|
46
|
+
|
|
47
|
+
alpha = Image.new("L", (src_w, src_h), 255)
|
|
48
|
+
alpha.paste(circle.crop((0, 0, rad, rad)), (0, 0))
|
|
49
|
+
alpha.paste(circle.crop((0, rad, rad, rad * 2)), (0, src_h - rad))
|
|
50
|
+
alpha.paste(circle.crop((rad, 0, rad * 2, rad)), (src_w - rad, 0))
|
|
51
|
+
alpha.paste(circle.crop((rad, rad, rad * 2, rad * 2)), (src_w - rad, src_h - rad))
|
|
52
|
+
im.putalpha(alpha)
|
|
53
|
+
return im
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
import typing
|
|
4
|
+
from PIL import Image
|
|
5
|
+
from imgprocessor import enums, settings
|
|
6
|
+
from imgprocessor.exceptions import ParamValidateException
|
|
7
|
+
from .base import BaseParser, pre_processing, compute_by_geography, compute_by_ratio
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CropParser(BaseParser):
|
|
11
|
+
|
|
12
|
+
KEY = enums.OpAction.CROP
|
|
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},
|
|
20
|
+
# percent field, eg: xywh
|
|
21
|
+
"pf": {"type": enums.ArgType.STRING, "default": ""},
|
|
22
|
+
# padding right
|
|
23
|
+
"padr": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
|
|
24
|
+
# padding bottom
|
|
25
|
+
"padb": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": settings.PROCESSOR_MAX_W_H},
|
|
26
|
+
# 左和上通过x,y控制
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
w: int = 0,
|
|
32
|
+
h: int = 0,
|
|
33
|
+
ratio: typing.Optional[str] = None,
|
|
34
|
+
x: int = 0,
|
|
35
|
+
y: int = 0,
|
|
36
|
+
g: typing.Optional[str] = None,
|
|
37
|
+
pf: str = "",
|
|
38
|
+
padr: int = 0,
|
|
39
|
+
padb: int = 0,
|
|
40
|
+
**kwargs: typing.Any,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.w = w
|
|
43
|
+
self.h = h
|
|
44
|
+
self.ratio = ratio
|
|
45
|
+
self.x = x
|
|
46
|
+
self.y = y
|
|
47
|
+
self.g = g
|
|
48
|
+
self.pf = pf
|
|
49
|
+
self.padr = padr
|
|
50
|
+
self.padb = padb
|
|
51
|
+
|
|
52
|
+
def compute(self, src_w: int, src_h: int) -> tuple:
|
|
53
|
+
x, y, w, h = self.x, self.y, self.w, self.h
|
|
54
|
+
|
|
55
|
+
pf = self.pf or ""
|
|
56
|
+
if self.g:
|
|
57
|
+
# g在的时候pf不生效
|
|
58
|
+
pf = ""
|
|
59
|
+
|
|
60
|
+
# 处理w,h; w,h默认原图大小
|
|
61
|
+
if self.ratio:
|
|
62
|
+
w, h = compute_by_ratio(src_w, src_h, self.ratio)
|
|
63
|
+
else:
|
|
64
|
+
if "w" in pf:
|
|
65
|
+
if w < 0 or w > 100:
|
|
66
|
+
raise ParamValidateException(f"pf={pf}包含了w,所以w作为百分比取值范围为[0,100]")
|
|
67
|
+
w = int(src_w * w / 100)
|
|
68
|
+
elif not w:
|
|
69
|
+
w = src_w
|
|
70
|
+
|
|
71
|
+
if "h" in pf:
|
|
72
|
+
if h < 0 or h > 100:
|
|
73
|
+
raise ParamValidateException(f"pf={pf}包含了h,所以h作为百分比取值范围为[0,100]")
|
|
74
|
+
h = int(src_h * h / 100)
|
|
75
|
+
elif not h:
|
|
76
|
+
h = src_h
|
|
77
|
+
|
|
78
|
+
# 按照其他方式计算x,y
|
|
79
|
+
x, y = compute_by_geography(src_w, src_h, x, y, w, h, self.g, pf)
|
|
80
|
+
|
|
81
|
+
# 处理裁边
|
|
82
|
+
if self.padr:
|
|
83
|
+
w = w - self.padr
|
|
84
|
+
if self.padb:
|
|
85
|
+
h = h - self.padb
|
|
86
|
+
|
|
87
|
+
if x < 0 or y < 0 or w <= 0 or h <= 0 or x + w > src_w or y + h > src_h:
|
|
88
|
+
raise ParamValidateException(f"(x, y, w, h)={(x, y, w, h)} 区域超过了原始图片")
|
|
89
|
+
|
|
90
|
+
return x, y, w, h
|
|
91
|
+
|
|
92
|
+
def do_action(self, im: Image) -> Image:
|
|
93
|
+
im = pre_processing(im)
|
|
94
|
+
x, y, w, h = self.compute(*im.size)
|
|
95
|
+
|
|
96
|
+
if x == 0 and y == 0 and (w, h) == im.size:
|
|
97
|
+
# 大小没有变化直接返回
|
|
98
|
+
return im
|
|
99
|
+
im = im.crop((x, y, x + w, y + h))
|
|
100
|
+
return im
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from PIL import Image
|
|
6
|
+
|
|
7
|
+
from imgprocessor import enums
|
|
8
|
+
from .base import BaseParser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GrayParser(BaseParser):
|
|
12
|
+
|
|
13
|
+
KEY = enums.OpAction.GRAY
|
|
14
|
+
ARGS = {}
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
**kwargs: typing.Any,
|
|
19
|
+
) -> None:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def do_action(self, im: Image) -> Image:
|
|
23
|
+
im = im.convert("L")
|
|
24
|
+
return im
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from PIL import Image
|
|
6
|
+
|
|
7
|
+
from imgprocessor import enums, settings
|
|
8
|
+
from .base import BaseParser, pre_processing, compute_by_geography, compute_splice_two_im
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MergeParser(BaseParser):
|
|
12
|
+
|
|
13
|
+
KEY = enums.OpAction.MERGE
|
|
14
|
+
ARGS = {
|
|
15
|
+
# 要处理的图片
|
|
16
|
+
"image": {"type": enums.ArgType.STRING, "required": True, "base64_encode": True},
|
|
17
|
+
# 对image的处理参数
|
|
18
|
+
"action": {"type": enums.ArgType.STRING, "base64_encode": True},
|
|
19
|
+
# 使用输入图像的大小作为参照进行缩放
|
|
20
|
+
"p": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": 1000},
|
|
21
|
+
# 对齐方式
|
|
22
|
+
"order": {"type": enums.ArgType.INTEGER, "choices": enums.PositionOrder},
|
|
23
|
+
"align": {"type": enums.ArgType.INTEGER, "default": enums.PositionAlign.BOTTOM, "choices": enums.PositionAlign},
|
|
24
|
+
"interval": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 1000},
|
|
25
|
+
# 粘贴的位置
|
|
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": ""},
|
|
30
|
+
# 拼接后大小包含2个图像,空白区域使用color颜色填充
|
|
31
|
+
"color": {
|
|
32
|
+
"type": enums.ArgType.STRING,
|
|
33
|
+
"default": "FFFFFF",
|
|
34
|
+
"regex": r"^([0-9a-fA-F]{6}|[0-9a-fA-F]{8}|[0-9a-fA-F]{3,4})$",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
image: typing.Optional[str] = None,
|
|
41
|
+
action: typing.Optional[str] = None,
|
|
42
|
+
p: int = 0,
|
|
43
|
+
order: typing.Optional[int] = None,
|
|
44
|
+
align: int = 2,
|
|
45
|
+
interval: int = 0,
|
|
46
|
+
g: typing.Optional[str] = None,
|
|
47
|
+
x: int = 0,
|
|
48
|
+
y: int = 0,
|
|
49
|
+
pf: str = "",
|
|
50
|
+
color: str = "FFFFFF",
|
|
51
|
+
**kwargs: typing.Any,
|
|
52
|
+
) -> None:
|
|
53
|
+
self.image = image
|
|
54
|
+
self.action = action
|
|
55
|
+
self.p = p
|
|
56
|
+
self.order = order
|
|
57
|
+
self.align = align
|
|
58
|
+
self.interval = interval
|
|
59
|
+
self.g = g
|
|
60
|
+
self.x = x
|
|
61
|
+
self.y = y
|
|
62
|
+
self.pf = pf
|
|
63
|
+
self.color = color
|
|
64
|
+
|
|
65
|
+
def compute(self, src_w: int, src_h: int, w2: int, h2: int) -> tuple:
|
|
66
|
+
if self.order in enums.PositionOrder: # type: ignore
|
|
67
|
+
order = typing.cast(int, self.order)
|
|
68
|
+
w, h, x1, y1, x2, y2 = compute_splice_two_im(
|
|
69
|
+
src_w,
|
|
70
|
+
src_h,
|
|
71
|
+
w2,
|
|
72
|
+
h2,
|
|
73
|
+
align=self.align,
|
|
74
|
+
order=order,
|
|
75
|
+
interval=self.interval,
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
x, y = compute_by_geography(src_w, src_h, self.x, self.y, w2, h2, self.g, self.pf)
|
|
79
|
+
x1, y1, x2, y2 = 0, 0, x, y
|
|
80
|
+
if x < 0:
|
|
81
|
+
# 一般是因为第2张图像大小大于第1张
|
|
82
|
+
x1, x2 = -x, 0
|
|
83
|
+
if y < 0:
|
|
84
|
+
y1, y2 = -y, 0
|
|
85
|
+
|
|
86
|
+
# 计算新建画布的大小
|
|
87
|
+
w, h = max(x + w2, src_w, w2), max(y + h2, src_h, h2)
|
|
88
|
+
|
|
89
|
+
return w, h, x1, y1, x2, y2
|
|
90
|
+
|
|
91
|
+
def do_action(self, im: Image) -> Image:
|
|
92
|
+
im = pre_processing(im, use_alpha=True)
|
|
93
|
+
src_w, src_h = im.size
|
|
94
|
+
|
|
95
|
+
im2 = Image.open(self.image)
|
|
96
|
+
im2 = pre_processing(im2, use_alpha=True)
|
|
97
|
+
if self.action:
|
|
98
|
+
from imgprocessor.processor import ProcessParams, handle_img_actions
|
|
99
|
+
|
|
100
|
+
params = ProcessParams.parse_str(self.action)
|
|
101
|
+
im2 = handle_img_actions(im2, params.actions)
|
|
102
|
+
if self.p:
|
|
103
|
+
w2, h2 = int(src_w * self.p / 100), int(src_h * self.p / 100)
|
|
104
|
+
im2 = im2.resize((w2, h2), resample=Image.LANCZOS)
|
|
105
|
+
w2, h2 = im2.size
|
|
106
|
+
|
|
107
|
+
w, h, x1, y1, x2, y2 = self.compute(src_w, src_h, w2, h2)
|
|
108
|
+
out = Image.new("RGBA", (w, h), color=f"#{self.color}")
|
|
109
|
+
out.paste(im, (x1, y1), im)
|
|
110
|
+
out.paste(im2, (x2, y2), im2)
|
|
111
|
+
return out
|