py-img-processor 1.0.3__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of py-img-processor might be problematic. Click here for more details.
- {py_img_processor-1.0.3/py_img_processor.egg-info → py_img_processor-1.2.0}/PKG-INFO +13 -7
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/README.md +12 -6
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/__init__.py +5 -1
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/enums.py +4 -2
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/main.py +2 -2
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/parsers/__init__.py +3 -2
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/parsers/base.py +110 -8
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/parsers/crop.py +2 -2
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/parsers/merge.py +38 -14
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/parsers/resize.py +15 -14
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/parsers/watermark.py +10 -9
- py_img_processor-1.2.0/imgprocessor/processor.py +130 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0/py_img_processor.egg-info}/PKG-INFO +13 -7
- py_img_processor-1.0.3/imgprocessor/processor.py +0 -128
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/LICENSE +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/MANIFEST.in +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/exceptions.py +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/parsers/alpha.py +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/parsers/blur.py +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/parsers/circle.py +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/parsers/gray.py +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/parsers/rotate.py +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/imgprocessor/utils.py +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/py_img_processor.egg-info/SOURCES.txt +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/py_img_processor.egg-info/dependency_links.txt +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/py_img_processor.egg-info/entry_points.txt +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/py_img_processor.egg-info/not-zip-safe +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/py_img_processor.egg-info/requires.txt +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/py_img_processor.egg-info/top_level.txt +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/setup.cfg +0 -0
- {py_img_processor-1.0.3 → py_img_processor-1.2.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: py-img-processor
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
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
|
|
@@ -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,8 @@ Image editor using Python and Pillow.
|
|
|
71
71
|
| PROCESSOR_MAX_PIXEL | int | width x height总像素3亿,处理前后的值都被此配置限制 | 300000000 |
|
|
72
72
|
| PROCESSOR_DEFAULT_QUALITY | int | 图像处理后的默认质量 | 75 |
|
|
73
73
|
| PROCESSOR_TEXT_FONT | str | 默认字体文件,默认从系统中寻找;也可以直接传递字体文件路径 | Arial Unicode.ttf |
|
|
74
|
+
| PROCESSOR_WORKSPACES | tuple | 限制水印等资源路径 (startswith匹配), 默认无限制 | `()` |
|
|
75
|
+
| PROCESSOR_ALLOW_DOMAINS | tuple | 限制链接地址域名 (endswith匹配),默认无限制 | `()` |
|
|
74
76
|
|
|
75
77
|
> `注意`:`PROCESSOR_TEXT_FONT` 字体的设置是文字水印必要参数,需保证系统已安装该字体。默认值 `Arial Unicode.ttf` 是MacOS系统存在的字体,建议设置字体文件路径。
|
|
76
78
|
|
|
@@ -82,11 +84,15 @@ Image editor using Python and Pillow.
|
|
|
82
84
|
|
|
83
85
|
|
|
84
86
|
### 处理函数
|
|
85
|
-
|
|
87
|
+
```python
|
|
88
|
+
from imgprocessor.processor import process_image
|
|
89
|
+
|
|
90
|
+
process_image(input_uri, out_path, params)
|
|
91
|
+
```
|
|
86
92
|
|
|
87
93
|
参数说明:
|
|
88
94
|
|
|
89
|
-
- `
|
|
95
|
+
- `input_uri` str,输入图像文件路径或者链接地址
|
|
90
96
|
- `out_path` str, 输出图像保存路径
|
|
91
97
|
- `params` str or json,图像处理参数,参数说明详见 [Reference.md](https://github.com/SkylerHu/py-img-processor/blob/master/docs/Reference.md)
|
|
92
98
|
|
|
@@ -100,9 +106,9 @@ Image editor using Python and Pillow.
|
|
|
100
106
|
|
|
101
107
|
```python
|
|
102
108
|
from imgprocessor.utils import base64url_encode
|
|
103
|
-
from imgprocessor.processor import
|
|
109
|
+
from imgprocessor.processor import process_image
|
|
104
110
|
|
|
105
|
-
|
|
111
|
+
process_image(
|
|
106
112
|
"docs/imgs/lenna-400x225.jpg",
|
|
107
113
|
"/tmp/output.png",
|
|
108
114
|
# 对图片缩放、裁剪、生成圆角、并转成png存储
|
|
@@ -120,7 +126,7 @@ process_image_by_path(
|
|
|
120
126
|
- 其他参数都放在 `actions` 数组中;
|
|
121
127
|
|
|
122
128
|
```python
|
|
123
|
-
|
|
129
|
+
process_image(
|
|
124
130
|
"docs/imgs/lenna-400x225.jpg",
|
|
125
131
|
"/tmp/output.png",
|
|
126
132
|
{
|
|
@@ -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,8 @@ Image editor using Python and Pillow.
|
|
|
45
45
|
| PROCESSOR_MAX_PIXEL | int | width x height总像素3亿,处理前后的值都被此配置限制 | 300000000 |
|
|
46
46
|
| PROCESSOR_DEFAULT_QUALITY | int | 图像处理后的默认质量 | 75 |
|
|
47
47
|
| PROCESSOR_TEXT_FONT | str | 默认字体文件,默认从系统中寻找;也可以直接传递字体文件路径 | Arial Unicode.ttf |
|
|
48
|
+
| PROCESSOR_WORKSPACES | tuple | 限制水印等资源路径 (startswith匹配), 默认无限制 | `()` |
|
|
49
|
+
| PROCESSOR_ALLOW_DOMAINS | tuple | 限制链接地址域名 (endswith匹配),默认无限制 | `()` |
|
|
48
50
|
|
|
49
51
|
> `注意`:`PROCESSOR_TEXT_FONT` 字体的设置是文字水印必要参数,需保证系统已安装该字体。默认值 `Arial Unicode.ttf` 是MacOS系统存在的字体,建议设置字体文件路径。
|
|
50
52
|
|
|
@@ -56,11 +58,15 @@ Image editor using Python and Pillow.
|
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
### 处理函数
|
|
59
|
-
|
|
61
|
+
```python
|
|
62
|
+
from imgprocessor.processor import process_image
|
|
63
|
+
|
|
64
|
+
process_image(input_uri, out_path, params)
|
|
65
|
+
```
|
|
60
66
|
|
|
61
67
|
参数说明:
|
|
62
68
|
|
|
63
|
-
- `
|
|
69
|
+
- `input_uri` str,输入图像文件路径或者链接地址
|
|
64
70
|
- `out_path` str, 输出图像保存路径
|
|
65
71
|
- `params` str or json,图像处理参数,参数说明详见 [Reference.md](./docs/Reference.md)
|
|
66
72
|
|
|
@@ -74,9 +80,9 @@ Image editor using Python and Pillow.
|
|
|
74
80
|
|
|
75
81
|
```python
|
|
76
82
|
from imgprocessor.utils import base64url_encode
|
|
77
|
-
from imgprocessor.processor import
|
|
83
|
+
from imgprocessor.processor import process_image
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
process_image(
|
|
80
86
|
"docs/imgs/lenna-400x225.jpg",
|
|
81
87
|
"/tmp/output.png",
|
|
82
88
|
# 对图片缩放、裁剪、生成圆角、并转成png存储
|
|
@@ -94,7 +100,7 @@ process_image_by_path(
|
|
|
94
100
|
- 其他参数都放在 `actions` 数组中;
|
|
95
101
|
|
|
96
102
|
```python
|
|
97
|
-
|
|
103
|
+
process_image(
|
|
98
104
|
"docs/imgs/lenna-400x225.jpg",
|
|
99
105
|
"/tmp/output.png",
|
|
100
106
|
{
|
|
@@ -5,7 +5,7 @@ import importlib
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
__all__ = ["settings", "VERSION"]
|
|
8
|
-
__version__ = "1.0
|
|
8
|
+
__version__ = "1.2.0"
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
VERSION = __version__
|
|
@@ -41,6 +41,10 @@ class SettingsProxy(object):
|
|
|
41
41
|
PROCESSOR_DEFAULT_QUALITY = 75
|
|
42
42
|
# 默认字体文件; 默认配置了MacOS系统中的字体
|
|
43
43
|
PROCESSOR_TEXT_FONT = "Arial Unicode.ttf"
|
|
44
|
+
# 工作目录列表:例如水印文件必须限制在设定的目录下,避免恶意访问文件
|
|
45
|
+
PROCESSOR_WORKSPACES = ()
|
|
46
|
+
# 当资源文件uri使用链接地址时,限制地址域名来源
|
|
47
|
+
PROCESSOR_ALLOW_DOMAINS = ()
|
|
44
48
|
|
|
45
49
|
def __getattribute__(self, attr: str) -> typing.Any:
|
|
46
50
|
try:
|
|
@@ -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
|
|
@@ -40,7 +41,7 @@ class ProcessParams(object):
|
|
|
40
41
|
) -> None:
|
|
41
42
|
self.save_parser: ImgSaveParser = ImgSaveParser.init(kwargs, enable_base64=enable_base64) # type: ignore
|
|
42
43
|
|
|
43
|
-
_actions = []
|
|
44
|
+
_actions: list[BaseParser] = []
|
|
44
45
|
for i in actions or []:
|
|
45
46
|
key = i.get("key")
|
|
46
47
|
cls = _ACTION_PARASER_MAP.get(key)
|
|
@@ -1,11 +1,20 @@
|
|
|
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
|
|
4
10
|
|
|
5
11
|
from PIL import Image, ImageOps
|
|
6
12
|
|
|
7
|
-
from imgprocessor import enums, utils
|
|
8
|
-
from imgprocessor.exceptions import ParamValidateException, ParamParseException
|
|
13
|
+
from imgprocessor import settings, enums, utils
|
|
14
|
+
from imgprocessor.exceptions import ParamValidateException, ParamParseException, ProcessLimitException
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_ALLOW_SCHEMES = ("http", "https")
|
|
9
18
|
|
|
10
19
|
|
|
11
20
|
class BaseParser(object):
|
|
@@ -64,6 +73,11 @@ class BaseParser(object):
|
|
|
64
73
|
value = cls._validate_number(value, use_float=True, **config)
|
|
65
74
|
elif _type == enums.ArgType.STRING:
|
|
66
75
|
value = cls._validate_str(value, enable_base64=enable_base64, **config)
|
|
76
|
+
elif _type == enums.ArgType.URI:
|
|
77
|
+
value = cls._validate_uri(value, enable_base64=enable_base64, **config)
|
|
78
|
+
elif _type == enums.ArgType.ACTION:
|
|
79
|
+
if value and isinstance(value, str):
|
|
80
|
+
value = cls._validate_str(value, enable_base64=enable_base64, **config)
|
|
67
81
|
|
|
68
82
|
choices = config.get("choices")
|
|
69
83
|
if choices and value not in choices:
|
|
@@ -82,7 +96,7 @@ class BaseParser(object):
|
|
|
82
96
|
regex: typing.Optional[str] = None,
|
|
83
97
|
base64_encode: bool = False,
|
|
84
98
|
max_length: typing.Optional[int] = None,
|
|
85
|
-
**kwargs:
|
|
99
|
+
**kwargs: typing.Any,
|
|
86
100
|
) -> str:
|
|
87
101
|
if not isinstance(value, str):
|
|
88
102
|
raise ParamValidateException("参数类型不符合要求,必须是字符串类型")
|
|
@@ -101,7 +115,7 @@ class BaseParser(object):
|
|
|
101
115
|
min: typing.Optional[int] = None,
|
|
102
116
|
max: typing.Optional[int] = None,
|
|
103
117
|
use_float: bool = False,
|
|
104
|
-
**kwargs:
|
|
118
|
+
**kwargs: typing.Any,
|
|
105
119
|
) -> typing.Union[int, float]:
|
|
106
120
|
if isinstance(value, int) or (use_float and isinstance(value, (int, float))):
|
|
107
121
|
v = value
|
|
@@ -125,6 +139,47 @@ class BaseParser(object):
|
|
|
125
139
|
|
|
126
140
|
return v
|
|
127
141
|
|
|
142
|
+
@classmethod
|
|
143
|
+
def _validate_uri(cls, value: typing.Any, **kwargs: typing.Any) -> str:
|
|
144
|
+
"""校验输入的资源,转换为本地绝对路径
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
value: 输入值
|
|
148
|
+
workspace: 限制资源路径,可传递空字符串忽略校验. Defaults to None.
|
|
149
|
+
allow_domains: 限制资源地址的域名,可传递空数组忽略校验. Defaults to None.
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
ParamValidateException: 参数校验异常
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
系统文件绝对路径
|
|
156
|
+
"""
|
|
157
|
+
# 首先是字符串
|
|
158
|
+
value = cls._validate_str(value, **kwargs)
|
|
159
|
+
ori_value = value
|
|
160
|
+
# 判断是否是链接
|
|
161
|
+
parsed_url = urllib.parse.urlparse(value)
|
|
162
|
+
if parsed_url.scheme not in _ALLOW_SCHEMES:
|
|
163
|
+
value = os.path.realpath(os.fspath(value))
|
|
164
|
+
if not os.path.isfile(value):
|
|
165
|
+
raise ParamValidateException(f"系统文件不存在: {ori_value}")
|
|
166
|
+
|
|
167
|
+
workspaces: tuple = settings.PROCESSOR_WORKSPACES or ()
|
|
168
|
+
_workspace = [os.path.realpath(os.fspath(ws)) for ws in workspaces]
|
|
169
|
+
if _workspace and not value.startswith(tuple(_workspace)):
|
|
170
|
+
raise ParamValidateException(f"文件必须在 PROCESSOR_WORKSPACES={workspaces} 目录下: {ori_value}")
|
|
171
|
+
else:
|
|
172
|
+
# 是链接地址
|
|
173
|
+
domain = parsed_url.netloc
|
|
174
|
+
if not domain:
|
|
175
|
+
raise ParamValidateException(f"链接未解析出域名: {ori_value}")
|
|
176
|
+
allow_domains = settings.PROCESSOR_ALLOW_DOMAINS
|
|
177
|
+
if allow_domains and not parsed_url.netloc.endswith(tuple(allow_domains)):
|
|
178
|
+
raise ParamValidateException(
|
|
179
|
+
f"域名不合法, {parsed_url.netloc} 不在 {allow_domains} 范围内: {ori_value}"
|
|
180
|
+
)
|
|
181
|
+
return value
|
|
182
|
+
|
|
128
183
|
@classmethod
|
|
129
184
|
def parse_str(cls, param_str: str) -> dict:
|
|
130
185
|
"""将字符串参数转化为json格式数据
|
|
@@ -208,11 +263,11 @@ def compute_by_geography(
|
|
|
208
263
|
if "x" in pf:
|
|
209
264
|
if x < 0 or x > 100:
|
|
210
265
|
raise ParamValidateException(f"pf={pf}包含了x,所以x作为百分比取值范围为[0,100]")
|
|
211
|
-
x =
|
|
266
|
+
x = round(src_w * x / 100)
|
|
212
267
|
if "y" in pf:
|
|
213
268
|
if y < 0 or y > 100:
|
|
214
269
|
raise ParamValidateException(f"pf={pf}包含了y,所以y作为百分比取值范围为[0,100]")
|
|
215
|
-
y =
|
|
270
|
+
y = round(src_h * y / 100)
|
|
216
271
|
return x, y
|
|
217
272
|
|
|
218
273
|
|
|
@@ -231,11 +286,11 @@ def compute_by_ratio(src_w: int, src_h: int, ratio: str) -> tuple[int, int]:
|
|
|
231
286
|
wr, hr = int(w_r), int(h_r)
|
|
232
287
|
if src_w * hr > src_h * wr:
|
|
233
288
|
# 相对于目标比例,宽长了
|
|
234
|
-
w =
|
|
289
|
+
w = round(src_h * wr / hr)
|
|
235
290
|
h = src_h
|
|
236
291
|
elif src_w * hr < src_h * wr:
|
|
237
292
|
w = src_w
|
|
238
|
-
h =
|
|
293
|
+
h = round(src_w * hr / wr)
|
|
239
294
|
else:
|
|
240
295
|
# 刚好符合比例
|
|
241
296
|
w, h = src_w, src_h
|
|
@@ -304,6 +359,53 @@ def compute_splice_two_im(
|
|
|
304
359
|
return w, h, x1, y1, x2, y2
|
|
305
360
|
|
|
306
361
|
|
|
362
|
+
def validate_ori_im(ori_im: Image) -> None:
|
|
363
|
+
src_w, src_h = ori_im.size
|
|
364
|
+
if src_w > settings.PROCESSOR_MAX_W_H or src_h > settings.PROCESSOR_MAX_W_H:
|
|
365
|
+
raise ProcessLimitException(
|
|
366
|
+
f"图像宽和高单边像素不能超过{settings.PROCESSOR_MAX_W_H}像素,输入图像({src_w}, {src_h})"
|
|
367
|
+
)
|
|
368
|
+
if src_w * src_h > settings.PROCESSOR_MAX_PIXEL:
|
|
369
|
+
raise ProcessLimitException(f"图像总像素不可超过{settings.PROCESSOR_MAX_PIXEL}像素,输入图像({src_w}, {src_h})")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def trans_uri_to_im(uri: str) -> Image:
|
|
373
|
+
"""将输入资源转换成Image对象
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
uri: 文件路径 或者 可下载的链接地址
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
ProcessLimitException: 处理图像大小/像素限制
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Image对象
|
|
383
|
+
"""
|
|
384
|
+
parsed_url = urllib.parse.urlparse(uri)
|
|
385
|
+
if parsed_url.scheme in _ALLOW_SCHEMES:
|
|
386
|
+
with tempfile.NamedTemporaryFile() as fp:
|
|
387
|
+
# 输入值计算md5作为文件名;重复地址本地若存在不下载多次
|
|
388
|
+
urlretrieve(uri, filename=fp.name)
|
|
389
|
+
fp.seek(0)
|
|
390
|
+
|
|
391
|
+
size = os.path.getsize(fp.name)
|
|
392
|
+
if size > settings.PROCESSOR_MAX_FILE_SIZE * 1024 * 1024:
|
|
393
|
+
raise ProcessLimitException(f"图像文件大小不得超过{settings.PROCESSOR_MAX_FILE_SIZE}MB")
|
|
394
|
+
|
|
395
|
+
ori_im = Image.open(fp)
|
|
396
|
+
validate_ori_im(ori_im)
|
|
397
|
+
# 解决临时文件close后im对象不能正常使用得问题
|
|
398
|
+
ori_im = ori_im.copy()
|
|
399
|
+
else:
|
|
400
|
+
size = os.path.getsize(uri)
|
|
401
|
+
if size > settings.PROCESSOR_MAX_FILE_SIZE * 1024 * 1024:
|
|
402
|
+
raise ProcessLimitException(f"图像文件大小不得超过{settings.PROCESSOR_MAX_FILE_SIZE}MB")
|
|
403
|
+
ori_im = Image.open(uri)
|
|
404
|
+
validate_ori_im(ori_im)
|
|
405
|
+
|
|
406
|
+
return ori_im
|
|
407
|
+
|
|
408
|
+
|
|
307
409
|
class ImgSaveParser(BaseParser):
|
|
308
410
|
KEY = ""
|
|
309
411
|
ARGS = {
|
|
@@ -64,14 +64,14 @@ class CropParser(BaseParser):
|
|
|
64
64
|
if "w" in pf:
|
|
65
65
|
if w < 0 or w > 100:
|
|
66
66
|
raise ParamValidateException(f"pf={pf}包含了w,所以w作为百分比取值范围为[0,100]")
|
|
67
|
-
w =
|
|
67
|
+
w = round(src_w * w / 100)
|
|
68
68
|
elif not w:
|
|
69
69
|
w = src_w
|
|
70
70
|
|
|
71
71
|
if "h" in pf:
|
|
72
72
|
if h < 0 or h > 100:
|
|
73
73
|
raise ParamValidateException(f"pf={pf}包含了h,所以h作为百分比取值范围为[0,100]")
|
|
74
|
-
h =
|
|
74
|
+
h = round(src_h * h / 100)
|
|
75
75
|
elif not h:
|
|
76
76
|
h = src_h
|
|
77
77
|
|
|
@@ -5,7 +5,8 @@ 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 BaseParser, pre_processing, compute_by_geography, compute_splice_two_im, trans_uri_to_im
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class MergeParser(BaseParser):
|
|
@@ -13,11 +14,13 @@ class MergeParser(BaseParser):
|
|
|
13
14
|
KEY = enums.OpAction.MERGE
|
|
14
15
|
ARGS = {
|
|
15
16
|
# 要处理的图片
|
|
16
|
-
"image": {"type": enums.ArgType.
|
|
17
|
+
"image": {"type": enums.ArgType.URI, "required": True, "base64_encode": True},
|
|
17
18
|
# 对image的处理参数
|
|
18
|
-
"
|
|
19
|
+
"actions": {"type": enums.ArgType.ACTION, "base64_encode": True},
|
|
19
20
|
# 使用输入图像的大小作为参照进行缩放
|
|
20
21
|
"p": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": 1000},
|
|
22
|
+
# 是否将imgae当做背景放在输入图像之下; 定义输入图像和image参数的拼接顺序
|
|
23
|
+
"bg": {"type": enums.ArgType.INTEGER, "default": 0, "choices": [0, 1]},
|
|
21
24
|
# 对齐方式
|
|
22
25
|
"order": {"type": enums.ArgType.INTEGER, "choices": enums.PositionOrder},
|
|
23
26
|
"align": {"type": enums.ArgType.INTEGER, "default": enums.PositionAlign.BOTTOM, "choices": enums.PositionAlign},
|
|
@@ -30,38 +33,52 @@ class MergeParser(BaseParser):
|
|
|
30
33
|
# 拼接后大小包含2个图像,空白区域使用color颜色填充
|
|
31
34
|
"color": {
|
|
32
35
|
"type": enums.ArgType.STRING,
|
|
33
|
-
"default": "
|
|
36
|
+
"default": "0000", # 为了保证透明背景
|
|
34
37
|
"regex": r"^([0-9a-fA-F]{6}|[0-9a-fA-F]{8}|[0-9a-fA-F]{3,4})$",
|
|
35
38
|
},
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
def __init__(
|
|
39
42
|
self,
|
|
40
|
-
image:
|
|
41
|
-
|
|
43
|
+
image: str = "",
|
|
44
|
+
actions: typing.Union[str, list] = "",
|
|
42
45
|
p: int = 0,
|
|
43
46
|
order: typing.Optional[int] = None,
|
|
44
47
|
align: int = 2,
|
|
45
48
|
interval: int = 0,
|
|
49
|
+
bg: int = 0,
|
|
46
50
|
g: typing.Optional[str] = None,
|
|
47
51
|
x: int = 0,
|
|
48
52
|
y: int = 0,
|
|
49
53
|
pf: str = "",
|
|
50
|
-
color: str = "
|
|
54
|
+
color: str = "0000",
|
|
51
55
|
**kwargs: typing.Any,
|
|
52
56
|
) -> None:
|
|
53
57
|
self.image = image
|
|
54
|
-
self.action = action
|
|
55
58
|
self.p = p
|
|
56
59
|
self.order = order
|
|
57
60
|
self.align = align
|
|
58
61
|
self.interval = interval
|
|
62
|
+
self.bg = bg
|
|
59
63
|
self.g = g
|
|
60
64
|
self.x = x
|
|
61
65
|
self.y = y
|
|
62
66
|
self.pf = pf
|
|
63
67
|
self.color = color
|
|
64
68
|
|
|
69
|
+
self.actions: list[BaseParser] = []
|
|
70
|
+
if actions:
|
|
71
|
+
from imgprocessor.processor import ProcessParams
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
if isinstance(actions, str):
|
|
75
|
+
params = ProcessParams.parse_str(actions)
|
|
76
|
+
else:
|
|
77
|
+
params = ProcessParams(actions=actions)
|
|
78
|
+
self.actions = params.actions
|
|
79
|
+
except ParamValidateException as e:
|
|
80
|
+
raise ParamValidateException(f"merage操作中actions参数校验异常,其中 {e}")
|
|
81
|
+
|
|
65
82
|
def compute(self, src_w: int, src_h: int, w2: int, h2: int) -> tuple:
|
|
66
83
|
if self.order in enums.PositionOrder: # type: ignore
|
|
67
84
|
order = typing.cast(int, self.order)
|
|
@@ -92,18 +109,25 @@ class MergeParser(BaseParser):
|
|
|
92
109
|
im = pre_processing(im, use_alpha=True)
|
|
93
110
|
src_w, src_h = im.size
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
# 处理要合并的图像
|
|
113
|
+
im2 = trans_uri_to_im(self.image)
|
|
96
114
|
im2 = pre_processing(im2, use_alpha=True)
|
|
97
|
-
if self.
|
|
98
|
-
from imgprocessor.processor import
|
|
115
|
+
if self.actions:
|
|
116
|
+
from imgprocessor.processor import ProcessorCtr
|
|
99
117
|
|
|
100
|
-
|
|
101
|
-
im2 = handle_img_actions(im2, params.actions)
|
|
118
|
+
im2 = ProcessorCtr.handle_img_actions(im2, self.actions)
|
|
102
119
|
if self.p:
|
|
103
|
-
w2, h2 =
|
|
120
|
+
w2, h2 = round(src_w * self.p / 100), round(src_h * self.p / 100)
|
|
104
121
|
im2 = im2.resize((w2, h2), resample=Image.LANCZOS)
|
|
122
|
+
|
|
123
|
+
if self.bg:
|
|
124
|
+
# 调整拼接顺序
|
|
125
|
+
im, im2 = im2, im
|
|
126
|
+
|
|
127
|
+
src_w, src_h = im.size
|
|
105
128
|
w2, h2 = im2.size
|
|
106
129
|
|
|
130
|
+
# 计算合并像素点
|
|
107
131
|
w, h, x1, y1, x2, y2 = self.compute(src_w, src_h, w2, h2)
|
|
108
132
|
out = Image.new("RGBA", (w, h), color=f"#{self.color}")
|
|
109
133
|
out.paste(im, (x1, y1), im)
|
|
@@ -20,7 +20,7 @@ class ResizeParser(BaseParser):
|
|
|
20
20
|
"limit": {"type": enums.ArgType.INTEGER, "default": 1, "choices": [0, 1]},
|
|
21
21
|
"color": {
|
|
22
22
|
"type": enums.ArgType.STRING,
|
|
23
|
-
"default": "FFFFFF",
|
|
23
|
+
"default": "FFFFFF", # 默认白色
|
|
24
24
|
"regex": r"^([0-9a-fA-F]{6}|[0-9a-fA-F]{8}|[0-9a-fA-F]{3,4})$",
|
|
25
25
|
},
|
|
26
26
|
"p": {"type": enums.ArgType.INTEGER, "default": 0, "min": 1, "max": 1000},
|
|
@@ -57,45 +57,46 @@ class ResizeParser(BaseParser):
|
|
|
57
57
|
# w,h按指定的即可,无需计算
|
|
58
58
|
w, h = self.w, self.h
|
|
59
59
|
elif self.m == enums.ResizeMode.MFIT:
|
|
60
|
+
# 低版本Pillow未实现 ImageOps.cover 方法,自行处理
|
|
60
61
|
# 等比缩放
|
|
61
62
|
if self.w and self.h:
|
|
62
63
|
# 指定w与h的矩形外的最小图像
|
|
63
64
|
if self.w / self.h > src_w / src_h:
|
|
64
|
-
w, h = self.w,
|
|
65
|
+
w, h = self.w, round(self.w * src_h / src_w)
|
|
65
66
|
else:
|
|
66
|
-
w, h =
|
|
67
|
+
w, h = round(self.h * src_w / src_h), self.h
|
|
67
68
|
elif self.w:
|
|
68
|
-
w, h = self.w,
|
|
69
|
+
w, h = self.w, round(self.w * src_h / src_w)
|
|
69
70
|
else:
|
|
70
|
-
w, h =
|
|
71
|
+
w, h = round(self.h * src_w / src_h), self.h
|
|
71
72
|
else:
|
|
72
73
|
# 默认 enums.ResizeMode.LFIT
|
|
73
74
|
# 等比缩放
|
|
74
75
|
if self.w and self.h:
|
|
75
76
|
# 指定w与h的矩形内的最大图像
|
|
76
77
|
if self.w / self.h > src_w / src_h:
|
|
77
|
-
w, h =
|
|
78
|
+
w, h = round(self.h * src_w / src_h), self.h
|
|
78
79
|
else:
|
|
79
|
-
w, h = self.w,
|
|
80
|
+
w, h = self.w, round(self.w * src_h / src_w)
|
|
80
81
|
elif self.w:
|
|
81
|
-
w, h = self.w,
|
|
82
|
+
w, h = self.w, round(self.w * src_h / src_w)
|
|
82
83
|
else:
|
|
83
|
-
w, h =
|
|
84
|
+
w, h = round(self.h * src_w / src_h), self.h
|
|
84
85
|
elif self.l:
|
|
85
86
|
# 按最长边缩放
|
|
86
87
|
if src_w > src_h:
|
|
87
|
-
w, h = self.l,
|
|
88
|
+
w, h = self.l, round(src_h * self.l / src_w)
|
|
88
89
|
else:
|
|
89
|
-
w, h =
|
|
90
|
+
w, h = round(src_w * self.l / src_h), self.l
|
|
90
91
|
elif self.s:
|
|
91
92
|
# 按最短边缩放
|
|
92
93
|
if src_w > src_h:
|
|
93
|
-
w, h =
|
|
94
|
+
w, h = round(src_w * self.s / src_h), self.s
|
|
94
95
|
else:
|
|
95
|
-
w, h = self.s,
|
|
96
|
+
w, h = self.s, round(src_h * self.s / src_w)
|
|
96
97
|
elif self.p:
|
|
97
98
|
# 按照比例缩放
|
|
98
|
-
w, h =
|
|
99
|
+
w, h = round(src_w * self.p / 100), round(src_h * self.p / 100)
|
|
99
100
|
else:
|
|
100
101
|
# 缺少参数
|
|
101
102
|
raise ParamValidateException("resize操作缺少合法参数")
|
|
@@ -6,7 +6,7 @@ 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 BaseParser, pre_processing, compute_splice_two_im, compute_by_geography
|
|
9
|
+
from .base import BaseParser, pre_processing, compute_splice_two_im, compute_by_geography, trans_uri_to_im
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class WatermarkParser(BaseParser):
|
|
@@ -25,12 +25,13 @@ class WatermarkParser(BaseParser):
|
|
|
25
25
|
"padx": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 4096},
|
|
26
26
|
"pady": {"type": enums.ArgType.INTEGER, "default": 0, "min": 0, "max": 4096},
|
|
27
27
|
# 图片水印路径
|
|
28
|
-
"image": {"type": enums.ArgType.
|
|
28
|
+
"image": {"type": enums.ArgType.URI, "base64_encode": True},
|
|
29
29
|
# 水印的原始设计参照尺寸,会根据原图大小缩放水印
|
|
30
30
|
"design": {"type": enums.ArgType.INTEGER, "min": 1, "max": settings.PROCESSOR_MAX_W_H},
|
|
31
31
|
# 文字
|
|
32
32
|
"text": {"type": enums.ArgType.STRING, "base64_encode": True, "max_length": 64},
|
|
33
33
|
"font": {"type": enums.ArgType.STRING, "base64_encode": True},
|
|
34
|
+
# 文字默认黑色
|
|
34
35
|
"color": {"type": enums.ArgType.STRING, "default": "000000", "regex": "^([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$"},
|
|
35
36
|
"size": {"type": enums.ArgType.INTEGER, "default": 40, "min": 1, "max": 1000},
|
|
36
37
|
# 文字水印的阴影透明度, 0表示没有阴影
|
|
@@ -99,7 +100,7 @@ class WatermarkParser(BaseParser):
|
|
|
99
100
|
w1, h1, w2, h2 = 0, 0, 0, 0
|
|
100
101
|
icon = None
|
|
101
102
|
if self.image:
|
|
102
|
-
icon =
|
|
103
|
+
icon = trans_uri_to_im(self.image)
|
|
103
104
|
icon = pre_processing(icon, use_alpha=True)
|
|
104
105
|
if not self.text:
|
|
105
106
|
# 没有文字,直接返回
|
|
@@ -132,7 +133,7 @@ class WatermarkParser(BaseParser):
|
|
|
132
133
|
|
|
133
134
|
# 阴影要单独处理透明度,放在文字之前处理
|
|
134
135
|
if self.shadow:
|
|
135
|
-
offset = max(
|
|
136
|
+
offset = max(round(self.size / 20), 2)
|
|
136
137
|
shadow_color = "#000000"
|
|
137
138
|
# 左上到右下的阴影,只保留这一个
|
|
138
139
|
draw.text((x2 + offset, y2 + offset), self.text, font=font, fill=shadow_color)
|
|
@@ -140,7 +141,7 @@ class WatermarkParser(BaseParser):
|
|
|
140
141
|
# draw.text((x2 + offset, y2 - offset), self.text, font=font, fill=shadow_color)
|
|
141
142
|
# draw.text((x2 - offset, y2 - offset), self.text, font=font, fill=shadow_color)
|
|
142
143
|
_, _, _, alpha_channel = mark.split()
|
|
143
|
-
alpha_channel = alpha_channel.point(lambda i: min(
|
|
144
|
+
alpha_channel = alpha_channel.point(lambda i: min(round(255 * self.shadow / 100), i))
|
|
144
145
|
mark.putalpha(alpha_channel)
|
|
145
146
|
|
|
146
147
|
# 处理文字
|
|
@@ -168,7 +169,7 @@ class WatermarkParser(BaseParser):
|
|
|
168
169
|
# 处理缩放
|
|
169
170
|
rate = min(src_w, src_h) / self.design
|
|
170
171
|
if rate != 1:
|
|
171
|
-
w, h =
|
|
172
|
+
w, h = round(w * rate), round(h * rate)
|
|
172
173
|
mark = mark.resize((w, h), resample=Image.LANCZOS)
|
|
173
174
|
|
|
174
175
|
if 0 < self.rotate < 360:
|
|
@@ -180,17 +181,17 @@ class WatermarkParser(BaseParser):
|
|
|
180
181
|
if w > src_w or h > src_h:
|
|
181
182
|
# 水印大小超过原图了, 原图矩形内的最大图像
|
|
182
183
|
if w / h > src_w / src_h:
|
|
183
|
-
w, h = src_w,
|
|
184
|
+
w, h = src_w, round(src_w * h / w)
|
|
184
185
|
self.x = 0
|
|
185
186
|
else:
|
|
186
|
-
w, h =
|
|
187
|
+
w, h = round(src_h * w / h), src_h
|
|
187
188
|
self.y = 0
|
|
188
189
|
mark = mark.resize((w, h), resample=Image.LANCZOS)
|
|
189
190
|
|
|
190
191
|
if self.t < 100:
|
|
191
192
|
# 处理透明度
|
|
192
193
|
_, _, _, alpha_channel = mark.split()
|
|
193
|
-
alpha_channel = alpha_channel.point(lambda i: min(
|
|
194
|
+
alpha_channel = alpha_channel.point(lambda i: min(round(255 * self.t / 100), i))
|
|
194
195
|
mark.putalpha(alpha_channel)
|
|
195
196
|
|
|
196
197
|
# 计算位置,粘贴水印
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
import typing
|
|
4
|
+
import tempfile
|
|
5
|
+
import colorsys
|
|
6
|
+
|
|
7
|
+
from PIL import Image, ImageOps
|
|
8
|
+
|
|
9
|
+
from imgprocessor import enums, settings
|
|
10
|
+
from imgprocessor.parsers import BaseParser, ProcessParams
|
|
11
|
+
from imgprocessor.parsers.base import trans_uri_to_im
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProcessorCtr(object):
|
|
15
|
+
|
|
16
|
+
def __init__(self, input_uri: str, out_path: str, params: typing.Union[ProcessParams, dict, str]) -> None:
|
|
17
|
+
# 输入文件检验路径
|
|
18
|
+
self.input_uri = BaseParser._validate_uri(input_uri)
|
|
19
|
+
self.out_path = out_path
|
|
20
|
+
# 初始化处理参数
|
|
21
|
+
if isinstance(params, dict):
|
|
22
|
+
params = ProcessParams(**params)
|
|
23
|
+
elif isinstance(params, str):
|
|
24
|
+
params = ProcessParams.parse_str(params)
|
|
25
|
+
params = typing.cast(ProcessParams, params)
|
|
26
|
+
self.params = params
|
|
27
|
+
|
|
28
|
+
def run(self) -> typing.Optional[typing.ByteString]:
|
|
29
|
+
# 初始化输入
|
|
30
|
+
ori_im = trans_uri_to_im(self.input_uri)
|
|
31
|
+
# 处理图像
|
|
32
|
+
im = self.handle_img_actions(ori_im, self.params.actions)
|
|
33
|
+
# 输出、保存
|
|
34
|
+
kwargs = self.params.save_parser.compute(ori_im, im)
|
|
35
|
+
return self.save_img_to_file(im, out_path=self.out_path, **kwargs)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def handle_img_actions(cls, ori_im: Image, actions: list[BaseParser]) -> Image:
|
|
39
|
+
im = ori_im
|
|
40
|
+
# 解决旋转问题
|
|
41
|
+
im = ImageOps.exif_transpose(im)
|
|
42
|
+
for parser in actions:
|
|
43
|
+
im = parser.do_action(im)
|
|
44
|
+
return im
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def save_img_to_file(
|
|
48
|
+
cls,
|
|
49
|
+
im: Image,
|
|
50
|
+
out_path: typing.Optional[str] = None,
|
|
51
|
+
**kwargs: typing.Any,
|
|
52
|
+
) -> typing.Optional[typing.ByteString]:
|
|
53
|
+
fmt = kwargs.get("format") or im.format
|
|
54
|
+
kwargs["format"] = fmt
|
|
55
|
+
|
|
56
|
+
if fmt.upper() == enums.ImageFormat.JPEG and im.mode == "RGBA":
|
|
57
|
+
im = im.convert("RGB")
|
|
58
|
+
|
|
59
|
+
if not kwargs.get("quality"):
|
|
60
|
+
if fmt.upper() == enums.ImageFormat.JPEG and im.format == enums.ImageFormat.JPEG:
|
|
61
|
+
kwargs["quality"] = "keep"
|
|
62
|
+
else:
|
|
63
|
+
kwargs["quality"] = settings.PROCESSOR_DEFAULT_QUALITY
|
|
64
|
+
|
|
65
|
+
if out_path:
|
|
66
|
+
# icc_profile 是为解决色域的问题
|
|
67
|
+
im.save(out_path, **kwargs)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# 没有传递保存的路径,返回文件内容
|
|
71
|
+
suffix = fmt or "png"
|
|
72
|
+
with tempfile.NamedTemporaryFile(suffix=f".{suffix}") as fp:
|
|
73
|
+
im.save(fp.name, **kwargs)
|
|
74
|
+
fp.seek(0)
|
|
75
|
+
content = fp.read()
|
|
76
|
+
return content
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def process_image(
|
|
80
|
+
input_uri: str, out_path: str, params: typing.Union[ProcessParams, dict, str]
|
|
81
|
+
) -> typing.Optional[typing.ByteString]:
|
|
82
|
+
"""处理图像
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
input_uri: 输入图像路径
|
|
86
|
+
out_path: 输出图像保存路径
|
|
87
|
+
params: 图像处理参数
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ProcessLimitException: 超过处理限制会抛出异常
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
默认输出直接存储无返回,仅当out_path为空时会返回处理后图像的二进制内容
|
|
94
|
+
"""
|
|
95
|
+
ctr = ProcessorCtr(input_uri, out_path, params)
|
|
96
|
+
return ctr.run()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def extract_main_color(img_path: str, delta_h: float = 0.3) -> str:
|
|
100
|
+
"""获取图像主色调
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
img_path: 输入图像的路径
|
|
104
|
+
delta_h: 像素色相和平均色相做减法的绝对值小于该值,才用于计算主色调,取值范围[0,1]
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
颜色值,eg: FFFFFF
|
|
108
|
+
"""
|
|
109
|
+
r, g, b = 0, 0, 0
|
|
110
|
+
im = Image.open(img_path)
|
|
111
|
+
if im.mode != "RGB":
|
|
112
|
+
im = im.convert("RGB")
|
|
113
|
+
# 转换成HSV即 色相(Hue)、饱和度(Saturation)、明度(alue),取值范围[0,1]
|
|
114
|
+
# 取H计算平均色相
|
|
115
|
+
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])]
|
|
116
|
+
avg_h = sum(all_h) / (im.size[0] * im.size[1])
|
|
117
|
+
# 取与平均色相相近的像素色值rgb用于计算,像素值取值范围[0,255]
|
|
118
|
+
beyond = list(
|
|
119
|
+
filter(
|
|
120
|
+
lambda x: abs(colorsys.rgb_to_hsv(*x)[0] - avg_h) < delta_h,
|
|
121
|
+
[im.getpixel((x, y)) for x in range(im.size[0]) for y in range(im.size[1])],
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
if len(beyond):
|
|
125
|
+
r = int(sum(e[0] for e in beyond) / len(beyond))
|
|
126
|
+
g = int(sum(e[1] for e in beyond) / len(beyond))
|
|
127
|
+
b = int(sum(e[2] for e in beyond) / len(beyond))
|
|
128
|
+
|
|
129
|
+
color = "{}{}{}".format(hex(r)[2:].zfill(2), hex(g)[2:].zfill(2), hex(b)[2:].zfill(2))
|
|
130
|
+
return color.upper()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: py-img-processor
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
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
|
|
@@ -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,8 @@ Image editor using Python and Pillow.
|
|
|
71
71
|
| PROCESSOR_MAX_PIXEL | int | width x height总像素3亿,处理前后的值都被此配置限制 | 300000000 |
|
|
72
72
|
| PROCESSOR_DEFAULT_QUALITY | int | 图像处理后的默认质量 | 75 |
|
|
73
73
|
| PROCESSOR_TEXT_FONT | str | 默认字体文件,默认从系统中寻找;也可以直接传递字体文件路径 | Arial Unicode.ttf |
|
|
74
|
+
| PROCESSOR_WORKSPACES | tuple | 限制水印等资源路径 (startswith匹配), 默认无限制 | `()` |
|
|
75
|
+
| PROCESSOR_ALLOW_DOMAINS | tuple | 限制链接地址域名 (endswith匹配),默认无限制 | `()` |
|
|
74
76
|
|
|
75
77
|
> `注意`:`PROCESSOR_TEXT_FONT` 字体的设置是文字水印必要参数,需保证系统已安装该字体。默认值 `Arial Unicode.ttf` 是MacOS系统存在的字体,建议设置字体文件路径。
|
|
76
78
|
|
|
@@ -82,11 +84,15 @@ Image editor using Python and Pillow.
|
|
|
82
84
|
|
|
83
85
|
|
|
84
86
|
### 处理函数
|
|
85
|
-
|
|
87
|
+
```python
|
|
88
|
+
from imgprocessor.processor import process_image
|
|
89
|
+
|
|
90
|
+
process_image(input_uri, out_path, params)
|
|
91
|
+
```
|
|
86
92
|
|
|
87
93
|
参数说明:
|
|
88
94
|
|
|
89
|
-
- `
|
|
95
|
+
- `input_uri` str,输入图像文件路径或者链接地址
|
|
90
96
|
- `out_path` str, 输出图像保存路径
|
|
91
97
|
- `params` str or json,图像处理参数,参数说明详见 [Reference.md](https://github.com/SkylerHu/py-img-processor/blob/master/docs/Reference.md)
|
|
92
98
|
|
|
@@ -100,9 +106,9 @@ Image editor using Python and Pillow.
|
|
|
100
106
|
|
|
101
107
|
```python
|
|
102
108
|
from imgprocessor.utils import base64url_encode
|
|
103
|
-
from imgprocessor.processor import
|
|
109
|
+
from imgprocessor.processor import process_image
|
|
104
110
|
|
|
105
|
-
|
|
111
|
+
process_image(
|
|
106
112
|
"docs/imgs/lenna-400x225.jpg",
|
|
107
113
|
"/tmp/output.png",
|
|
108
114
|
# 对图片缩放、裁剪、生成圆角、并转成png存储
|
|
@@ -120,7 +126,7 @@ process_image_by_path(
|
|
|
120
126
|
- 其他参数都放在 `actions` 数组中;
|
|
121
127
|
|
|
122
128
|
```python
|
|
123
|
-
|
|
129
|
+
process_image(
|
|
124
130
|
"docs/imgs/lenna-400x225.jpg",
|
|
125
131
|
"/tmp/output.png",
|
|
126
132
|
{
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
# coding=utf-8
|
|
3
|
-
import typing
|
|
4
|
-
import os
|
|
5
|
-
import tempfile
|
|
6
|
-
import colorsys
|
|
7
|
-
|
|
8
|
-
from PIL import Image, ImageOps
|
|
9
|
-
|
|
10
|
-
from imgprocessor import settings, enums
|
|
11
|
-
from imgprocessor.exceptions import ProcessLimitException
|
|
12
|
-
from imgprocessor.parsers import BaseParser, ProcessParams
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def handle_img_actions(ori_im: Image, actions: list[BaseParser]) -> Image:
|
|
16
|
-
src_w, src_h = ori_im.size
|
|
17
|
-
if src_w > settings.PROCESSOR_MAX_W_H or src_h > settings.PROCESSOR_MAX_W_H:
|
|
18
|
-
raise ProcessLimitException(
|
|
19
|
-
f"图像宽和高单边像素不能超过{settings.PROCESSOR_MAX_W_H}像素,输入图像({src_w}, {src_h})"
|
|
20
|
-
)
|
|
21
|
-
if src_w * src_h > settings.PROCESSOR_MAX_PIXEL:
|
|
22
|
-
raise ProcessLimitException(f"图像总像素不可超过{settings.PROCESSOR_MAX_PIXEL}像素,输入图像({src_w}, {src_h})")
|
|
23
|
-
|
|
24
|
-
im = ori_im
|
|
25
|
-
im = ImageOps.exif_transpose(im)
|
|
26
|
-
|
|
27
|
-
for parser in actions:
|
|
28
|
-
im = parser.do_action(im)
|
|
29
|
-
|
|
30
|
-
return im
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def save_img_to_file(
|
|
34
|
-
im: Image,
|
|
35
|
-
out_path: typing.Optional[str] = None,
|
|
36
|
-
**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
|
-
) -> typing.Optional[typing.ByteString]:
|
|
67
|
-
"""处理图像
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
input_path: 输入图像文件路径
|
|
71
|
-
out_path: 输出图像保存路径
|
|
72
|
-
params: 图像处理参数
|
|
73
|
-
|
|
74
|
-
Raises:
|
|
75
|
-
ProcessLimitException: 超过处理限制会抛出异常
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
默认输出直接存储无返回,仅当out_path为空时会返回处理后图像的二进制内容
|
|
79
|
-
"""
|
|
80
|
-
size = os.path.getsize(input_path)
|
|
81
|
-
if size > settings.PROCESSOR_MAX_FILE_SIZE * 1024 * 1024:
|
|
82
|
-
raise ProcessLimitException(f"图像文件大小不得超过{settings.PROCESSOR_MAX_FILE_SIZE}MB")
|
|
83
|
-
if isinstance(params, dict):
|
|
84
|
-
params = ProcessParams(**params)
|
|
85
|
-
elif isinstance(params, str):
|
|
86
|
-
params = ProcessParams.parse_str(params)
|
|
87
|
-
params = typing.cast(ProcessParams, params)
|
|
88
|
-
|
|
89
|
-
ori_im = Image.open(input_path)
|
|
90
|
-
# 处理图像
|
|
91
|
-
im = handle_img_actions(ori_im, params.actions)
|
|
92
|
-
|
|
93
|
-
kwargs = params.save_parser.compute(ori_im, im)
|
|
94
|
-
return save_img_to_file(im, out_path=out_path, **kwargs)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def extract_main_color(img_path: str, delta_h: float = 0.3) -> str:
|
|
98
|
-
"""获取图像主色调
|
|
99
|
-
|
|
100
|
-
Args:
|
|
101
|
-
img_path: 输入图像的路径
|
|
102
|
-
delta_h: 像素色相和平均色相做减法的绝对值小于该值,才用于计算主色调,取值范围[0,1]
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
颜色值,eg: FFFFFF
|
|
106
|
-
"""
|
|
107
|
-
r, g, b = 0, 0, 0
|
|
108
|
-
im = Image.open(img_path)
|
|
109
|
-
if im.mode != "RGB":
|
|
110
|
-
im = im.convert("RGB")
|
|
111
|
-
# 转换成HSV即 色相(Hue)、饱和度(Saturation)、明度(alue),取值范围[0,1]
|
|
112
|
-
# 取H计算平均色相
|
|
113
|
-
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])]
|
|
114
|
-
avg_h = sum(all_h) / (im.size[0] * im.size[1])
|
|
115
|
-
# 取与平均色相相近的像素色值rgb用于计算,像素值取值范围[0,255]
|
|
116
|
-
beyond = list(
|
|
117
|
-
filter(
|
|
118
|
-
lambda x: abs(colorsys.rgb_to_hsv(*x)[0] - avg_h) < delta_h,
|
|
119
|
-
[im.getpixel((x, y)) for x in range(im.size[0]) for y in range(im.size[1])],
|
|
120
|
-
)
|
|
121
|
-
)
|
|
122
|
-
if len(beyond):
|
|
123
|
-
r = int(sum(e[0] for e in beyond) / len(beyond))
|
|
124
|
-
g = int(sum(e[1] for e in beyond) / len(beyond))
|
|
125
|
-
b = int(sum(e[2] for e in beyond) / len(beyond))
|
|
126
|
-
|
|
127
|
-
color = "{}{}{}".format(hex(r)[2:].zfill(2), hex(g)[2:].zfill(2), hex(b)[2:].zfill(2))
|
|
128
|
-
return color.upper()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_img_processor-1.0.3 → py_img_processor-1.2.0}/py_img_processor.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{py_img_processor-1.0.3 → py_img_processor-1.2.0}/py_img_processor.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|