py-img-processor 1.0.1__tar.gz → 1.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {py_img_processor-1.0.1/py_img_processor.egg-info → py_img_processor-1.2.4}/PKG-INFO +15 -8
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/README.md +13 -6
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/__init__.py +7 -1
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/enums.py +4 -2
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/main.py +2 -2
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/parsers/__init__.py +15 -14
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/parsers/alpha.py +2 -2
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/parsers/base.py +162 -41
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/parsers/blur.py +2 -2
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/parsers/circle.py +2 -2
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/parsers/crop.py +12 -12
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/parsers/gray.py +1 -1
- py_img_processor-1.2.4/imgprocessor/parsers/merge.py +148 -0
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/parsers/resize.py +34 -29
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/parsers/rotate.py +2 -2
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/parsers/watermark.py +48 -29
- py_img_processor-1.2.4/imgprocessor/processor.py +129 -0
- {py_img_processor-1.0.1 → py_img_processor-1.2.4/py_img_processor.egg-info}/PKG-INFO +15 -8
- py_img_processor-1.2.4/py_img_processor.egg-info/requires.txt +2 -0
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/setup.py +1 -1
- py_img_processor-1.0.1/imgprocessor/parsers/merge.py +0 -111
- py_img_processor-1.0.1/imgprocessor/processor.py +0 -128
- py_img_processor-1.0.1/py_img_processor.egg-info/requires.txt +0 -2
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/LICENSE +0 -0
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/MANIFEST.in +0 -0
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/exceptions.py +0 -0
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/imgprocessor/utils.py +0 -0
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/py_img_processor.egg-info/SOURCES.txt +0 -0
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/py_img_processor.egg-info/dependency_links.txt +0 -0
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/py_img_processor.egg-info/entry_points.txt +0 -0
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/py_img_processor.egg-info/not-zip-safe +0 -0
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/py_img_processor.egg-info/top_level.txt +0 -0
- {py_img_processor-1.0.1 → py_img_processor-1.2.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: py-img-processor
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.4
|
|
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
|
|
@@ -54,7 +54,7 @@ Image editor using Python and Pillow.
|
|
|
54
54
|
|
|
55
55
|
## 2. 使用(Usage)
|
|
56
56
|
|
|
57
|
-
具体使用说明查看 [readthedocs](https://py-img-processor.readthedocs.io) 。
|
|
57
|
+
具体使用说明查看 [readthedocs](https://py-img-processor.readthedocs.io/) 。
|
|
58
58
|
|
|
59
59
|
## 2.1 运行配置
|
|
60
60
|
可以通过指定环境变量`PY_SETTINGS_MODULE`加载配置文件:
|
|
@@ -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,11 +85,15 @@ Image editor using Python and Pillow.
|
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
### 处理函数
|
|
85
|
-
|
|
88
|
+
```python
|
|
89
|
+
from imgprocessor.processor import process_image
|
|
90
|
+
|
|
91
|
+
process_image(input_uri, out_path, params)
|
|
92
|
+
```
|
|
86
93
|
|
|
87
94
|
参数说明:
|
|
88
95
|
|
|
89
|
-
- `
|
|
96
|
+
- `input_uri` str,输入图像文件路径或者链接地址
|
|
90
97
|
- `out_path` str, 输出图像保存路径
|
|
91
98
|
- `params` str or json,图像处理参数,参数说明详见 [Reference.md](https://github.com/SkylerHu/py-img-processor/blob/master/docs/Reference.md)
|
|
92
99
|
|
|
@@ -100,9 +107,9 @@ Image editor using Python and Pillow.
|
|
|
100
107
|
|
|
101
108
|
```python
|
|
102
109
|
from imgprocessor.utils import base64url_encode
|
|
103
|
-
from imgprocessor.processor import
|
|
110
|
+
from imgprocessor.processor import process_image
|
|
104
111
|
|
|
105
|
-
|
|
112
|
+
process_image(
|
|
106
113
|
"docs/imgs/lenna-400x225.jpg",
|
|
107
114
|
"/tmp/output.png",
|
|
108
115
|
# 对图片缩放、裁剪、生成圆角、并转成png存储
|
|
@@ -120,7 +127,7 @@ process_image_by_path(
|
|
|
120
127
|
- 其他参数都放在 `actions` 数组中;
|
|
121
128
|
|
|
122
129
|
```python
|
|
123
|
-
|
|
130
|
+
process_image(
|
|
124
131
|
"docs/imgs/lenna-400x225.jpg",
|
|
125
132
|
"/tmp/output.png",
|
|
126
133
|
{
|
|
@@ -28,7 +28,7 @@ Image editor using Python and Pillow.
|
|
|
28
28
|
|
|
29
29
|
## 2. 使用(Usage)
|
|
30
30
|
|
|
31
|
-
具体使用说明查看 [readthedocs](https://py-img-processor.readthedocs.io) 。
|
|
31
|
+
具体使用说明查看 [readthedocs](https://py-img-processor.readthedocs.io/) 。
|
|
32
32
|
|
|
33
33
|
## 2.1 运行配置
|
|
34
34
|
可以通过指定环境变量`PY_SETTINGS_MODULE`加载配置文件:
|
|
@@ -45,6 +45,9 @@ Image editor using Python and Pillow.
|
|
|
45
45
|
| PROCESSOR_MAX_PIXEL | int | width x height总像素3亿,处理前后的值都被此配置限制 | 300000000 |
|
|
46
46
|
| PROCESSOR_DEFAULT_QUALITY | int | 图像处理后的默认质量 | 75 |
|
|
47
47
|
| PROCESSOR_TEXT_FONT | str | 默认字体文件,默认从系统中寻找;也可以直接传递字体文件路径 | Arial Unicode.ttf |
|
|
48
|
+
| PROCESSOR_WORKSPACES | tuple | 限制水印等资源路径 (startswith匹配), 默认无限制 | `()` |
|
|
49
|
+
| PROCESSOR_ALLOW_DOMAINS | tuple | 限制链接地址域名 (endswith匹配),默认无限制 | `()` |
|
|
50
|
+
| PROCESSOR_TEMP_DIR | str | tmpfile使用的临时目录,不设置默认使用系统tmp目录 | `None` |
|
|
48
51
|
|
|
49
52
|
> `注意`:`PROCESSOR_TEXT_FONT` 字体的设置是文字水印必要参数,需保证系统已安装该字体。默认值 `Arial Unicode.ttf` 是MacOS系统存在的字体,建议设置字体文件路径。
|
|
50
53
|
|
|
@@ -56,11 +59,15 @@ Image editor using Python and Pillow.
|
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
### 处理函数
|
|
59
|
-
|
|
62
|
+
```python
|
|
63
|
+
from imgprocessor.processor import process_image
|
|
64
|
+
|
|
65
|
+
process_image(input_uri, out_path, params)
|
|
66
|
+
```
|
|
60
67
|
|
|
61
68
|
参数说明:
|
|
62
69
|
|
|
63
|
-
- `
|
|
70
|
+
- `input_uri` str,输入图像文件路径或者链接地址
|
|
64
71
|
- `out_path` str, 输出图像保存路径
|
|
65
72
|
- `params` str or json,图像处理参数,参数说明详见 [Reference.md](./docs/Reference.md)
|
|
66
73
|
|
|
@@ -74,9 +81,9 @@ Image editor using Python and Pillow.
|
|
|
74
81
|
|
|
75
82
|
```python
|
|
76
83
|
from imgprocessor.utils import base64url_encode
|
|
77
|
-
from imgprocessor.processor import
|
|
84
|
+
from imgprocessor.processor import process_image
|
|
78
85
|
|
|
79
|
-
|
|
86
|
+
process_image(
|
|
80
87
|
"docs/imgs/lenna-400x225.jpg",
|
|
81
88
|
"/tmp/output.png",
|
|
82
89
|
# 对图片缩放、裁剪、生成圆角、并转成png存储
|
|
@@ -94,7 +101,7 @@ process_image_by_path(
|
|
|
94
101
|
- 其他参数都放在 `actions` 数组中;
|
|
95
102
|
|
|
96
103
|
```python
|
|
97
|
-
|
|
104
|
+
process_image(
|
|
98
105
|
"docs/imgs/lenna-400x225.jpg",
|
|
99
106
|
"/tmp/output.png",
|
|
100
107
|
{
|
|
@@ -5,7 +5,7 @@ import importlib
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
__all__ = ["settings", "VERSION"]
|
|
8
|
-
__version__ = "1.
|
|
8
|
+
__version__ = "1.2.4"
|
|
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:
|
|
@@ -51,8 +51,8 @@ class OpAction(ChoiceEnum):
|
|
|
51
51
|
class ResizeMode(ChoiceEnum):
|
|
52
52
|
"""图像缩放的模式"""
|
|
53
53
|
|
|
54
|
-
LFIT = ("lfit", "等比缩放,缩放图限制为指定w与h的矩形内的最大图片") # 类似ImageOps.contain
|
|
55
|
-
MFIT = ("mfit", "等比缩放,缩放图为延伸出指定w与h的矩形框外的最小图片") # 类似ImageOps.cover
|
|
54
|
+
LFIT = ("lfit", "等比缩放,缩放图限制为指定w与h的矩形内的最大图片") # 类似 ImageOps.contain
|
|
55
|
+
MFIT = ("mfit", "等比缩放,缩放图为延伸出指定w与h的矩形框外的最小图片") # 类似 ImageOps.cover
|
|
56
56
|
FIT = ("fit", "将原图等比缩放为延伸出指定w与h的矩形框外的最小图片,然后将超出的部分进行居中裁剪") # ImageOps.fit
|
|
57
57
|
PAD = ("pad", "将原图缩放为指定w与h的矩形内的最大图片,然后使用指定颜色居中填充空白部分") # ImageOps.pad
|
|
58
58
|
FIXED = ("fixed", "固定宽高,强制缩放")
|
|
@@ -62,6 +62,8 @@ class ArgType(ChoiceEnum):
|
|
|
62
62
|
STRING = ("str", "字符串")
|
|
63
63
|
INTEGER = ("int", "整数")
|
|
64
64
|
FLOAT = ("float", "浮点数")
|
|
65
|
+
URI = ("uri", "资源路径/链接")
|
|
66
|
+
ACTION = ("action", "对图像的操作")
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
class Geography(ChoiceEnum):
|
|
@@ -7,7 +7,7 @@ import argparse
|
|
|
7
7
|
import traceback
|
|
8
8
|
|
|
9
9
|
from imgprocessor import VERSION
|
|
10
|
-
from imgprocessor.processor import ProcessParams,
|
|
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, out_path, param_str)
|
|
99
99
|
print(f"{tag}\t 成功", flush=True)
|
|
100
100
|
except Exception as e:
|
|
101
101
|
print(f"{tag}\t \033[31m失败:{e}\033[0m", file=sys.stderr, flush=True)
|
|
@@ -4,7 +4,8 @@ import typing
|
|
|
4
4
|
|
|
5
5
|
from imgprocessor import enums
|
|
6
6
|
from imgprocessor.exceptions import ParamParseException
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
from .base import BaseParser, ImgSaveParser
|
|
8
9
|
from .resize import ResizeParser
|
|
9
10
|
from .crop import CropParser
|
|
10
11
|
from .circle import CircleParser
|
|
@@ -16,16 +17,16 @@ from .watermark import WatermarkParser
|
|
|
16
17
|
from .merge import MergeParser
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
_ACTION_PARASER_MAP: dict[str,
|
|
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
|
|
|
@@ -38,9 +39,9 @@ class ProcessParams(object):
|
|
|
38
39
|
actions: typing.Optional[list] = None,
|
|
39
40
|
**kwargs: typing.Any,
|
|
40
41
|
) -> None:
|
|
41
|
-
self.save_parser: ImgSaveParser = ImgSaveParser.init(kwargs, enable_base64=enable_base64)
|
|
42
|
+
self.save_parser: ImgSaveParser = ImgSaveParser.init(kwargs, enable_base64=enable_base64) # type: ignore
|
|
42
43
|
|
|
43
|
-
_actions = []
|
|
44
|
+
_actions: list[BaseParser] = []
|
|
44
45
|
for i in actions or []:
|
|
45
46
|
key = i.get("key")
|
|
46
47
|
cls = _ACTION_PARASER_MAP.get(key)
|
|
@@ -76,7 +77,7 @@ class ProcessParams(object):
|
|
|
76
77
|
key, param_str = info
|
|
77
78
|
if not key:
|
|
78
79
|
raise ParamParseException(f"参数必须指定操作类型 [{item}]不符合参数要求")
|
|
79
|
-
if key in [enums.OpAction.FORMAT, enums.OpAction.QUALITY, enums.OpAction.INTERLACE]:
|
|
80
|
+
if key in [enums.OpAction.FORMAT.value, enums.OpAction.QUALITY.value, enums.OpAction.INTERLACE.value]:
|
|
80
81
|
save_args.append(f"{key}_{param_str}")
|
|
81
82
|
else:
|
|
82
83
|
action_cls = _ACTION_PARASER_MAP.get(key)
|
|
@@ -10,10 +10,10 @@ from .base import BaseParser, pre_processing
|
|
|
10
10
|
|
|
11
11
|
class AlphaParser(BaseParser):
|
|
12
12
|
|
|
13
|
-
KEY = enums.OpAction.ALPHA
|
|
13
|
+
KEY = enums.OpAction.ALPHA.value
|
|
14
14
|
ARGS = {
|
|
15
15
|
# 不透明度, 为100时,完全不透明,即原图; 为0时,完全透明
|
|
16
|
-
"value": {"type": enums.ArgType.INTEGER, "default": 100, "min": 0, "max": 100},
|
|
16
|
+
"value": {"type": enums.ArgType.INTEGER.value, "default": 100, "min": 0, "max": 100},
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
def __init__(
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf-8
|
|
1
3
|
import typing
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
# python3.11只后
|
|
5
|
-
from typing import Self # type: ignore
|
|
6
|
-
except Exception:
|
|
7
|
-
from typing_extensions import Self
|
|
8
|
-
|
|
5
|
+
import os
|
|
9
6
|
import re
|
|
7
|
+
import tempfile
|
|
8
|
+
import urllib.parse
|
|
9
|
+
from urllib.request import urlretrieve
|
|
10
|
+
from contextlib import contextmanager
|
|
10
11
|
|
|
11
12
|
from PIL import Image, ImageOps
|
|
12
13
|
|
|
13
|
-
from
|
|
14
|
-
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")
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
class BaseParser(object):
|
|
@@ -23,14 +28,14 @@ class BaseParser(object):
|
|
|
23
28
|
pass
|
|
24
29
|
|
|
25
30
|
@classmethod
|
|
26
|
-
def init(cls, data: dict, enable_base64: bool = False) ->
|
|
31
|
+
def init(cls, data: dict, enable_base64: bool = False) -> "BaseParser":
|
|
27
32
|
params = cls.validate_args(enable_base64=enable_base64, **data)
|
|
28
33
|
ins = cls(**params)
|
|
29
34
|
ins.validate()
|
|
30
35
|
return ins
|
|
31
36
|
|
|
32
37
|
@classmethod
|
|
33
|
-
def init_by_str(cls, param_str: str) ->
|
|
38
|
+
def init_by_str(cls, param_str: str) -> "BaseParser":
|
|
34
39
|
data = cls.parse_str(param_str)
|
|
35
40
|
return cls.init(data, enable_base64=True)
|
|
36
41
|
|
|
@@ -64,16 +69,23 @@ class BaseParser(object):
|
|
|
64
69
|
else:
|
|
65
70
|
value = kwargs.get(key)
|
|
66
71
|
try:
|
|
67
|
-
if _type == enums.ArgType.INTEGER:
|
|
72
|
+
if _type == enums.ArgType.INTEGER.value:
|
|
68
73
|
value = cls._validate_number(value, **config)
|
|
69
|
-
elif _type == enums.ArgType.FLOAT:
|
|
74
|
+
elif _type == enums.ArgType.FLOAT.value:
|
|
70
75
|
value = cls._validate_number(value, use_float=True, **config)
|
|
71
|
-
elif _type == enums.ArgType.STRING:
|
|
76
|
+
elif _type == enums.ArgType.STRING.value:
|
|
72
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)
|
|
73
83
|
|
|
74
84
|
choices = config.get("choices")
|
|
85
|
+
if isinstance(choices, ChoiceEnum):
|
|
86
|
+
choices = choices.values
|
|
75
87
|
if choices and value not in choices:
|
|
76
|
-
raise ParamValidateException(f"{key}枚举值只能是其中之一 {choices
|
|
88
|
+
raise ParamValidateException(f"{key}枚举值只能是其中之一 {choices}")
|
|
77
89
|
except ParamValidateException as e:
|
|
78
90
|
raise ParamValidateException(f"参数 {key}={value} 不符合要求:{e}")
|
|
79
91
|
data[key] = value
|
|
@@ -88,7 +100,7 @@ class BaseParser(object):
|
|
|
88
100
|
regex: typing.Optional[str] = None,
|
|
89
101
|
base64_encode: bool = False,
|
|
90
102
|
max_length: typing.Optional[int] = None,
|
|
91
|
-
**kwargs:
|
|
103
|
+
**kwargs: typing.Any,
|
|
92
104
|
) -> str:
|
|
93
105
|
if not isinstance(value, str):
|
|
94
106
|
raise ParamValidateException("参数类型不符合要求,必须是字符串类型")
|
|
@@ -107,7 +119,7 @@ class BaseParser(object):
|
|
|
107
119
|
min: typing.Optional[int] = None,
|
|
108
120
|
max: typing.Optional[int] = None,
|
|
109
121
|
use_float: bool = False,
|
|
110
|
-
**kwargs:
|
|
122
|
+
**kwargs: typing.Any,
|
|
111
123
|
) -> typing.Union[int, float]:
|
|
112
124
|
if isinstance(value, int) or (use_float and isinstance(value, (int, float))):
|
|
113
125
|
v = value
|
|
@@ -131,6 +143,47 @@ class BaseParser(object):
|
|
|
131
143
|
|
|
132
144
|
return v
|
|
133
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
|
+
|
|
134
187
|
@classmethod
|
|
135
188
|
def parse_str(cls, param_str: str) -> dict:
|
|
136
189
|
"""将字符串参数转化为json格式数据
|
|
@@ -192,33 +245,33 @@ def compute_by_geography(
|
|
|
192
245
|
src_w: int, src_h: int, x: int, y: int, w: int, h: int, g: typing.Optional[str], pf: str
|
|
193
246
|
) -> tuple[int, int]:
|
|
194
247
|
"""计算 大小(w,h)的图像相对于(src_w, src_h)图像的原点(x,y)位置"""
|
|
195
|
-
if g == enums.Geography.NW:
|
|
248
|
+
if g == enums.Geography.NW.value:
|
|
196
249
|
x, y = 0, 0
|
|
197
|
-
elif g == enums.Geography.NORTH:
|
|
250
|
+
elif g == enums.Geography.NORTH.value:
|
|
198
251
|
x, y = int(src_w / 2 - w / 2), 0
|
|
199
|
-
elif g == enums.Geography.NE:
|
|
252
|
+
elif g == enums.Geography.NE.value:
|
|
200
253
|
x, y = src_w - w, 0
|
|
201
|
-
elif g == enums.Geography.WEST:
|
|
254
|
+
elif g == enums.Geography.WEST.value:
|
|
202
255
|
x, y = 0, int(src_h / 2 - h / 2)
|
|
203
|
-
elif g == enums.Geography.CENTER:
|
|
256
|
+
elif g == enums.Geography.CENTER.value:
|
|
204
257
|
x, y = int(src_w / 2 - w / 2), int(src_h / 2 - h / 2)
|
|
205
|
-
elif g == enums.Geography.EAST:
|
|
258
|
+
elif g == enums.Geography.EAST.value:
|
|
206
259
|
x, y = src_w - w, int(src_h / 2 - h / 2)
|
|
207
|
-
elif g == enums.Geography.SW:
|
|
260
|
+
elif g == enums.Geography.SW.value:
|
|
208
261
|
x, y = 0, src_h - h
|
|
209
|
-
elif g == enums.Geography.SOUTH:
|
|
262
|
+
elif g == enums.Geography.SOUTH.value:
|
|
210
263
|
x, y = int(src_w / 2 - w / 2), src_h - h
|
|
211
|
-
elif g == enums.Geography.SE:
|
|
264
|
+
elif g == enums.Geography.SE.value:
|
|
212
265
|
x, y = src_w - w, src_h - h
|
|
213
266
|
elif pf:
|
|
214
267
|
if "x" in pf:
|
|
215
268
|
if x < 0 or x > 100:
|
|
216
269
|
raise ParamValidateException(f"pf={pf}包含了x,所以x作为百分比取值范围为[0,100]")
|
|
217
|
-
x =
|
|
270
|
+
x = round(src_w * x / 100)
|
|
218
271
|
if "y" in pf:
|
|
219
272
|
if y < 0 or y > 100:
|
|
220
273
|
raise ParamValidateException(f"pf={pf}包含了y,所以y作为百分比取值范围为[0,100]")
|
|
221
|
-
y =
|
|
274
|
+
y = round(src_h * y / 100)
|
|
222
275
|
return x, y
|
|
223
276
|
|
|
224
277
|
|
|
@@ -237,11 +290,11 @@ def compute_by_ratio(src_w: int, src_h: int, ratio: str) -> tuple[int, int]:
|
|
|
237
290
|
wr, hr = int(w_r), int(h_r)
|
|
238
291
|
if src_w * hr > src_h * wr:
|
|
239
292
|
# 相对于目标比例,宽长了
|
|
240
|
-
w =
|
|
293
|
+
w = round(src_h * wr / hr)
|
|
241
294
|
h = src_h
|
|
242
295
|
elif src_w * hr < src_h * wr:
|
|
243
296
|
w = src_w
|
|
244
|
-
h =
|
|
297
|
+
h = round(src_w * hr / wr)
|
|
245
298
|
else:
|
|
246
299
|
# 刚好符合比例
|
|
247
300
|
w, h = src_w, src_h
|
|
@@ -253,8 +306,8 @@ def compute_splice_two_im(
|
|
|
253
306
|
h1: int,
|
|
254
307
|
w2: int,
|
|
255
308
|
h2: int,
|
|
256
|
-
align: int = enums.PositionAlign.VERTIAL_CENTER,
|
|
257
|
-
order: int = enums.PositionOrder.BEFORE,
|
|
309
|
+
align: int = enums.PositionAlign.VERTIAL_CENTER.value,
|
|
310
|
+
order: int = enums.PositionOrder.BEFORE.value,
|
|
258
311
|
interval: int = 0,
|
|
259
312
|
) -> tuple:
|
|
260
313
|
"""拼接2个图像,计算整体大小和元素原点位置;数值单位都是像素
|
|
@@ -276,33 +329,37 @@ def compute_splice_two_im(
|
|
|
276
329
|
第2个元素的原点位置x2
|
|
277
330
|
第2个元素的原点位置y2
|
|
278
331
|
"""
|
|
279
|
-
if align in [
|
|
332
|
+
if align in [
|
|
333
|
+
enums.PositionAlign.TOP.value,
|
|
334
|
+
enums.PositionAlign.HORIZONTAL_CENTER.value,
|
|
335
|
+
enums.PositionAlign.BOTTOM.value,
|
|
336
|
+
]:
|
|
280
337
|
# 水平顺序
|
|
281
338
|
# 计算整体占位大小w,h
|
|
282
339
|
w, h = w1 + w2 + interval, max(h1, h2)
|
|
283
340
|
|
|
284
|
-
if align == enums.PositionAlign.TOP:
|
|
341
|
+
if align == enums.PositionAlign.TOP.value:
|
|
285
342
|
y1, y2 = 0, 0
|
|
286
|
-
elif align == enums.PositionAlign.BOTTOM:
|
|
343
|
+
elif align == enums.PositionAlign.BOTTOM.value:
|
|
287
344
|
y1, y2 = h - h1, h - h2
|
|
288
345
|
else:
|
|
289
346
|
y1, y2 = int((h - h1) / 2), int((h - h2) / 2)
|
|
290
347
|
|
|
291
|
-
if order == enums.PositionOrder.BEFORE:
|
|
348
|
+
if order == enums.PositionOrder.BEFORE.value:
|
|
292
349
|
x1, x2 = 0, w1 + interval
|
|
293
350
|
else:
|
|
294
351
|
x1, x2 = w2 + interval, 0
|
|
295
352
|
else:
|
|
296
353
|
# 垂直
|
|
297
354
|
w, h = max(w1, w2), h1 + h2 + interval
|
|
298
|
-
if align == enums.PositionAlign.LEFT:
|
|
355
|
+
if align == enums.PositionAlign.LEFT.value:
|
|
299
356
|
x1, x2 = 0, 0
|
|
300
|
-
elif align == enums.PositionAlign.RIGHT:
|
|
357
|
+
elif align == enums.PositionAlign.RIGHT.value:
|
|
301
358
|
x1, x2 = w - w1, w - w2
|
|
302
359
|
else:
|
|
303
360
|
x1, x2 = int((w - w1) / 2), int((w - w2) / 2)
|
|
304
361
|
|
|
305
|
-
if order == enums.PositionOrder.BEFORE:
|
|
362
|
+
if order == enums.PositionOrder.BEFORE.value:
|
|
306
363
|
y1, y2 = 0, h1 + interval
|
|
307
364
|
else:
|
|
308
365
|
y1, y2 = h2 + interval, 0
|
|
@@ -310,13 +367,77 @@ def compute_splice_two_im(
|
|
|
310
367
|
return w, h, x1, y1, x2, y2
|
|
311
368
|
|
|
312
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
|
+
|
|
313
434
|
class ImgSaveParser(BaseParser):
|
|
314
435
|
KEY = ""
|
|
315
436
|
ARGS = {
|
|
316
|
-
"format": {"type": enums.ArgType.STRING, "default": None},
|
|
317
|
-
"quality": {"type": enums.ArgType.INTEGER, "default": None, "min": 1, "max": 100},
|
|
437
|
+
"format": {"type": enums.ArgType.STRING.value, "default": None},
|
|
438
|
+
"quality": {"type": enums.ArgType.INTEGER.value, "default": None, "min": 1, "max": 100},
|
|
318
439
|
# 1 表示将原图设置成渐进显示
|
|
319
|
-
"interlace": {"type": enums.ArgType.INTEGER, "default": 0, "choices": [0, 1]},
|
|
440
|
+
"interlace": {"type": enums.ArgType.INTEGER.value, "default": 0, "choices": [0, 1]},
|
|
320
441
|
}
|
|
321
442
|
|
|
322
443
|
def __init__(
|
|
@@ -10,10 +10,10 @@ from .base import BaseParser, pre_processing
|
|
|
10
10
|
|
|
11
11
|
class BlurParser(BaseParser):
|
|
12
12
|
|
|
13
|
-
KEY = enums.OpAction.BLUR
|
|
13
|
+
KEY = enums.OpAction.BLUR.value
|
|
14
14
|
ARGS = {
|
|
15
15
|
# 模糊半径,值越大,图片越模糊
|
|
16
|
-
"r": {"type": enums.ArgType.INTEGER, "required": True, "min": 1, "max": 50},
|
|
16
|
+
"r": {"type": enums.ArgType.INTEGER.value, "required": True, "min": 1, "max": 50},
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
def __init__(
|
|
@@ -10,9 +10,9 @@ from .base import BaseParser, pre_processing
|
|
|
10
10
|
|
|
11
11
|
class CircleParser(BaseParser):
|
|
12
12
|
|
|
13
|
-
KEY = enums.OpAction.CIRCLE
|
|
13
|
+
KEY = enums.OpAction.CIRCLE.value
|
|
14
14
|
ARGS = {
|
|
15
|
-
"r": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
|
|
15
|
+
"r": {"type": enums.ArgType.INTEGER.value, "default": 0, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
def __init__(
|