import hashlib import os from io import BytesIO import aiohttp import cv2 import numpy as np import requests import requests.exceptions from PIL import Image, ImageDraw, ImageFont from nonebot import logger from hexi.plugins.nonebot_plugin_bf_bot.user_data.data_utils import UserManager, DesignationManager from .param import rank_pic filepath = os.path.dirname(__file__).replace("\\", "/") # 圆角遮罩处理 def draw_rect(img, pos, radius, **kwargs): trans = Image.new('RGBA', img.size, (0, 0, 0, 0)) alpha_draw = ImageDraw.Draw(trans, "RGBA") alpha_draw.rounded_rectangle(pos, radius, **kwargs) img.paste(Image.alpha_composite(img, trans)) return img # 圆角处理 def circle_corner(img, radii): """ 半透明圆角处理 :param img: 要修改的文件 :param radii: 圆角弧度 :return: 返回修改过的文件 """ circle = Image.new('L', (radii * 2, radii * 2), 0) # 创建黑色方形 draw = ImageDraw.Draw(circle) draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 黑色方形内切白色圆形 img = img.convert("RGBA") w, h = img.size # 创建一个alpha层,存放四个圆角,使用透明度切除圆角外的图片 alpha = Image.new('L', img.size, 255) alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) # 左上角 alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0)) # 右上角 alpha.paste(circle.crop((radii, radii, radii * 2, radii * 2)), (w - radii, h - radii)) # 右下角 alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii)) # 左下角 img.putalpha(alpha) # 白色区域透明可见,黑色区域不可见 # 添加圆角边框 draw = ImageDraw.Draw(img) draw.rounded_rectangle(img.getbbox(), outline="white", width=3, radius=radii) return img def png_resize(source_file, new_width=0, new_height=0, resample="LANCZOS", ref_file=""): """ PNG 等比缩放(保持 Alpha 透明度) :param source_file: Image.open() 得到的 Image 对象 :param new_width: 目标宽度(可选) :param new_height: 目标高度(可选) :param resample: NEAREST / BILINEAR / BICUBIC / LANCZOS :param ref_file: 参考图片路径(等比适配到参考尺寸) :return: PIL.Image (RGBA) """ img = source_file.convert("RGBA") src_w, src_h = img.size # ------------------------- # 计算目标尺寸(等比) # ------------------------- if ref_file: ref_img = Image.open(ref_file) ref_w, ref_h = ref_img.size scale = min(ref_w / src_w, ref_h / src_h) target_w = int(src_w * scale) target_h = int(src_h * scale) else: if new_width > 0 and new_height > 0: scale = min(new_width / src_w, new_height / src_h) target_w = int(src_w * scale) target_h = int(src_h * scale) elif new_width > 0: scale = new_width / src_w target_w = new_width target_h = int(src_h * scale) elif new_height > 0: scale = new_height / src_h target_h = new_height target_w = int(src_w * scale) else: # 未指定任何尺寸,直接返回 return img # ------------------------- # 重采样方式 # ------------------------- resample_map = { "NEAREST": Image.NEAREST, "BILINEAR": Image.BILINEAR, "BICUBIC": Image.BICUBIC, "LANCZOS": Image.LANCZOS } resample_method = resample_map.get(resample, Image.LANCZOS) # ------------------------- # Alpha 安全 resize(逐通道) # ------------------------- bands = img.split() resized_bands = [ band.resize((target_w, target_h), resample=resample_method) for band in bands ] return Image.merge("RGBA", resized_bands) # 图片粘贴 def image_paste(paste_image, under_image, pos): """ :param paste_image: 需要粘贴的图片 :param under_image: 底图 :param pos: 位置(x,y)坐标 :return: 返回图片 """ # 获取需要贴入图片的透明通道 r, g, b, alpha = paste_image.split() # 粘贴时将alpha值传递至mask属性 under_image.paste(paste_image, pos, alpha) return under_image def image_paste_center(paste_image: Image, under_image: Image, pos): """ 将一张图片粘贴到另一张图片上,并使粘贴图片的Y轴中心对齐到指定y坐标 :param paste_image: 需要粘贴的图片 (RGBA) :param under_image: 底图 (RGBA) :param pos: 粘贴位置 (x, y),其中y表示目标对齐的中心位置 :return: 返回合成后的图片 """ x, y = pos # 计算上下居中偏移 paste_w, paste_h = paste_image.size y_top = y - paste_h // 2 # 上边位置 = 中心点y - 半高 # 获取透明通道 r, g, b, alpha = paste_image.split() under_image.paste(paste_image, (x, y_top), alpha) return under_image def download_icon(url): data = download_url(url) img = BytesIO(data) return img # 下载QQ头像 def download_avatar(user_id: str) -> bytes: url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640" data = download_url(url) if not data or hashlib.md5(data).hexdigest() == "acef72340ac0e914090bd35799f5594e": url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=100" data = download_url(url) return data # 根据URL下载文件 def download_url(url: str) -> bytes: for i in range(3): try: resp = requests.get(url) if resp.status_code != 200: continue return resp.content except Exception as e: print(f"Error downloading {url}, retry {i}/3: {str(e)}") # 图片裁剪 def cut_image(pic_data: bytes, target_ratio: float): try: pic_data = Image.open(BytesIO(pic_data)) w, h = pic_data.size pic_ratio = w / h if pic_ratio > target_ratio: # 宽高比大于目标比例,按高度缩放。保持宽度不变 new_h = w / target_ratio h_delta = (h - new_h) / 2 w_delta = 0 else: # 宽高比小于目标比例,按宽度缩放。保持高度不变 new_w = h * target_ratio w_delta = (w - new_w) / 2 h_delta = 0 cropped = pic_data.crop((w_delta, h_delta, w - w_delta, h - h_delta)) return cropped except Exception as e: raise Exception(f"图片剪裁失败{str(e)}") def draw_centered_text(draw, text, x_left, x_right, y, font, fill): """ 在指定区域水平居中绘制文本 :param draw: ImageDraw对象 :param text: 要绘制的文本 :param x_left: 区域左边界x坐标 :param x_right: 区域右边界x坐标 :param y: 文本基线y坐标 :param font: ImageFont对象 :param fill: 文本颜色 """ text = str(text) text_width = font.getlength(text) # 获取文本像素宽度 area_width = x_right - x_left x_center = x_left + (area_width - text_width) / 2 draw.text((x_center, y), text, font=font, fill=fill) def draw_right_aligned_text(draw, text, x_left, x_right, y, font, fill): """ 在指定区域内从右往左绘制文本(右对齐) :param draw: ImageDraw 对象 :param text: 要绘制的文本 :param x_left: 区域左边界 x :param x_right: 区域右边界 x(文本右侧贴齐这里) :param y: 文本基线 y 坐标 :param font: ImageFont 对象 :param fill: 文本颜色 """ text = str(text) text_width = font.getlength(text) # 右对齐:文本右边贴到 x_right x_start = x_right - text_width # 可选:防止越界(超出左边界时截断) if x_start < x_left: x_start = x_left draw.text((x_start, y), text, font=font, fill=fill) def get_save_icon(game, name, icon_type, url): if game == 'bf6': name = name.replace("/", "").upper() else: name = name.replace("/", "") logger.info(f"查询的物品: {name},图标类型:{icon_type},游戏:{game}") icon = get_icon_from_cache(name, game, icon_type) if not icon: if game == "bf1": icon = get_icon_from_url(url) if icon_type == "weapon": icon = png_resize(icon, 500, 125) else: icon = png_resize(icon, 315, 79) elif game == "bf3": icon = get_icon_from_url(url) if icon_type == "weapon": icon = png_resize(icon, 500, 300) else: icon = png_resize(icon, 315, 200) elif game == "bf4": icon = get_icon_from_url(url) if icon_type == "weapon": icon = png_resize(icon, 500, 165) else: icon = png_resize(icon, 315, 80) elif game == "bfv": icon = get_icon_from_url(url) if icon_type == "weapon": icon = png_resize(icon, 500, 166) else: icon = png_resize(icon, 315, 315) icon.save(f"{filepath}/img/{game}/{icon_type}/{name}.png") logger.info(f"文件已经缓存至:{filepath}/img/{game}/{icon_type}/{name}.png") # 上述步骤处理完再执行一次icon判空 if icon: return icon else: # 针对空图片处理 if icon_type == "weapon": icon = Image.open(filepath + "/img/wp.png").convert('RGBA') else: icon = Image.open(filepath + "/img/vc.png").convert('RGBA') return icon # 获取icon def get_icon_from_url(url): if url: try: # 添加10s超时判断,如果超时返回空 res = BytesIO(requests.get(url, timeout=10).content) icon = Image.open(res).convert('RGBA') return icon except requests.exceptions.RequestException as e: logger.error(f"请求异常:{e}") def get_icon_from_cache(icon_name, game, icon_type): path = f"{filepath}/img/{game}/{icon_type}" logger.info(f"查询物品:{icon_name}物品路径:{path}") try: icon_list = os.listdir(path) # logger.info(f"所有物品:{icon_list}") if icon_name in str(icon_list): logger.info(f"本地存在{icon_name}物品") img = Image.open(f"{path}/{icon_name}.png").convert('RGBA') return img except Exception as err: logger.error(f"获取图标失败:{str(err)}") return None def cutout_region(img, mask, alpha=0): """ 在图像指定区域设置透明度 :param img: 输入图像 (BGR 或 BGRA) :param mask: 区域定义 - 矩形: (x1, y1, x2, y2) - 圆形: (cx, cy, r) - 任意 mask: numpy.ndarray (0/255) :param alpha: 设置的透明度 (0=透明, 255=不透明, 0-255之间半透明) :return: 处理后的图像 (BGRA) """ # 转为 BGRA (确保有 alpha 通道) if img.shape[2] == 3: b, g, r = cv2.split(img) a = np.ones(b.shape, dtype=np.uint8) * 255 img = cv2.merge((b, g, r, a)) h, w = img.shape[:2] region_mask = np.zeros((h, w), dtype=np.uint8) # 根据传入参数类型生成区域 mask if isinstance(mask, tuple): if len(mask) == 4: # 矩形 x1, y1, x2, y2 = mask region_mask[y1:y2, x1:x2] = 255 elif len(mask) == 3: # 圆形 cx, cy, r = mask cv2.circle(region_mask, (cx, cy), r, 255, -1) else: raise ValueError("不支持的 mask tuple 格式") elif isinstance(mask, np.ndarray): # 自定义 mask if mask.shape != (h, w): raise ValueError("mask 尺寸必须与图像一致") region_mask = mask else: raise TypeError("mask 必须是 tuple 或 numpy.ndarray") # 修改 alpha 通道 img[region_mask > 0, 3] = alpha return img def paste_image_cv2(fg, bg, pos): """ 将前景图粘贴到背景图上,支持透明度 (Alpha 混合) :param fg: 前景图 (必须是 BGRA) :param bg: 背景图 (BGR 或 BGRA) :param pos: 粘贴位置 (x, y) 左上角坐标 :return: 合成后的图像 (BGRA) """ x, y = pos fh, fw = fg.shape[:2] # 确保背景有 alpha 通道 if bg.shape[2] == 3: b, g, r = cv2.split(bg) a = np.ones(b.shape, dtype=np.uint8) * 255 bg = cv2.merge((b, g, r, a)) # 截取 ROI 区域 roi = bg[y:y + fh, x:x + fw] # 防止越界 if roi.shape[0] != fh or roi.shape[1] != fw: raise ValueError("前景图超出背景范围") # 提取 alpha 通道 alpha_fg = fg[:, :, 3] / 255.0 alpha_bg = 1.0 - alpha_fg # 混合 RGB for c in range(3): roi[:, :, c] = (alpha_fg * fg[:, :, c] + alpha_bg * roi[:, :, c]) # 更新 alpha 通道(取最大值,保证透明度正确) roi[:, :, 3] = np.clip(fg[:, :, 3] + roi[:, :, 3], 0, 255) # 放回背景 bg[y:y + fh, x:x + fw] = roi return bg def notice_paste(cv2_bg): pos = (1176, 318) # 获取公告 notice_path = f"{filepath}/notice/notice.png" front_img = cv2.imread(notice_path) mask = np.zeros((631, 1355), dtype=np.uint8) pts = np.array([[0, 0], [167, 0], [0, 631]], np.int32) cv2.fillPoly(mask, [pts], 255) front_img = cutout_region(front_img, mask, alpha=0) mix_bg = paste_image_cv2(front_img, cv2_bg, pos) return mix_bg def notice_paste_bf6(cv2_bg): x, y = 0, 0 notice_path = f"{filepath}/notice/notice_bf6.png" front_img = cv2.imread(notice_path, cv2.IMREAD_UNCHANGED) if front_img is None: raise RuntimeError(f"Failed to load image: {notice_path}") h, w = front_img.shape[:2] # ROI 边界裁剪 bg_h, bg_w = cv2_bg.shape[:2] w = min(w, bg_w - x) h = min(h, bg_h - y) roi = cv2_bg[y:y + h, x:x + w] # 有 Alpha 通道 → Alpha 混合 if front_img.shape[2] == 4: alpha = front_img[:h, :w, 3] / 255.0 alpha = alpha[:, :, None] cv2_bg[y:y + h, x:x + w] = ( alpha * front_img[:h, :w, :3] + (1 - alpha) * roi ).astype(np.uint8) # 无 Alpha → 直接覆盖 else: cv2_bg[y:y + h, x:x + w] = front_img[:h, :w] return cv2_bg def designation_paste(player, cv2_bg, user_id, start_x=10, start_y=10, target_height=40, spacing=6 ): designation_list_path = get_designation_list_by_user_id(user_id, player) if not designation_list_path: return cv2_bg icons = [] total_width = 0 for path in designation_list_path: icon = cv2.imread(path, cv2.IMREAD_UNCHANGED) if icon is None: continue h, w = icon.shape[:2] scale = target_height / h new_w = int(w * scale) icon = cv2.resize(icon, (new_w, target_height)) icons.append(icon) total_width += new_w total_width += spacing * (len(icons) - 1) x = start_x y = start_y # 关键变化点:直接使用 top-left bg_h, bg_w = cv2_bg.shape[:2] for icon in icons: ih, iw = icon.shape[:2] if x + iw > bg_w or y + ih > bg_h: break roi = cv2_bg[y:y + ih, x:x + iw] if icon.shape[2] == 4: alpha = icon[:, :, 3] / 255.0 for c in range(3): roi[:, :, c] = roi[:, :, c] * (1 - alpha) + icon[:, :, c] * alpha else: roi[:] = icon cv2_bg[y:y + ih, x:x + iw] = roi x += iw + spacing return cv2_bg def get_designation_list_by_user_id(user_id, player): usermanager = UserManager() designationmanager = DesignationManager() user_info = usermanager.get_user_by_qq(user_id) icon_no_list = [] if user_info is not None and user_info['ea_player_name'].upper() == player.upper(): logger.info(f"玩家标签列表:{user_info}") icon_no_list = user_info['designation'] icon_list = designationmanager.get_designation_by_id_list(icon_no_list) icon_path_list = [] for icon in icon_list: icon_file_name = icon['icon_path'] icon_path_list.append(f"{filepath}/img/icon/designation/{icon_file_name}") return icon_path_list def paste_rank_icon(rank, img): rank_pic_path = get_rank_pic(rank) rank_icon_path = f"{filepath}/img/icon/rank/{rank_pic_path}" rank_icon = Image.open(rank_icon_path) rank_icon = png_resize(rank_icon, new_width=132, new_height=192) fm_img = image_paste(rank_icon, img, (946, 61)) return fm_img def get_rank_pic(rank: int) -> str: """ 根据 rank 返回对应的 rank_pic 路径 """ for (start, end), info in rank_pic.items(): if start <= rank <= end: return info["path"] return "t_ui_rankswatch_1-9 1.png"