import os.path from PIL import Image, ImageDraw, ImageEnhance, ImageFile import pillow_avif # import cv2 import numpy as np from blend_modes import multiply ImageFile.LOAD_TRUNCATED_IMAGES = True class PictureProcessing(object): def __init__(self, *args, **kwargs): self.im: Image if args: p = args[0] if isinstance(p, str): if p == "RGB" or p == "RGBA": size = int(args[1][0]), int(args[1][1]) self.im = Image.new(args[0], size, args[2]) else: # 设计优先取值处理 _path = args[0] file_path, file = os.path.split(_path) custom_path = "{}/custom/{}".format(file_path, file) if os.path.exists(custom_path): _path = custom_path self.im = Image.open(_path) elif "im" in kwargs: self.im = kwargs["im"] else: self.im = Image.new(mode="RGB", size=(1600, 500), color=(255, 255, 255)) def getbbox(self): return self.im.getbbox() def expand_bbox(self, bbox, value=100): x1, y1, x2, y2 = ( bbox[0] - value, bbox[1] - value, bbox[2] + value, bbox[3] + value, ) if x1 < 0: x1 = 0 if y1 < 0: y1 = 0 bbox = (x1, y1, x2, y2) return [int(x) for x in bbox] def show(self): self.im.show() def get_size(self): return self.im.size def get_im(self): self.im: Image return self.im def to_color_2(self, target, blend): # 正片叠底 return np.array(np.multiply(target / 256, blend / 256) * 256, dtype=np.uint8) def get_overlay_pic(self, top_img, color=None): # 正片叠底 top_img: PictureProcessing image_white = Image.new("RGB", (self.get_size()), color) backdrop_prepped = np.asfarray(image_white.convert("RGBA")) source_prepped = np.asfarray(self.im.convert("RGBA")) blended_np = multiply(backdrop_prepped, source_prepped, 1) img = Image.fromarray(np.uint8(blended_np)).convert("RGB") img.paste(top_img.im, (0, 0), top_img.im) return PictureProcessing(im=img) def to_overlay_pic_advance( self, mode="pixel", top_img="", base="nw", value=(0, 0), percentage=(0, 0), margins=(0, 0, 0, 0), opacity=100, top_png_img=None, ): # opacity 透明度 # 正片叠底 _p = PictureProcessing("RGBA", (self.width, self.height), (255, 255, 255, 255)) top_img = _p.paste_img( mode=mode, top_img=top_img, base=base, value=value, percentage=percentage, margins=margins, ) backdrop_prepped = np.asfarray(self.im.convert("RGBA")) source_prepped = np.asfarray(top_img.im.convert("RGBA")) blended_np = multiply(backdrop_prepped, source_prepped, opacity / 100) im = Image.fromarray(np.uint8(blended_np)).convert("RGB") pp = PictureProcessing(im=im) if top_png_img: pp = pp.paste_img( mode=mode, top_img=top_png_img, base=base, value=value, percentage=percentage, margins=margins, ) return pp def __to_resize(self, width=None, height=None): _im_x, _im_y = self.get_size() if width and height: if _im_x >= _im_y: high = None else: width = None if width: re_x = int(width) re_y = int(_im_y * re_x / _im_x) else: re_y = int(height) re_x = int(_im_x * re_y / _im_y) img = self.get_im().resize((re_x, re_y)) return img # 锐化图片 def sharpen_image(self, factor=1.0): # 创建一个ImageEnhance对象 enhancer = ImageEnhance.Sharpness(self.im) # 应用增强,值为0.0给出模糊图像,1.0给出原始图像,大于1.0给出锐化效果 # 调整这个值来增加或减少锐化的程度 sharp_img = enhancer.enhance(factor) return PictureProcessing(im=sharp_img) @property def width(self): return self.im.width @property def height(self): return self.im.height @property def size(self): return self.im.size def resize( self, mode="pixel", base="width", value=0, base_im=None, percentage=0, base_by_box=None, ): """ plugins_mode # relative相对(宽度、高度、其他参考图),或 pixel绝对像素 base # pixel,width,height,by_im 基于长边、基于短边 (基于短边时,则缩放到指定尺寸)by_im确保能塞进参考图内 value # 固定值,如果为百分比,则为0 percentage #百分比 base_im # 参考基于其他图 PictureProcessing 格式 """ # 在箱子内 if base_by_box: mode = "relative" base = "by_im" base_by_box = (int(base_by_box[0]), int(base_by_box[1])) base_im = Image.new("RGB", base_by_box, (255, 255, 255)) # 绝对值 if mode == "pixel": if base == "width": img = self.__to_resize(width=value) if base == "high": img = self.__to_resize(height=value) # 相对值 if mode == "relative": if base == "width": img = self.__to_resize( width=( self.width * percentage if not base_im else int(base_im.width * percentage) ) ) if base == "height": img = self.__to_resize( width=( self.height * percentage if not base_im else int(base_im.height * percentage) ) ) # by_im确保能塞进参考图内 if base == "by_im": percentage = 1 if not percentage else percentage box_width, box_height = int(base_im.width * percentage), int( base_im.height * percentage ) width, height = self.width, self.height if box_width / box_height < width / height: scale = box_width / width else: scale = box_height / height img = self.get_im().resize((int(width * scale), int(height * scale))) return PictureProcessing(im=img) def paste_img( self, mode="pixel", top_img="", base="nw", value=(0, 0), percentage=(0, 0), margins=(0, 0, 0, 0), ): top_img: PictureProcessing """ { "command": "paste_img", "im": 需要粘贴的图片 "pos": {"plugins_mode": "relative", # pixel "base": "center", # nw,nc,ne,ec ... 各个方向参考点 "value": (100, 100), "percentage": (0.5, 0.5), }, "margins": (0, 0, 0, 0), # 上下左右边距 } """ value = (int(value[0]), int(value[1])) # 处理默认值 base = "nw" if not base else base percentage = (0, 0) if not percentage else percentage if margins: top, down, left, right = margins else: top, down, left, right = 0, 0, 0, 0 if percentage != (0, 0): # percentage 不按占比模式 if base in ("nw", "wn", "wc", "cw", "nc", "cn", "center"): value = (int(self.width), int(self.height)) if base in ("sw", "ws", "sc", "cs", "center"): value = (int(self.width), -1 * int(self.height)) if base in ("ec", "ce"): value = (int(self.width), int(self.height)) if mode == "pixel": # 基于右边,上下居中 if base == "ec" or base == "ce": p_x = int(self.width - (top_img.width + value[0])) p_y = int((self.height - top_img.height) / 2) + value[1] # 基于顶部,左右居中 if base == "nc" or base == "cn": # 顶部对齐 deviation_x, deviation_y = int((self.width - top_img.width) / 2), int( (self.height - top_img.height) / 2 ) p_x = deviation_x + value[0] + left p_y = value[1] # 基于右上角 if base == "en" or base == "ne": p_x = int(self.width - (top_img.width + value[0])) + left p_y = value[1] # 基于左上角 if base == "nw" or base == "wn": deviation_x, deviation_y = 0, 0 p_x, p_y = value # 基于底部,左右居中 if base == "cs" or base == "sc": deviation_x, deviation_y = int((self.width - top_img.width) / 2), int( (self.height - top_img.height) / 2 ) p_y = self.height - (top_img.height + value[1] + down) p_x = deviation_x + value[0] + left # 上下左右居中 if base == "center" or base == "cc": deviation_x, deviation_y = int((self.width - top_img.width) / 2), int( (self.height - top_img.height) / 2 ) p_x = deviation_x + value[0] + left p_y = deviation_y + value[1] + top # 基于左下角 if base == "sw" or base == "ws": # deviation_x, deviation_y = 0, int((img.height - img_1.height)) p_x = value[0] + left p_y = self.height - (top_img.height + value[1] + down) # 基于左边,上下居中 if base == "wc" or base == "cw": p_x = value[0] + left p_y = int((self.height - top_img.height) / 2) + value[1] + top # 基于右下角 if base == "es" or base == "se": p_x = int(self.width - (top_img.width + value[0])) + left p_y = self.height - (top_img.height + value[1] + down) + top img = PictureProcessing(im=self.im).im # top_img.get_im().show() top_img_im = top_img.im try: img.paste(top_img_im, box=(p_x, p_y), mask=top_img_im.convert("RGBA")) except: img.paste(top_img_im, box=(p_x, p_y), mask=top_img_im) return PictureProcessing(im=img) def paste_img_invert( self, mode="pixel", top_img="", base="nw", value=(0, 0), percentage=(0, 0), margins=(0, 0, 0, 0), ): top_img: PictureProcessing bg_img = top_img.im top_img = PictureProcessing(im=self.im) self.im = bg_img return self.paste_img( mode=mode, top_img=top_img, base=base, value=value, percentage=percentage, margins=margins, ) # 水平分布处理,一行N个 def horizontal_distribution( self, pp_list, bg_width=1200, margins=(0, 0, 0, 0), line_spacing=0, number_per_row=3, ): """ pp_list line_spacing:行间距 number_per_row:每行个数 margins: (0, 0, 0, 0), # 上下左右边距 """ bg_width = int(bg_width) total_row = len(pp_list) // number_per_row if len(pp_list) % number_per_row > 0: total_row = total_row + 1 # print("total_row", total_row) every_height = pp_list[0].height bg_pp = PictureProcessing( "RGBA", ( bg_width, margins[0] + every_height * total_row + line_spacing * (total_row - 1) + margins[1], ), (255, 255, 255, 0), ) row_num = 0 y = margins[0] for i in range(0, len(pp_list), number_per_row): # 切片获取每N个元素 row_list = pp_list[i : i + number_per_row] # 输出每行的元素 if row_list: row_num += 1 # 计算每个元素间距 t_w = bg_pp.width - margins[2] - margins[3] al_w = sum([x.width for x in row_list]) space = int((t_w - al_w) / (len(row_list) + 1)) x = margins[2] # 粘贴每个元素 for pp in row_list: x = x + space bg_pp = bg_pp.paste_img( mode="pixel", top_img=pp, base="", value=(x, y) ) x = x + pp.width if row_num != total_row: y += every_height + line_spacing return bg_pp def get_text_image_advanced( self, value=(0, 0), font="", text="", anchor=None, align="left", spacing=0, fill=(0, 0, 0), return_mode="image", margins=(0, 0, 0, 0), max_len_one_line=0, ): """ { "command": "add_text", "pos": {"plugins_mode": "relative", # pixel "base": "center", # nw,nc,ne,ec ... 各个方向参考点 "value": (100, 100), "percentage": 0, }, "font": "", "text": "", "anchor": "", # mm 为居中 ma 为左右居中,上面居顶 rs 右边;从哪边开始输入 "align": "对齐方式",left center right "direction": "文本的方向", "max_len_one_line": "单行长度", "spacing": 10, "fill": "文字颜色", } margins 边距像素,上下左右;只有返回min_image_high 有效 """ # ===== """ return_mode:image返回图片,min_image_high返回最小高度尺寸但宽度不变,min_image返回最小高度与宽度 """ if not text: return self # pp = PictureProcessing("RGBA", self.size, (0, 0, 0, 0)) pp = PictureProcessing("RGBA", self.size, (255, 255, 255, 0)) draw_1 = ImageDraw.Draw(pp.im) # 定义字体,你需要有一个.ttf字体文件 spacing = 4 if not spacing else spacing align = "left" if not align else align # left, center 或 right if max_len_one_line > 0: text = text[:max_len_one_line] _, _, text_width, text_height = draw_1.textbbox((0, 0), text, font=font) value = (int(value[0]), int(value[1])) draw_1.multiline_text( (value[0], value[1] + margins[0]), text, fill=fill, font=font, anchor=anchor, spacing=spacing, align=align, direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, ) if return_mode == "image": pp = pp.paste_img_invert( mode="pixel", top_img=self, base="nw", value=(0, 0) ) pass if return_mode == "min_image_high": # pp.show() bbox = pp.getbbox() bbox = (0, 0, pp.width, bbox[3] + margins[1]) pp = pp.paste_img_invert( mode="pixel", top_img=self, base="nw", value=(0, 0) ) pp = pp.crop(bbox) if return_mode == "min_image": bbox = pp.getbbox() pp = pp.crop(bbox) return pp def add_text( self, mode="", base="", value="", percentage="", font="", text="", anchor="", align="", direction="", max_len_one_line="", spacing="", fill="", ): """ { "command": "add_text", "pos": {"plugins_mode": "relative", # pixel "base": "center", # nw,nc,ne,ec ... 各个方向参考点 "value": (100, 100), "percentage": 0, }, "font": "", "text": "", "anchor": "", # mm 为居中 ma 为左右居中,上面居顶 rs 右边;从哪边开始输入 "align": "对齐方式",left center right "direction": "文本的方向", "max_len_one_line": "单行长度", "spacing": 10, "fill": "文字颜色", } """ pp = PictureProcessing(im=self.im) draw_1 = ImageDraw.Draw(pp.im) # 定义字体,你需要有一个.ttf字体文件 spacing = 4 if not spacing else spacing anchor = None if not anchor else anchor align = "left" if not align else align # left, center 或 right _, _, text_width, text_height = draw_1.textbbox((0, 0), text, font=font) xy = (0, 0) if mode == "pixel": xy = value draw_1.multiline_text( xy, text, fill=fill, font=font, anchor=anchor, spacing=spacing, align=align, direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, ) return pp def crop_img( self, mode="pixel", base="nw", value=(100, 100, 10, 10), color_fill=(255, 255, 255), margins=(0, 0, 0, 0), ): """ { "command": "crop_img", "img": {"im": "im"}, "pos": {"plugins_mode": "relative", # pixel "base": "center", # nw,nc,ne,ec ... 各个方向参考点 "value": (100, 100, 10, 10),# 顶点,以及图片大小 }, "color_fill": (255, 255, 255) } """ pp = PictureProcessing(im=self.im) base = "nw" if not base else base if margins: top, down, left, right = margins else: top, down, left, right = 0, 0, 0, 0 out_img_size = (value[2], value[3]) # 默认填充色 if not color_fill: color_fill = (0, 0, 0) if mode == "pixel": # 左上脚 if base == "nw" or "wn": box = value # 左下角 if base == "sw" or base == "ws": # deviation_x, deviation_y = 0, int((img.height - img_1.height)) box = (value[0], pp.height - (value[1] + value[3]), value[2], value[3]) # print(box) # 右下角 if base == "se" or base == "es": box = ( pp.width - (value[0] + value[2]), pp.height - (value[1] + value[3]), value[2], value[3], ) # print(box) # 居中 if base == "cc": # print("11-value", value) x = int((pp.width + value[0] - value[2]) / 2) y = int((pp.height + value[1] - value[3]) / 2) box = (x, y, value[2], value[3]) # print("11-box", box) box = [box[0], box[1], box[0] + box[2], box[1] + box[3]] # print("12-box", box) # print("ww-hhh", pp.width, pp.height) out_img = pp.im.crop(box=box) # print("out_img", out_img.width, out_img.height) # print("-----") if box[0] < 0: out_img.paste( Image.new("RGB", (-1 * box[0], out_img.height), color_fill), (0, 0) ) if box[1] < 0: out_img.paste( Image.new("RGB", (out_img.width, -1 * box[1]), color_fill), (0, 0) ) if box[2] > pp.width: # print(box[2] - img.width, img.height) i = Image.new("RGB", (box[2] - pp.width, out_img.height), color_fill) out_img.paste(i, (pp.width - box[0], 0)) if box[3] > pp.height: out_img.paste( Image.new("RGB", (out_img.width, box[3] - pp.height), color_fill), (0, pp.height - box[1]), ) return PictureProcessing(im=out_img) def crop(self, bbox=(0, 0, 0, 0), mode=None): bbox = (int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3])) if mode == "min": bbox = self.getbbox() return PictureProcessing(im=self.im.crop(bbox)) def rotate(self, doge): return PictureProcessing(im=self.im.rotate(doge)) def rotate_advance(self, doge, is_crop=True): max_px = max(self.width, self.height) max_px = max_px * 2 bg = PictureProcessing("RGBA", (max_px, max_px), (255, 255, 255, 0)) bg = bg.paste_img(top_img=self, base="cc") bg = bg.rotate(doge) if is_crop: bg = bg.crop(mode="min") return bg def radius(self, mode="pixel", circular_pos=(1, 1, 1, 1), value=30, percentage=""): """ {"command": "radius", # radius "plugins_mode": "relative", # pixel 相对(短边),或绝对像素 "circular_pos": (0, 1, 0, 1), # 从左上角顺时针,记录圆角数量 "value": 649, # 固定值,如果为百分比,则为0 "percentage": 0, } # 百分比 """ # 单图圆角处理 pp = PictureProcessing(im=self.im) radii = value if radii > pp.width / 2: radii = int(pp.width / 2) if radii > pp.height / 2: radii = int(pp.height / 2) # 画圆(用于分离4个角) circle = Image.new("L", (radii * 2, radii * 2), 0) # 创建一个黑色背景的画布 draw = ImageDraw.Draw(circle) draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 画白色圆形 # 原图 img = pp.im.convert("RGBA") w, h = img.size # 画4个角(将整圆分离为4个部分) alpha = Image.new("L", img.size, 255) _pos = circular_pos if not _pos: _pos = (1, 1, 1, 1) for index, i in enumerate(_pos): if index == 0 and i == 1: alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) # 左上角 if index == 1 and i == 1: alpha.paste( circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0) ) # 右上角 if index == 2 and i == 1: alpha.paste( circle.crop((radii, radii, radii * 2, radii * 2)), (w - radii, h - radii), ) # 右下角 if index == 3 and i == 1: alpha.paste( circle.crop((0, radii, radii, radii * 2)), (0, h - radii) ) # 左下角 # alpha.show() img.putalpha(alpha) # 白色区域透明可见,黑色区域不可见 return PictureProcessing(im=img) # 图片翻转 def transpose(self, mode="left_right"): img = self.get_im() img = img.transpose(Image.FLIP_LEFT_RIGHT) return PictureProcessing(im=img) def convert(self, mode): return PictureProcessing(im=self.im.convert(mode)) def save_as_rgb(self, path): self.im = self.im.convert("RGB") self.im.save(path, format="JPEG") def save_as_png(self, path): self.im = self.im.convert("RGBA") self.im.save(path) def save_as_other(self, path, format): self.im.save(path, format=format)