pyxllib 0.3.197__py3-none-any.whl → 3.201.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyxllib/__init__.py +14 -21
- pyxllib/algo/__init__.py +8 -8
- pyxllib/algo/disjoint.py +54 -54
- pyxllib/algo/geo.py +537 -541
- pyxllib/algo/intervals.py +964 -964
- pyxllib/algo/matcher.py +389 -389
- pyxllib/algo/newbie.py +166 -166
- pyxllib/algo/pupil.py +629 -629
- pyxllib/algo/shapelylib.py +67 -67
- pyxllib/algo/specialist.py +241 -241
- pyxllib/algo/stat.py +494 -494
- pyxllib/algo/treelib.py +145 -149
- pyxllib/algo/unitlib.py +62 -66
- pyxllib/autogui/__init__.py +5 -5
- pyxllib/autogui/activewin.py +246 -246
- pyxllib/autogui/all.py +9 -9
- pyxllib/autogui/autogui.py +846 -852
- pyxllib/autogui/uiautolib.py +362 -362
- pyxllib/autogui/virtualkey.py +102 -102
- pyxllib/autogui/wechat.py +827 -827
- pyxllib/autogui/wechat_msg.py +421 -421
- pyxllib/autogui/wxautolib.py +84 -84
- pyxllib/cv/__init__.py +5 -5
- pyxllib/cv/expert.py +267 -267
- pyxllib/cv/imfile.py +159 -159
- pyxllib/cv/imhash.py +39 -39
- pyxllib/cv/pupil.py +9 -9
- pyxllib/cv/rgbfmt.py +1525 -1525
- pyxllib/cv/slidercaptcha.py +137 -137
- pyxllib/cv/trackbartools.py +251 -251
- pyxllib/cv/xlcvlib.py +1040 -1040
- pyxllib/cv/xlpillib.py +423 -423
- pyxllib/data/echarts.py +236 -240
- pyxllib/data/jsonlib.py +85 -89
- pyxllib/data/oss.py +72 -72
- pyxllib/data/pglib.py +1111 -1127
- pyxllib/data/sqlite.py +568 -568
- pyxllib/data/sqllib.py +297 -297
- pyxllib/ext/JLineViewer.py +505 -505
- pyxllib/ext/__init__.py +6 -6
- pyxllib/ext/demolib.py +251 -246
- pyxllib/ext/drissionlib.py +277 -277
- pyxllib/ext/kq5034lib.py +12 -12
- pyxllib/ext/qt.py +449 -449
- pyxllib/ext/robustprocfile.py +493 -497
- pyxllib/ext/seleniumlib.py +76 -76
- pyxllib/ext/tk.py +173 -173
- pyxllib/ext/unixlib.py +821 -827
- pyxllib/ext/utools.py +345 -351
- pyxllib/ext/webhook.py +124 -119
- pyxllib/ext/win32lib.py +40 -40
- pyxllib/ext/wjxlib.py +91 -88
- pyxllib/ext/wpsapi.py +124 -124
- pyxllib/ext/xlwork.py +9 -9
- pyxllib/ext/yuquelib.py +1110 -1105
- pyxllib/file/__init__.py +17 -17
- pyxllib/file/docxlib.py +757 -761
- pyxllib/file/gitlib.py +309 -309
- pyxllib/file/libreoffice.py +165 -165
- pyxllib/file/movielib.py +144 -148
- pyxllib/file/newbie.py +10 -10
- pyxllib/file/onenotelib.py +1469 -1469
- pyxllib/file/packlib/__init__.py +330 -330
- pyxllib/file/packlib/zipfile.py +2441 -2441
- pyxllib/file/pdflib.py +422 -426
- pyxllib/file/pupil.py +185 -185
- pyxllib/file/specialist/__init__.py +681 -685
- pyxllib/file/specialist/dirlib.py +799 -799
- pyxllib/file/specialist/download.py +193 -193
- pyxllib/file/specialist/filelib.py +2825 -2829
- pyxllib/file/xlsxlib.py +3122 -3131
- pyxllib/file/xlsyncfile.py +341 -341
- pyxllib/prog/__init__.py +5 -5
- pyxllib/prog/cachetools.py +58 -64
- pyxllib/prog/deprecatedlib.py +233 -233
- pyxllib/prog/filelock.py +42 -42
- pyxllib/prog/ipyexec.py +253 -253
- pyxllib/prog/multiprogs.py +940 -940
- pyxllib/prog/newbie.py +451 -451
- pyxllib/prog/pupil.py +1208 -1197
- pyxllib/prog/sitepackages.py +33 -33
- pyxllib/prog/specialist/__init__.py +348 -391
- pyxllib/prog/specialist/bc.py +203 -203
- pyxllib/prog/specialist/browser.py +497 -497
- pyxllib/prog/specialist/common.py +347 -347
- pyxllib/prog/specialist/datetime.py +198 -198
- pyxllib/prog/specialist/tictoc.py +240 -240
- pyxllib/prog/specialist/xllog.py +180 -180
- pyxllib/prog/xlosenv.py +110 -108
- pyxllib/stdlib/__init__.py +17 -17
- pyxllib/stdlib/tablepyxl/__init__.py +10 -10
- pyxllib/stdlib/tablepyxl/style.py +303 -303
- pyxllib/stdlib/tablepyxl/tablepyxl.py +130 -130
- pyxllib/text/__init__.py +8 -8
- pyxllib/text/ahocorasick.py +36 -39
- pyxllib/text/airscript.js +754 -744
- pyxllib/text/charclasslib.py +121 -121
- pyxllib/text/jiebalib.py +267 -267
- pyxllib/text/jinjalib.py +27 -32
- pyxllib/text/jsa_ai_prompt.md +271 -271
- pyxllib/text/jscode.py +922 -922
- pyxllib/text/latex/__init__.py +158 -158
- pyxllib/text/levenshtein.py +303 -303
- pyxllib/text/nestenv.py +1215 -1215
- pyxllib/text/newbie.py +300 -300
- pyxllib/text/pupil/__init__.py +8 -8
- pyxllib/text/pupil/common.py +1121 -1121
- pyxllib/text/pupil/xlalign.py +326 -326
- pyxllib/text/pycode.py +47 -47
- pyxllib/text/specialist/__init__.py +8 -8
- pyxllib/text/specialist/common.py +112 -112
- pyxllib/text/specialist/ptag.py +186 -186
- pyxllib/text/spellchecker.py +172 -172
- pyxllib/text/templates/echart_base.html +10 -10
- pyxllib/text/templates/highlight_code.html +16 -16
- pyxllib/text/templates/latex_editor.html +102 -102
- pyxllib/text/vbacode.py +17 -17
- pyxllib/text/xmllib.py +741 -747
- pyxllib/xl.py +42 -39
- pyxllib/xlcv.py +17 -17
- pyxllib-3.201.1.dist-info/METADATA +296 -0
- pyxllib-3.201.1.dist-info/RECORD +125 -0
- {pyxllib-0.3.197.dist-info → pyxllib-3.201.1.dist-info}/licenses/LICENSE +190 -190
- pyxllib/ext/old.py +0 -663
- pyxllib-0.3.197.dist-info/METADATA +0 -48
- pyxllib-0.3.197.dist-info/RECORD +0 -126
- {pyxllib-0.3.197.dist-info → pyxllib-3.201.1.dist-info}/WHEEL +0 -0
pyxllib/cv/xlpillib.py
CHANGED
@@ -1,423 +1,423 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
|
-
# @Author : 陈坤泽
|
4
|
-
# @Email : 877362867@qq.com
|
5
|
-
# @Date : 2021/08/25 15:57
|
6
|
-
|
7
|
-
"""
|
8
|
-
图方便的时候,PilImg和CvImg可以进行图片类型转换,然后互相引用彼此已有的一个实现版本
|
9
|
-
为了性能的时候,则尽量减少各种绕弯,使用最源生的代码来实现
|
10
|
-
"""
|
11
|
-
|
12
|
-
import base64
|
13
|
-
import io
|
14
|
-
import os
|
15
|
-
import random
|
16
|
-
|
17
|
-
import cv2
|
18
|
-
import numpy as np
|
19
|
-
import PIL.ExifTags
|
20
|
-
import PIL.Image
|
21
|
-
import PIL.ImageOps
|
22
|
-
import requests
|
23
|
-
|
24
|
-
try:
|
25
|
-
import accimage
|
26
|
-
except ImportError:
|
27
|
-
accimage = None
|
28
|
-
|
29
|
-
from pyxllib.prog.pupil import inject_members
|
30
|
-
from pyxllib.file.specialist import XlPath, get_font_file
|
31
|
-
from pyxllib.cv.xlcvlib import xlcv
|
32
|
-
|
33
|
-
|
34
|
-
class PilImg(PIL.Image.Image):
|
35
|
-
|
36
|
-
def __1_read(self):
|
37
|
-
pass
|
38
|
-
|
39
|
-
@classmethod
|
40
|
-
def read(cls, file, flags=None, *, apply_exif_orientation=False, **kwargs) -> 'PilImg':
|
41
|
-
if PilImg.is_pil_image(file):
|
42
|
-
im = file
|
43
|
-
elif xlcv.is_cv2_image(file):
|
44
|
-
im = xlcv.to_pil_image(file)
|
45
|
-
elif XlPath.safe_init(file):
|
46
|
-
im = PIL.Image.open(str(file), **kwargs)
|
47
|
-
else:
|
48
|
-
raise TypeError(f'类型错误或文件不存在:{type(file)} {file}')
|
49
|
-
if apply_exif_orientation:
|
50
|
-
im = PilImg.apply_exif_orientation(im)
|
51
|
-
return PilImg.cvt_channel(im, flags)
|
52
|
-
|
53
|
-
@classmethod
|
54
|
-
def read_from_buffer(cls, buffer, flags=None, *, b64decode=False):
|
55
|
-
""" 先用opencv实现,以后可以再研究PIL.Image.frombuffer是否有更快处理策略 """
|
56
|
-
if b64decode:
|
57
|
-
buffer = base64.b64decode(buffer)
|
58
|
-
im = PIL.Image.open(io.BytesIO(buffer))
|
59
|
-
return PilImg.cvt_channel(im, flags)
|
60
|
-
|
61
|
-
@classmethod
|
62
|
-
def read_from_url(cls, url, flags=None, *, b64decode=False):
|
63
|
-
content = requests.get(url).content
|
64
|
-
return PilImg.read_from_buffer(content, flags, b64decode=b64decode)
|
65
|
-
|
66
|
-
def __2_attrs(self):
|
67
|
-
pass
|
68
|
-
|
69
|
-
def imsize(self):
|
70
|
-
return self.size[::-1]
|
71
|
-
|
72
|
-
def n_channels(self):
|
73
|
-
""" 通道数 """
|
74
|
-
return len(self.getbands())
|
75
|
-
|
76
|
-
def __3_write(self):
|
77
|
-
pass
|
78
|
-
|
79
|
-
def to_cv2_image(self):
|
80
|
-
""" pil图片转np图片 """
|
81
|
-
y = np.array(self)
|
82
|
-
y = cv2.cvtColor(y, cv2.COLOR_BGR2RGB) if y.size else None
|
83
|
-
return y
|
84
|
-
|
85
|
-
def is_pil_image(self):
|
86
|
-
if accimage is not None:
|
87
|
-
return isinstance(self, (PIL.Image.Image, accimage.Image))
|
88
|
-
else:
|
89
|
-
return isinstance(self, PIL.Image.Image)
|
90
|
-
|
91
|
-
def write(self, path, *, if_exists=None, **kwargs):
|
92
|
-
p = XlPath(path)
|
93
|
-
if p.exist_preprcs(if_exists):
|
94
|
-
os.makedirs(p.parent, exist_ok=True)
|
95
|
-
suffix = p.suffix[1:]
|
96
|
-
if suffix.lower() == 'jpg':
|
97
|
-
suffix = 'jpeg'
|
98
|
-
if self.mode in ('RGBA', 'P') and suffix == 'jpeg':
|
99
|
-
im = self.convert('RGB')
|
100
|
-
else:
|
101
|
-
im = self
|
102
|
-
im.save(str(p), suffix, **kwargs)
|
103
|
-
|
104
|
-
def cvt_channel(self, flags=None):
|
105
|
-
im = self
|
106
|
-
if flags is None or flags == -1: return im
|
107
|
-
n_c = im.n_channels
|
108
|
-
if flags == 0 and n_c > 1:
|
109
|
-
im = im.convert('L')
|
110
|
-
elif flags == 1 and n_c != 3:
|
111
|
-
im = im.convert('RGB')
|
112
|
-
return im
|
113
|
-
|
114
|
-
def to_buffer(self, ext='.jpg', *, b64encode=False):
|
115
|
-
# 主要是偷懒,不想重写一遍,就直接去调用cv版本的实现了
|
116
|
-
return xlcv.to_buffer(self.to_cv2_image(), ext, b64encode=b64encode)
|
117
|
-
|
118
|
-
def display(self):
|
119
|
-
""" 在jupyter中展示 """
|
120
|
-
try:
|
121
|
-
from IPython.display import display
|
122
|
-
display(self)
|
123
|
-
except ModuleNotFoundError:
|
124
|
-
pass
|
125
|
-
|
126
|
-
def __4_plot(self):
|
127
|
-
pass
|
128
|
-
|
129
|
-
def plot_border(self, border=1, fill='black'):
|
130
|
-
""" 给图片加上边框
|
131
|
-
|
132
|
-
Args:
|
133
|
-
self:
|
134
|
-
border: 边框的厚度
|
135
|
-
fill: 边框颜色
|
136
|
-
|
137
|
-
Returns: 一张新图
|
138
|
-
"""
|
139
|
-
from PIL import ImageOps
|
140
|
-
im2 = ImageOps.expand(self, border=border, fill=fill)
|
141
|
-
return im2
|
142
|
-
|
143
|
-
def plot_text(self, text, xy=None, font_size=10, font_type='simfang.ttf', **kwargs):
|
144
|
-
"""
|
145
|
-
:param xy: 写入文本的起始坐标,没写入则自动写在垂直居中位置
|
146
|
-
"""
|
147
|
-
from PIL import ImageFont, ImageDraw
|
148
|
-
font_file = get_font_file(font_type)
|
149
|
-
font = ImageFont.truetype(font=str(font_file), size=font_size, encoding="utf-8")
|
150
|
-
draw = ImageDraw.Draw(self)
|
151
|
-
if xy is None:
|
152
|
-
w, h = font_getsize(font, text)
|
153
|
-
xy = ((self.size[0] - w) / 2, (self.size[1] - h) / 2)
|
154
|
-
draw.text(xy, text, font=font, **kwargs)
|
155
|
-
return self
|
156
|
-
|
157
|
-
def __5_resize(self):
|
158
|
-
pass
|
159
|
-
|
160
|
-
def reduce_area(self, area):
|
161
|
-
""" 根据面积上限缩小图片
|
162
|
-
|
163
|
-
即图片面积超过area时,按照等比例缩小到面积为area的图片
|
164
|
-
"""
|
165
|
-
im = self
|
166
|
-
h, w = PilImg.imsize(im)
|
167
|
-
s = h * w
|
168
|
-
if s > area:
|
169
|
-
r = (area / s) ** 0.5
|
170
|
-
size = int(r * h), int(r * w)
|
171
|
-
im = PilImg.resize2(im, size)
|
172
|
-
return im
|
173
|
-
|
174
|
-
def resize2(self, size, **kwargs):
|
175
|
-
"""
|
176
|
-
:param size: 默认是 (w, h), 这里我倒过来 (h, w)
|
177
|
-
但计算机领域,确实经常都是用 (w, h) 的格式,毕竟横轴是x,纵轴才是y
|
178
|
-
:param kwargs:
|
179
|
-
resample=3,插值算法;有PIL.Image.NEAREST, ~BOX, ~BILINEAR, ~HAMMING, ~BICUBIC, ~LANCZOS等
|
180
|
-
默认是 PIL.Image.BICUBIC;如果mode是"1"或"P"模式,则总是 PIL.Image.NEAREST
|
181
|
-
|
182
|
-
>>> im = read(np.zeros([100, 200], dtype='uint8'), 0)
|
183
|
-
>>> self.size
|
184
|
-
(100, 200)
|
185
|
-
>>> im2 = im.reduce_area(50*50, **kwargs)
|
186
|
-
>>> im2.size
|
187
|
-
(35, 70)
|
188
|
-
"""
|
189
|
-
# 注意pil图像尺寸接口都是[w,h],跟标准的[h,w]相反
|
190
|
-
return self.resize(size[::-1], **kwargs)
|
191
|
-
|
192
|
-
def evaluate_image_file_size(self, suffix='.jpeg'):
|
193
|
-
""" 评估图像存成文件后的大小
|
194
|
-
|
195
|
-
:param suffix: 使用的图片类型
|
196
|
-
:return int: 存储后的文件大小,单位为字节
|
197
|
-
"""
|
198
|
-
im = self
|
199
|
-
|
200
|
-
# save接口不支持jpg参数
|
201
|
-
if suffix[0] == '.':
|
202
|
-
suffix = suffix[1:]
|
203
|
-
if suffix.lower() == 'jpg':
|
204
|
-
suffix = 'jpeg'
|
205
|
-
|
206
|
-
file = io.BytesIO()
|
207
|
-
if im.mode in ('RGBA', 'P') and suffix == 'jpeg':
|
208
|
-
im = im.convert('RGB')
|
209
|
-
im.save(file, suffix)
|
210
|
-
return len(file.getvalue())
|
211
|
-
|
212
|
-
def reduce_filesize(self, filesize=None, suffix='.jpeg'):
|
213
|
-
""" 按照保存后的文件大小来压缩im
|
214
|
-
|
215
|
-
:param filesize: 单位Bytes
|
216
|
-
int, 可以用 300*1024 来表示 300KB
|
217
|
-
None, 可以不输入,默认读取后按原尺寸返回,这样看似没变化,其实图片一读一写,是会对手机拍照的很多大图进行压缩的
|
218
|
-
|
219
|
-
>> reduce_filesize(im, 300*1024, 'jpg')
|
220
|
-
"""
|
221
|
-
im = self
|
222
|
-
# 循环处理
|
223
|
-
while filesize:
|
224
|
-
r = xlpil.evaluate_image_file_size(im, suffix) / filesize
|
225
|
-
if r <= 1:
|
226
|
-
break
|
227
|
-
|
228
|
-
# 假设图片面积和文件大小成正比,如果r=4,表示长宽要各减小至1/(r**0.5)才能到目标文件大小
|
229
|
-
rate = min(1 / (r ** 0.5), 0.95) # 并且限制每轮至少要缩小至95%,避免可能会迭代太多轮
|
230
|
-
im = im.resize((int(im.size[0] * rate), int(im.size[1] * rate)))
|
231
|
-
return im
|
232
|
-
|
233
|
-
def trim(self, *, border=0, color=None):
|
234
|
-
""" 默认裁剪掉白色边缘,可以配合 get_backgroup_color 裁剪掉背景色
|
235
|
-
|
236
|
-
:param border: 上下左右保留多少边缘
|
237
|
-
输入一个整数,表示统一留边
|
238
|
-
也可以输入[int, int, int, int],分别表示left top right bottom留白
|
239
|
-
:param color: 要裁剪的颜色,这是精确值,没有误差,如果需要模糊,可以提前预处理成精确值
|
240
|
-
这个默认值设成None,本来是想说支持灰度图,此时输入一个255的值就好
|
241
|
-
但是pil的灰度图机制好像有些不太一样,总之目前默认值还是自动会设成 (255, 255, 255)
|
242
|
-
:param percent-background: TODO 控制裁剪百分比上限
|
243
|
-
"""
|
244
|
-
from PIL import Image, ImageChops
|
245
|
-
from pyxllib.algo.geo import xywh2ltrb, ltrb2xywh, ltrb_border
|
246
|
-
|
247
|
-
if color is None:
|
248
|
-
color = (255, 255, 255)
|
249
|
-
else:
|
250
|
-
color = tuple(color)
|
251
|
-
|
252
|
-
# 如果图片通道数跟预设的color不同,要断言
|
253
|
-
assert self.n_channels() == len(color), f'图片通道数{self.n_channels}跟预设的color{color}不同'
|
254
|
-
|
255
|
-
im = self
|
256
|
-
bg = Image.new(im.mode, im.size, color)
|
257
|
-
diff = ImageChops.difference(im, bg)
|
258
|
-
bbox = diff.getbbox() # 如果im跟bg一样,也就是裁"消失"了,此时bbox值为None
|
259
|
-
if bbox:
|
260
|
-
if border:
|
261
|
-
ltrb = xywh2ltrb(bbox)
|
262
|
-
ltrb = ltrb_border(ltrb, border, im.size)
|
263
|
-
bbox = ltrb2xywh(ltrb)
|
264
|
-
im = im.crop(bbox)
|
265
|
-
return im
|
266
|
-
|
267
|
-
def __6_warp(self):
|
268
|
-
pass
|
269
|
-
|
270
|
-
def __x_other(self):
|
271
|
-
pass
|
272
|
-
|
273
|
-
def random_direction(self):
|
274
|
-
""" 假设原图片是未旋转的状态0
|
275
|
-
|
276
|
-
顺时针转90度是label=1,顺时针转180度是label2 ...
|
277
|
-
"""
|
278
|
-
im = self
|
279
|
-
label = np.random.randint(4)
|
280
|
-
if label == 1:
|
281
|
-
# PIL的旋转角度,是指逆时针角度;但是我这里编号是顺时针
|
282
|
-
im = im.transpose(PIL.Image.ROTATE_270)
|
283
|
-
elif label == 2:
|
284
|
-
im = im.transpose(PIL.Image.ROTATE_180)
|
285
|
-
elif label == 3:
|
286
|
-
im = im.transpose(PIL.Image.ROTATE_90)
|
287
|
-
return im, label
|
288
|
-
|
289
|
-
def flip_direction(self, direction):
|
290
|
-
"""
|
291
|
-
:param direction: 顺时针旋转几个90度
|
292
|
-
标记现在图片是哪个方向:0是正常,1是向右翻转,2是向下翻转,3是向左翻转
|
293
|
-
"""
|
294
|
-
im = self
|
295
|
-
direction = direction % 4
|
296
|
-
if direction:
|
297
|
-
im = im.transpose({1: PIL.Image.ROTATE_270,
|
298
|
-
2: PIL.Image.ROTATE_180,
|
299
|
-
3: PIL.Image.ROTATE_90}[direction])
|
300
|
-
return im
|
301
|
-
|
302
|
-
def apply_exif_orientation(self):
|
303
|
-
""" 摆正图片角度
|
304
|
-
|
305
|
-
Image.open读取图片时,是手机严格正放时拍到的图片效果,
|
306
|
-
但手机拍照时是会记录旋转位置的,即可以判断是物理空间中,实际朝上、朝下的方向,
|
307
|
-
从而识别出正拍(代号1),顺时针旋转90度拍摄(代号8),顺时针180度拍摄(代号3),顺时针270度拍摄(代号6)。
|
308
|
-
windows上的图片查阅软件能识别方向代号后正确摆放;
|
309
|
-
为了让python处理图片的时候能增加这个属性的考虑,这个函数能修正识别角度返回新的图片。
|
310
|
-
|
311
|
-
我自己写过个版本,后来发现 labelme.utils.image 写过功能更强的,抄了过来~~
|
312
|
-
"""
|
313
|
-
im = self
|
314
|
-
try:
|
315
|
-
exif = im._getexif()
|
316
|
-
except AttributeError:
|
317
|
-
exif = None
|
318
|
-
|
319
|
-
if exif is None:
|
320
|
-
return im
|
321
|
-
|
322
|
-
exif = {
|
323
|
-
PIL.ExifTags.TAGS[k]: v
|
324
|
-
for k, v in exif.items()
|
325
|
-
if k in PIL.ExifTags.TAGS
|
326
|
-
}
|
327
|
-
|
328
|
-
orientation = exif.get("Orientation", None)
|
329
|
-
|
330
|
-
if orientation == 1:
|
331
|
-
# do nothing
|
332
|
-
return im
|
333
|
-
elif orientation == 2:
|
334
|
-
# left-to-right mirror
|
335
|
-
return PIL.ImageOps.mirror(im)
|
336
|
-
elif orientation == 3:
|
337
|
-
# rotate 180
|
338
|
-
return im.transpose(PIL.Image.ROTATE_180)
|
339
|
-
elif orientation == 4:
|
340
|
-
# top-to-bottom mirror
|
341
|
-
return PIL.ImageOps.flip(im)
|
342
|
-
elif orientation == 5:
|
343
|
-
# top-to-left mirror
|
344
|
-
return PIL.ImageOps.mirror(im.transpose(PIL.Image.ROTATE_270))
|
345
|
-
elif orientation == 6:
|
346
|
-
# rotate 270
|
347
|
-
return im.transpose(PIL.Image.ROTATE_270)
|
348
|
-
elif orientation == 7:
|
349
|
-
# top-to-right mirror
|
350
|
-
return PIL.ImageOps.mirror(im.transpose(PIL.Image.ROTATE_90))
|
351
|
-
elif orientation == 8:
|
352
|
-
# rotate 90
|
353
|
-
return im.transpose(PIL.Image.ROTATE_90)
|
354
|
-
else:
|
355
|
-
return im
|
356
|
-
|
357
|
-
def get_exif(self):
|
358
|
-
""" 旧函数名:查看图片的Exif信息 """
|
359
|
-
exif_data = self._getexif()
|
360
|
-
if exif_data:
|
361
|
-
exif = {PIL.ExifTags.TAGS[k]: v for k, v in exif_data.items() if k in PIL.ExifTags.TAGS}
|
362
|
-
else:
|
363
|
-
exif = None
|
364
|
-
return exif
|
365
|
-
|
366
|
-
def rgba2rgb(self):
|
367
|
-
if self.mode in ('RGBA', 'P'):
|
368
|
-
# 判断图片mode模式,如果是RGBA或P等可能有透明底,则和一个白底图片合成去除透明底
|
369
|
-
background = PIL.Image.new('RGBA', self.size, (255, 255, 255))
|
370
|
-
# composite是合成的意思。将右图的alpha替换为左图内容
|
371
|
-
self = PIL.Image.alpha_composite(background, self.convert('RGBA')).convert('RGB')
|
372
|
-
return self
|
373
|
-
|
374
|
-
def keep_subtitles(self, judge_func=None, trim_color=(255, 255, 255)):
|
375
|
-
im = self.to_cv2_image()
|
376
|
-
im = im.keep_subtitles(judge_func=judge_func, trim_color=trim_color)
|
377
|
-
return im.to_pil_image()
|
378
|
-
|
379
|
-
|
380
|
-
# pil相比cv,由于无法类似CvImg这样新建一个和np.ndarray等效的类,所以还是比较支持嵌入到Image中直接操作
|
381
|
-
inject_members(PilImg, PIL.Image.Image)
|
382
|
-
|
383
|
-
xlpil = PilImg # 与xlcv对称
|
384
|
-
|
385
|
-
|
386
|
-
def font_getsize(font, text):
|
387
|
-
""" 官方自带的font.getsize遇到换行符的text,计算不准确
|
388
|
-
"""
|
389
|
-
texts = text.split('\n')
|
390
|
-
sizes = [font.getsize(t) for t in texts]
|
391
|
-
w = max([w for w, h in sizes])
|
392
|
-
h = sum([h for w, h in sizes])
|
393
|
-
return w, h
|
394
|
-
|
395
|
-
|
396
|
-
def create_text_image(text, size=None, *, xy=None, font_size=14, bg_color=None, text_color=None, **kwargs):
|
397
|
-
""" 生成文字图片
|
398
|
-
|
399
|
-
:param size: 注意我这里顺序是 (height, width)
|
400
|
-
默认None,根据写入的文字动态生成图片大小
|
401
|
-
:param bg_color: 背景图颜色,如 (0, 0, 0)
|
402
|
-
默认None,随机颜色
|
403
|
-
:param text_color: 文本颜色,如 (255, 255, 255)
|
404
|
-
默认None,随机颜色
|
405
|
-
"""
|
406
|
-
if size is None:
|
407
|
-
from PIL import ImageFont, ImageDraw
|
408
|
-
font_file = get_font_file(kwargs.get('font_type', 'simfang.ttf'))
|
409
|
-
font = ImageFont.truetype(font=str(font_file), size=font_size, encoding="utf-8")
|
410
|
-
w, h = font_getsize(font, text)
|
411
|
-
size = (h + 10, w + 10)
|
412
|
-
xy = (5, 0) # 自动生成尺寸的情况下,文本从左上角开始写
|
413
|
-
|
414
|
-
if bg_color is None:
|
415
|
-
bg_color = tuple([random.randint(0, 255) for i in range(3)])
|
416
|
-
if text_color is None:
|
417
|
-
text_color = tuple([random.randint(0, 255) for i in range(3)])
|
418
|
-
|
419
|
-
h, w = size
|
420
|
-
im: PilImg = PIL.Image.new('RGB', (w, h), tuple(bg_color))
|
421
|
-
im2 = im.plot_text(text, xy=xy, font_size=font_size, fill=tuple(text_color), **kwargs)
|
422
|
-
|
423
|
-
return im2
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# @Author : 陈坤泽
|
4
|
+
# @Email : 877362867@qq.com
|
5
|
+
# @Date : 2021/08/25 15:57
|
6
|
+
|
7
|
+
"""
|
8
|
+
图方便的时候,PilImg和CvImg可以进行图片类型转换,然后互相引用彼此已有的一个实现版本
|
9
|
+
为了性能的时候,则尽量减少各种绕弯,使用最源生的代码来实现
|
10
|
+
"""
|
11
|
+
|
12
|
+
import base64
|
13
|
+
import io
|
14
|
+
import os
|
15
|
+
import random
|
16
|
+
|
17
|
+
import cv2
|
18
|
+
import numpy as np
|
19
|
+
import PIL.ExifTags
|
20
|
+
import PIL.Image
|
21
|
+
import PIL.ImageOps
|
22
|
+
import requests
|
23
|
+
|
24
|
+
try:
|
25
|
+
import accimage
|
26
|
+
except ImportError:
|
27
|
+
accimage = None
|
28
|
+
|
29
|
+
from pyxllib.prog.pupil import inject_members
|
30
|
+
from pyxllib.file.specialist import XlPath, get_font_file
|
31
|
+
from pyxllib.cv.xlcvlib import xlcv
|
32
|
+
|
33
|
+
|
34
|
+
class PilImg(PIL.Image.Image):
|
35
|
+
|
36
|
+
def __1_read(self):
|
37
|
+
pass
|
38
|
+
|
39
|
+
@classmethod
|
40
|
+
def read(cls, file, flags=None, *, apply_exif_orientation=False, **kwargs) -> 'PilImg':
|
41
|
+
if PilImg.is_pil_image(file):
|
42
|
+
im = file
|
43
|
+
elif xlcv.is_cv2_image(file):
|
44
|
+
im = xlcv.to_pil_image(file)
|
45
|
+
elif XlPath.safe_init(file):
|
46
|
+
im = PIL.Image.open(str(file), **kwargs)
|
47
|
+
else:
|
48
|
+
raise TypeError(f'类型错误或文件不存在:{type(file)} {file}')
|
49
|
+
if apply_exif_orientation:
|
50
|
+
im = PilImg.apply_exif_orientation(im)
|
51
|
+
return PilImg.cvt_channel(im, flags)
|
52
|
+
|
53
|
+
@classmethod
|
54
|
+
def read_from_buffer(cls, buffer, flags=None, *, b64decode=False):
|
55
|
+
""" 先用opencv实现,以后可以再研究PIL.Image.frombuffer是否有更快处理策略 """
|
56
|
+
if b64decode:
|
57
|
+
buffer = base64.b64decode(buffer)
|
58
|
+
im = PIL.Image.open(io.BytesIO(buffer))
|
59
|
+
return PilImg.cvt_channel(im, flags)
|
60
|
+
|
61
|
+
@classmethod
|
62
|
+
def read_from_url(cls, url, flags=None, *, b64decode=False):
|
63
|
+
content = requests.get(url).content
|
64
|
+
return PilImg.read_from_buffer(content, flags, b64decode=b64decode)
|
65
|
+
|
66
|
+
def __2_attrs(self):
|
67
|
+
pass
|
68
|
+
|
69
|
+
def imsize(self):
|
70
|
+
return self.size[::-1]
|
71
|
+
|
72
|
+
def n_channels(self):
|
73
|
+
""" 通道数 """
|
74
|
+
return len(self.getbands())
|
75
|
+
|
76
|
+
def __3_write(self):
|
77
|
+
pass
|
78
|
+
|
79
|
+
def to_cv2_image(self):
|
80
|
+
""" pil图片转np图片 """
|
81
|
+
y = np.array(self)
|
82
|
+
y = cv2.cvtColor(y, cv2.COLOR_BGR2RGB) if y.size else None
|
83
|
+
return y
|
84
|
+
|
85
|
+
def is_pil_image(self):
|
86
|
+
if accimage is not None:
|
87
|
+
return isinstance(self, (PIL.Image.Image, accimage.Image))
|
88
|
+
else:
|
89
|
+
return isinstance(self, PIL.Image.Image)
|
90
|
+
|
91
|
+
def write(self, path, *, if_exists=None, **kwargs):
|
92
|
+
p = XlPath(path)
|
93
|
+
if p.exist_preprcs(if_exists):
|
94
|
+
os.makedirs(p.parent, exist_ok=True)
|
95
|
+
suffix = p.suffix[1:]
|
96
|
+
if suffix.lower() == 'jpg':
|
97
|
+
suffix = 'jpeg'
|
98
|
+
if self.mode in ('RGBA', 'P') and suffix == 'jpeg':
|
99
|
+
im = self.convert('RGB')
|
100
|
+
else:
|
101
|
+
im = self
|
102
|
+
im.save(str(p), suffix, **kwargs)
|
103
|
+
|
104
|
+
def cvt_channel(self, flags=None):
|
105
|
+
im = self
|
106
|
+
if flags is None or flags == -1: return im
|
107
|
+
n_c = im.n_channels
|
108
|
+
if flags == 0 and n_c > 1:
|
109
|
+
im = im.convert('L')
|
110
|
+
elif flags == 1 and n_c != 3:
|
111
|
+
im = im.convert('RGB')
|
112
|
+
return im
|
113
|
+
|
114
|
+
def to_buffer(self, ext='.jpg', *, b64encode=False):
|
115
|
+
# 主要是偷懒,不想重写一遍,就直接去调用cv版本的实现了
|
116
|
+
return xlcv.to_buffer(self.to_cv2_image(), ext, b64encode=b64encode)
|
117
|
+
|
118
|
+
def display(self):
|
119
|
+
""" 在jupyter中展示 """
|
120
|
+
try:
|
121
|
+
from IPython.display import display
|
122
|
+
display(self)
|
123
|
+
except ModuleNotFoundError:
|
124
|
+
pass
|
125
|
+
|
126
|
+
def __4_plot(self):
|
127
|
+
pass
|
128
|
+
|
129
|
+
def plot_border(self, border=1, fill='black'):
|
130
|
+
""" 给图片加上边框
|
131
|
+
|
132
|
+
Args:
|
133
|
+
self:
|
134
|
+
border: 边框的厚度
|
135
|
+
fill: 边框颜色
|
136
|
+
|
137
|
+
Returns: 一张新图
|
138
|
+
"""
|
139
|
+
from PIL import ImageOps
|
140
|
+
im2 = ImageOps.expand(self, border=border, fill=fill)
|
141
|
+
return im2
|
142
|
+
|
143
|
+
def plot_text(self, text, xy=None, font_size=10, font_type='simfang.ttf', **kwargs):
|
144
|
+
"""
|
145
|
+
:param xy: 写入文本的起始坐标,没写入则自动写在垂直居中位置
|
146
|
+
"""
|
147
|
+
from PIL import ImageFont, ImageDraw
|
148
|
+
font_file = get_font_file(font_type)
|
149
|
+
font = ImageFont.truetype(font=str(font_file), size=font_size, encoding="utf-8")
|
150
|
+
draw = ImageDraw.Draw(self)
|
151
|
+
if xy is None:
|
152
|
+
w, h = font_getsize(font, text)
|
153
|
+
xy = ((self.size[0] - w) / 2, (self.size[1] - h) / 2)
|
154
|
+
draw.text(xy, text, font=font, **kwargs)
|
155
|
+
return self
|
156
|
+
|
157
|
+
def __5_resize(self):
|
158
|
+
pass
|
159
|
+
|
160
|
+
def reduce_area(self, area):
|
161
|
+
""" 根据面积上限缩小图片
|
162
|
+
|
163
|
+
即图片面积超过area时,按照等比例缩小到面积为area的图片
|
164
|
+
"""
|
165
|
+
im = self
|
166
|
+
h, w = PilImg.imsize(im)
|
167
|
+
s = h * w
|
168
|
+
if s > area:
|
169
|
+
r = (area / s) ** 0.5
|
170
|
+
size = int(r * h), int(r * w)
|
171
|
+
im = PilImg.resize2(im, size)
|
172
|
+
return im
|
173
|
+
|
174
|
+
def resize2(self, size, **kwargs):
|
175
|
+
"""
|
176
|
+
:param size: 默认是 (w, h), 这里我倒过来 (h, w)
|
177
|
+
但计算机领域,确实经常都是用 (w, h) 的格式,毕竟横轴是x,纵轴才是y
|
178
|
+
:param kwargs:
|
179
|
+
resample=3,插值算法;有PIL.Image.NEAREST, ~BOX, ~BILINEAR, ~HAMMING, ~BICUBIC, ~LANCZOS等
|
180
|
+
默认是 PIL.Image.BICUBIC;如果mode是"1"或"P"模式,则总是 PIL.Image.NEAREST
|
181
|
+
|
182
|
+
>>> im = read(np.zeros([100, 200], dtype='uint8'), 0)
|
183
|
+
>>> self.size
|
184
|
+
(100, 200)
|
185
|
+
>>> im2 = im.reduce_area(50*50, **kwargs)
|
186
|
+
>>> im2.size
|
187
|
+
(35, 70)
|
188
|
+
"""
|
189
|
+
# 注意pil图像尺寸接口都是[w,h],跟标准的[h,w]相反
|
190
|
+
return self.resize(size[::-1], **kwargs)
|
191
|
+
|
192
|
+
def evaluate_image_file_size(self, suffix='.jpeg'):
|
193
|
+
""" 评估图像存成文件后的大小
|
194
|
+
|
195
|
+
:param suffix: 使用的图片类型
|
196
|
+
:return int: 存储后的文件大小,单位为字节
|
197
|
+
"""
|
198
|
+
im = self
|
199
|
+
|
200
|
+
# save接口不支持jpg参数
|
201
|
+
if suffix[0] == '.':
|
202
|
+
suffix = suffix[1:]
|
203
|
+
if suffix.lower() == 'jpg':
|
204
|
+
suffix = 'jpeg'
|
205
|
+
|
206
|
+
file = io.BytesIO()
|
207
|
+
if im.mode in ('RGBA', 'P') and suffix == 'jpeg':
|
208
|
+
im = im.convert('RGB')
|
209
|
+
im.save(file, suffix)
|
210
|
+
return len(file.getvalue())
|
211
|
+
|
212
|
+
def reduce_filesize(self, filesize=None, suffix='.jpeg'):
|
213
|
+
""" 按照保存后的文件大小来压缩im
|
214
|
+
|
215
|
+
:param filesize: 单位Bytes
|
216
|
+
int, 可以用 300*1024 来表示 300KB
|
217
|
+
None, 可以不输入,默认读取后按原尺寸返回,这样看似没变化,其实图片一读一写,是会对手机拍照的很多大图进行压缩的
|
218
|
+
|
219
|
+
>> reduce_filesize(im, 300*1024, 'jpg')
|
220
|
+
"""
|
221
|
+
im = self
|
222
|
+
# 循环处理
|
223
|
+
while filesize:
|
224
|
+
r = xlpil.evaluate_image_file_size(im, suffix) / filesize
|
225
|
+
if r <= 1:
|
226
|
+
break
|
227
|
+
|
228
|
+
# 假设图片面积和文件大小成正比,如果r=4,表示长宽要各减小至1/(r**0.5)才能到目标文件大小
|
229
|
+
rate = min(1 / (r ** 0.5), 0.95) # 并且限制每轮至少要缩小至95%,避免可能会迭代太多轮
|
230
|
+
im = im.resize((int(im.size[0] * rate), int(im.size[1] * rate)))
|
231
|
+
return im
|
232
|
+
|
233
|
+
def trim(self, *, border=0, color=None):
|
234
|
+
""" 默认裁剪掉白色边缘,可以配合 get_backgroup_color 裁剪掉背景色
|
235
|
+
|
236
|
+
:param border: 上下左右保留多少边缘
|
237
|
+
输入一个整数,表示统一留边
|
238
|
+
也可以输入[int, int, int, int],分别表示left top right bottom留白
|
239
|
+
:param color: 要裁剪的颜色,这是精确值,没有误差,如果需要模糊,可以提前预处理成精确值
|
240
|
+
这个默认值设成None,本来是想说支持灰度图,此时输入一个255的值就好
|
241
|
+
但是pil的灰度图机制好像有些不太一样,总之目前默认值还是自动会设成 (255, 255, 255)
|
242
|
+
:param percent-background: TODO 控制裁剪百分比上限
|
243
|
+
"""
|
244
|
+
from PIL import Image, ImageChops
|
245
|
+
from pyxllib.algo.geo import xywh2ltrb, ltrb2xywh, ltrb_border
|
246
|
+
|
247
|
+
if color is None:
|
248
|
+
color = (255, 255, 255)
|
249
|
+
else:
|
250
|
+
color = tuple(color)
|
251
|
+
|
252
|
+
# 如果图片通道数跟预设的color不同,要断言
|
253
|
+
assert self.n_channels() == len(color), f'图片通道数{self.n_channels}跟预设的color{color}不同'
|
254
|
+
|
255
|
+
im = self
|
256
|
+
bg = Image.new(im.mode, im.size, color)
|
257
|
+
diff = ImageChops.difference(im, bg)
|
258
|
+
bbox = diff.getbbox() # 如果im跟bg一样,也就是裁"消失"了,此时bbox值为None
|
259
|
+
if bbox:
|
260
|
+
if border:
|
261
|
+
ltrb = xywh2ltrb(bbox)
|
262
|
+
ltrb = ltrb_border(ltrb, border, im.size)
|
263
|
+
bbox = ltrb2xywh(ltrb)
|
264
|
+
im = im.crop(bbox)
|
265
|
+
return im
|
266
|
+
|
267
|
+
def __6_warp(self):
|
268
|
+
pass
|
269
|
+
|
270
|
+
def __x_other(self):
|
271
|
+
pass
|
272
|
+
|
273
|
+
def random_direction(self):
|
274
|
+
""" 假设原图片是未旋转的状态0
|
275
|
+
|
276
|
+
顺时针转90度是label=1,顺时针转180度是label2 ...
|
277
|
+
"""
|
278
|
+
im = self
|
279
|
+
label = np.random.randint(4)
|
280
|
+
if label == 1:
|
281
|
+
# PIL的旋转角度,是指逆时针角度;但是我这里编号是顺时针
|
282
|
+
im = im.transpose(PIL.Image.ROTATE_270)
|
283
|
+
elif label == 2:
|
284
|
+
im = im.transpose(PIL.Image.ROTATE_180)
|
285
|
+
elif label == 3:
|
286
|
+
im = im.transpose(PIL.Image.ROTATE_90)
|
287
|
+
return im, label
|
288
|
+
|
289
|
+
def flip_direction(self, direction):
|
290
|
+
"""
|
291
|
+
:param direction: 顺时针旋转几个90度
|
292
|
+
标记现在图片是哪个方向:0是正常,1是向右翻转,2是向下翻转,3是向左翻转
|
293
|
+
"""
|
294
|
+
im = self
|
295
|
+
direction = direction % 4
|
296
|
+
if direction:
|
297
|
+
im = im.transpose({1: PIL.Image.ROTATE_270,
|
298
|
+
2: PIL.Image.ROTATE_180,
|
299
|
+
3: PIL.Image.ROTATE_90}[direction])
|
300
|
+
return im
|
301
|
+
|
302
|
+
def apply_exif_orientation(self):
|
303
|
+
""" 摆正图片角度
|
304
|
+
|
305
|
+
Image.open读取图片时,是手机严格正放时拍到的图片效果,
|
306
|
+
但手机拍照时是会记录旋转位置的,即可以判断是物理空间中,实际朝上、朝下的方向,
|
307
|
+
从而识别出正拍(代号1),顺时针旋转90度拍摄(代号8),顺时针180度拍摄(代号3),顺时针270度拍摄(代号6)。
|
308
|
+
windows上的图片查阅软件能识别方向代号后正确摆放;
|
309
|
+
为了让python处理图片的时候能增加这个属性的考虑,这个函数能修正识别角度返回新的图片。
|
310
|
+
|
311
|
+
我自己写过个版本,后来发现 labelme.utils.image 写过功能更强的,抄了过来~~
|
312
|
+
"""
|
313
|
+
im = self
|
314
|
+
try:
|
315
|
+
exif = im._getexif()
|
316
|
+
except AttributeError:
|
317
|
+
exif = None
|
318
|
+
|
319
|
+
if exif is None:
|
320
|
+
return im
|
321
|
+
|
322
|
+
exif = {
|
323
|
+
PIL.ExifTags.TAGS[k]: v
|
324
|
+
for k, v in exif.items()
|
325
|
+
if k in PIL.ExifTags.TAGS
|
326
|
+
}
|
327
|
+
|
328
|
+
orientation = exif.get("Orientation", None)
|
329
|
+
|
330
|
+
if orientation == 1:
|
331
|
+
# do nothing
|
332
|
+
return im
|
333
|
+
elif orientation == 2:
|
334
|
+
# left-to-right mirror
|
335
|
+
return PIL.ImageOps.mirror(im)
|
336
|
+
elif orientation == 3:
|
337
|
+
# rotate 180
|
338
|
+
return im.transpose(PIL.Image.ROTATE_180)
|
339
|
+
elif orientation == 4:
|
340
|
+
# top-to-bottom mirror
|
341
|
+
return PIL.ImageOps.flip(im)
|
342
|
+
elif orientation == 5:
|
343
|
+
# top-to-left mirror
|
344
|
+
return PIL.ImageOps.mirror(im.transpose(PIL.Image.ROTATE_270))
|
345
|
+
elif orientation == 6:
|
346
|
+
# rotate 270
|
347
|
+
return im.transpose(PIL.Image.ROTATE_270)
|
348
|
+
elif orientation == 7:
|
349
|
+
# top-to-right mirror
|
350
|
+
return PIL.ImageOps.mirror(im.transpose(PIL.Image.ROTATE_90))
|
351
|
+
elif orientation == 8:
|
352
|
+
# rotate 90
|
353
|
+
return im.transpose(PIL.Image.ROTATE_90)
|
354
|
+
else:
|
355
|
+
return im
|
356
|
+
|
357
|
+
def get_exif(self):
|
358
|
+
""" 旧函数名:查看图片的Exif信息 """
|
359
|
+
exif_data = self._getexif()
|
360
|
+
if exif_data:
|
361
|
+
exif = {PIL.ExifTags.TAGS[k]: v for k, v in exif_data.items() if k in PIL.ExifTags.TAGS}
|
362
|
+
else:
|
363
|
+
exif = None
|
364
|
+
return exif
|
365
|
+
|
366
|
+
def rgba2rgb(self):
|
367
|
+
if self.mode in ('RGBA', 'P'):
|
368
|
+
# 判断图片mode模式,如果是RGBA或P等可能有透明底,则和一个白底图片合成去除透明底
|
369
|
+
background = PIL.Image.new('RGBA', self.size, (255, 255, 255))
|
370
|
+
# composite是合成的意思。将右图的alpha替换为左图内容
|
371
|
+
self = PIL.Image.alpha_composite(background, self.convert('RGBA')).convert('RGB')
|
372
|
+
return self
|
373
|
+
|
374
|
+
def keep_subtitles(self, judge_func=None, trim_color=(255, 255, 255)):
|
375
|
+
im = self.to_cv2_image()
|
376
|
+
im = im.keep_subtitles(judge_func=judge_func, trim_color=trim_color)
|
377
|
+
return im.to_pil_image()
|
378
|
+
|
379
|
+
|
380
|
+
# pil相比cv,由于无法类似CvImg这样新建一个和np.ndarray等效的类,所以还是比较支持嵌入到Image中直接操作
|
381
|
+
inject_members(PilImg, PIL.Image.Image)
|
382
|
+
|
383
|
+
xlpil = PilImg # 与xlcv对称
|
384
|
+
|
385
|
+
|
386
|
+
def font_getsize(font, text):
|
387
|
+
""" 官方自带的font.getsize遇到换行符的text,计算不准确
|
388
|
+
"""
|
389
|
+
texts = text.split('\n')
|
390
|
+
sizes = [font.getsize(t) for t in texts]
|
391
|
+
w = max([w for w, h in sizes])
|
392
|
+
h = sum([h for w, h in sizes])
|
393
|
+
return w, h
|
394
|
+
|
395
|
+
|
396
|
+
def create_text_image(text, size=None, *, xy=None, font_size=14, bg_color=None, text_color=None, **kwargs):
|
397
|
+
""" 生成文字图片
|
398
|
+
|
399
|
+
:param size: 注意我这里顺序是 (height, width)
|
400
|
+
默认None,根据写入的文字动态生成图片大小
|
401
|
+
:param bg_color: 背景图颜色,如 (0, 0, 0)
|
402
|
+
默认None,随机颜色
|
403
|
+
:param text_color: 文本颜色,如 (255, 255, 255)
|
404
|
+
默认None,随机颜色
|
405
|
+
"""
|
406
|
+
if size is None:
|
407
|
+
from PIL import ImageFont, ImageDraw
|
408
|
+
font_file = get_font_file(kwargs.get('font_type', 'simfang.ttf'))
|
409
|
+
font = ImageFont.truetype(font=str(font_file), size=font_size, encoding="utf-8")
|
410
|
+
w, h = font_getsize(font, text)
|
411
|
+
size = (h + 10, w + 10)
|
412
|
+
xy = (5, 0) # 自动生成尺寸的情况下,文本从左上角开始写
|
413
|
+
|
414
|
+
if bg_color is None:
|
415
|
+
bg_color = tuple([random.randint(0, 255) for i in range(3)])
|
416
|
+
if text_color is None:
|
417
|
+
text_color = tuple([random.randint(0, 255) for i in range(3)])
|
418
|
+
|
419
|
+
h, w = size
|
420
|
+
im: PilImg = PIL.Image.new('RGB', (w, h), tuple(bg_color))
|
421
|
+
im2 = im.plot_text(text, xy=xy, font_size=font_size, fill=tuple(text_color), **kwargs)
|
422
|
+
|
423
|
+
return im2
|