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.

@@ -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