import os import uuid import random import math import glob import io import base64 from concurrent.futures import ThreadPoolExecutor, as_completed from PIL import Image, ImageDraw, ImageFont from flask import request, jsonify, send_file from lib import Qiniu import logging # 配置日志 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def get_font_list(): """ 获取"数字"文件夹下所有可用的字体文件列表 返回: - 字体文件路径列表,每个元素为字典:{"path": 路径, "name": 字体名称} """ font_dir = './手写字体' font_list = [] if os.path.exists(font_dir): # 查找所有字体文件(支持 .ttf, .TTF, .otf, .OTF) font_extensions = ['*.ttf', '*.TTF', '*.otf', '*.OTF'] font_files = [] for ext in font_extensions: font_files.extend(glob.glob(os.path.join(font_dir, ext))) # 验证每个字体文件是否有效 for font_file in font_files: try: # 尝试加载字体以验证是否有效 ImageFont.truetype(font_file, 60) # 获取字体文件名(不包含路径) font_name = os.path.basename(font_file) # 去掉扩展名作为显示名称 display_name = os.path.splitext(font_name)[0] font_list.append({ "path": font_file, "name": display_name, "filename": font_name }) except Exception as e: logger.warning(f"跳过无效字体文件 {font_file}: {e}") continue # 按名称排序 font_list.sort(key=lambda x: x['name']) return font_list def find_font_file(): """ 查找可用的字体文件 优先从"数字"文件夹随机选择一个字体,如果没有则使用系统默认字体 """ # 首先尝试从"数字"文件夹随机选择字体 font_dir = './数字' if os.path.exists(font_dir): # 查找所有字体文件(支持 .ttf, .TTF, .otf, .OTF) font_extensions = ['*.ttf', '*.TTF', '*.otf', '*.OTF'] font_files = [] for ext in font_extensions: font_files.extend(glob.glob(os.path.join(font_dir, ext))) if font_files: # 随机选择一个字体文件 selected_font = random.choice(font_files) try: # 尝试加载字体以验证是否有效 ImageFont.truetype(selected_font, 60) logger.info(f"从数字文件夹随机选择字体: {selected_font}") return selected_font except Exception as e: logger.warning(f"无法加载字体文件 {selected_font}: {e}") # 如果选中的字体无效,尝试其他字体 for font_file in font_files: try: ImageFont.truetype(font_file, 60) logger.info(f"使用备用字体: {font_file}") return font_file except: continue # 如果"数字"文件夹没有可用字体,尝试系统默认字体 font_paths = [ # macOS 系统字体 '/System/Library/Fonts/Supplemental/Arial.ttf', '/System/Library/Fonts/Helvetica.ttc', # Windows 系统字体(如果需要在Windows上运行) 'C:/Windows/Fonts/simhei.ttf', 'C:/Windows/Fonts/msyh.ttf', # Linux 系统字体 '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf', ] # 尝试查找系统字体文件 for font_path in font_paths: if os.path.exists(font_path): try: return font_path except: continue # 如果找不到,返回 None,将使用 PIL 默认字体 logger.warning("未找到可用字体文件,将使用 PIL 默认字体") return None def add_handwriting_effect(draw, text, x, y, font, font_size, text_color=(0, 0, 0)): """ 添加手写效果:轻微的位置扰动 注意:对于中文字符,建议使用手写字体文件而不是字符级别的偏移 """ chars = list(text) current_x = x for i, char in enumerate(chars): # 每个字符的轻微随机偏移(模拟手写的不规则性) offset_x = random.randint(-1, 1) offset_y = random.randint(-1, 1) # 绘制字符 draw.text((current_x + offset_x, y + offset_y), char, font=font, fill=text_color) # 获取字符宽度 try: bbox = draw.textbbox((0, 0), char, font=font) char_width = bbox[2] - bbox[0] except: # 如果失败,使用简单估算 # 中文字符通常宽度约为字体大小,英文和数字约为字体大小的0.6倍 if ord(char) > 127: # 中文字符 char_width = font_size else: # 英文或数字 char_width = int(font_size * 0.6) current_x += char_width def generate_handwriting_image(text, font_size=60, width=None, height=None, background_color=(255, 255, 255), text_color=(0, 0, 0), padding=20, font_path=None, use_handwriting_effect=True): """ 生成手写字体图片 参数: - text: 要生成的文字 - font_size: 字体大小(默认60) - width: 图片宽度(None 表示自动计算) - height: 图片高度(None 表示自动计算) - background_color: 背景颜色(RGB元组,默认白色;如果传入RGBA元组,则支持透明度) - text_color: 文字颜色(RGB元组,默认黑色) - padding: 内边距(默认20像素) - font_path: 字体文件路径(None 表示使用默认字体或自动查找) - use_handwriting_effect: 是否使用手写效果(默认True) 返回: - PIL Image 对象 """ try: # 加载字体 if font_path and os.path.exists(font_path): try: font = ImageFont.truetype(font_path, font_size) except: logger.warning(f"无法加载字体文件 {font_path},使用默认字体") font = ImageFont.load_default() else: # 尝试查找字体文件 found_font_path = find_font_file() if found_font_path: try: font = ImageFont.truetype(found_font_path, font_size) except: font = ImageFont.load_default() else: font = ImageFont.load_default() # 将背景颜色转换为RGBA格式(支持透明背景) if len(background_color) == 3: # RGB格式,转换为RGBA,alpha设为0(完全透明) bg_color_rgba = (*background_color, 0) elif len(background_color) == 4: # 已经是RGBA格式 bg_color_rgba = background_color else: # 默认透明背景 bg_color_rgba = (255, 255, 255, 0) # 创建临时绘图对象来测量文字尺寸(使用RGBA模式) temp_img = Image.new('RGBA', (100, 100), bg_color_rgba) temp_draw = ImageDraw.Draw(temp_img) # 获取文字边界框 try: bbox = temp_draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] # bbox的偏移量(某些字符可能超出基线) bbox_offset_x = bbox[0] bbox_offset_y = bbox[1] except: # 如果失败,使用简单估算 text_width = len(text) * font_size * 0.6 text_height = font_size * 1.2 bbox_offset_x = 0 bbox_offset_y = 0 # 计算图片尺寸 if width is None: width = int(text_width + padding * 2) if height is None: height = int(text_height + padding * 2) # 创建图片(使用RGBA模式支持透明背景) image = Image.new('RGBA', (width, height), bg_color_rgba) draw = ImageDraw.Draw(image) # 计算文字起始位置(完美居中) # 考虑bbox的偏移量,确保文字在图片正中央 x = (width - text_width) // 2 - bbox_offset_x y = (height - text_height) // 2 - bbox_offset_y # 绘制文字 if use_handwriting_effect: # 使用手写效果(每个字符轻微偏移) add_handwriting_effect(draw, text, x, y, font, font_size, text_color) else: # 直接绘制文字 draw.text((x, y), text, font=font, fill=text_color) logger.info(f"成功生成文字图片: {text[:20]}...") return image except Exception as e: logger.error(f"生成文字图片失败: {e}") raise Exception(f"生成文字图片失败: {str(e)}") def generate_and_upload_handwriting(text, font_size=60, width=None, height=None, background_color=(255, 255, 255), text_color=(0, 0, 0), padding=20, font_path=None, use_handwriting_effect=True): """ 生成手写字体图片并上传到七牛云 参数: - text: 要生成的文字 - font_size: 字体大小(默认60) - width: 图片宽度(None 表示自动计算) - height: 图片高度(None 表示自动计算) - background_color: 背景颜色(RGB元组,默认白色) - text_color: 文字颜色(RGB元组,默认黑色) - padding: 内边距(默认20像素) - font_path: 字体文件路径(None 表示使用默认字体或自动查找) - use_handwriting_effect: 是否使用手写效果(默认True) 返回: - 七牛云上的图片URL """ temp_file = None try: # 创建临时目录 temp_dir = "./temp" if not os.path.exists(temp_dir): os.makedirs(temp_dir) # 生成图片 image = generate_handwriting_image( text=text, font_size=font_size, width=width, height=height, background_color=background_color, text_color=text_color, padding=padding, font_path=font_path, use_handwriting_effect=use_handwriting_effect ) # 保存临时文件 temp_file = os.path.join(temp_dir, f"handwriting_{uuid.uuid4()}.png") image.save(temp_file, "PNG") # 上传到七牛云 file_key = f"UpImage/{uuid.uuid4()}.png" image_url = Qiniu.upload_to_qiniu(temp_file, file_key) logger.info(f"成功上传文字图片到七牛云: {image_url}") return image_url except Exception as e: logger.error(f"生成并上传文字图片失败: {e}") raise finally: # 清理临时文件 if temp_file and os.path.exists(temp_file): try: os.remove(temp_file) except Exception as e: logger.warning(f"清理临时文件失败: {e}") def create_handwriting_route(app): """ 创建手写字体图片生成接口路由 参数: - app: Flask应用实例 """ @app.route('/get_font_list', methods=['GET']) def get_font_list_api(): """ 获取可用字体列表接口 返回: { "success": true, "fonts": [ {"path": "字体路径", "name": "字体名称", "filename": "文件名"} ] } """ try: fonts = get_font_list() return jsonify({ "success": True, "fonts": fonts }) except Exception as e: logger.error(f"获取字体列表失败: {e}") return jsonify({ "success": False, "error": str(e) }), 500 def _generate_single_font_preview(font, text, font_size, width, height, background_color, text_color, padding, use_handwriting_effect): """ 辅助函数:生成单个字体的预览图片 用于多线程并行处理 """ try: # 生成图片 image = generate_handwriting_image( text=text, font_size=font_size, width=width, height=height, background_color=background_color, text_color=text_color, padding=padding, font_path=font['path'], use_handwriting_effect=use_handwriting_effect ) # 将图片转换为base64 img_io = io.BytesIO() image.save(img_io, 'PNG') img_io.seek(0) img_base64 = base64.b64encode(img_io.read()).decode('utf-8') return { "font_path": font['path'], "font_name": font['name'], "image_base64": "data:image/png;base64," + img_base64 } except Exception as e: logger.warning(f"生成字体 {font['name']} 预览失败: {e}") return None @app.route('/batch_generate_preview', methods=['POST']) def batch_generate_preview(): """ 批量生成字体预览接口(返回多个图片的base64编码) 请求参数(JSON): { "text": "要预览的文字", "font_size": 60, "padding": 20, "background_color": [255, 255, 255, 0], "text_color": [0, 0, 0], "use_handwriting_effect": true } 返回: { "success": true, "previews": [ { "font_path": "字体路径", "font_name": "字体名称", "image_base64": "base64编码的图片数据" } ] } """ try: data = request.get_json() if not data: return jsonify({ "success": False, "error": "请求参数为空" }), 400 text = data.get('text') if not text: return jsonify({ "success": False, "error": "缺少text参数" }), 400 # 获取可选参数 font_size = data.get('font_size', 60) width = data.get('width') height = data.get('height') background_color = tuple(data.get('background_color', [255, 255, 255])) text_color = tuple(data.get('text_color', [0, 0, 0])) padding = data.get('padding', 20) use_handwriting_effect = data.get('use_handwriting_effect', True) # 获取所有字体 fonts = get_font_list() if not fonts: return jsonify({ "success": False, "error": "没有可用的字体" }), 400 previews = [] # 使用线程池并行处理,最多同时处理10个字体(可根据需要调整) max_workers = min(10, len(fonts)) # 根据字体数量动态调整线程数 with ThreadPoolExecutor(max_workers=max_workers) as executor: # 提交所有任务 future_to_font = { executor.submit( _generate_single_font_preview, font, text, font_size, width, height, background_color, text_color, padding, use_handwriting_effect ): font for font in fonts } # 收集结果 for future in as_completed(future_to_font): result = future.result() if result is not None: previews.append(result) return jsonify({ "success": True, "previews": previews, "total": len(previews) }) except Exception as e: logger.error(f"批量生成预览失败: {e}") return jsonify({ "success": False, "error": str(e) }), 500 @app.route('/generate_handwriting', methods=['POST']) def generate_handwriting(): """ 生成手写字体图片接口(直接返回图片文件) 请求参数(JSON): { "text": "要生成的文字", // 必需 "font_size": 60, // 可选,字体大小(默认60) "width": null, // 可选,图片宽度(null表示自动计算) "height": null, // 可选,图片高度(null表示自动计算) "background_color": [255, 255, 255] 或 [255, 255, 255, 0], // 可选,背景颜色RGB或RGBA(默认透明背景,alpha=0) "text_color": [0, 0, 0], // 可选,文字颜色RGB(默认黑色) "padding": 20, // 可选,内边距(默认20像素) "font_path": null, // 可选,字体文件路径(null表示使用默认字体) "use_handwriting_effect": true // 可选,是否使用手写效果(默认true) } 返回: - PNG图片文件(直接返回,不返回JSON) """ try: # 获取请求参数 data = request.get_json() if not data: return jsonify({ "success": False, "error": "请求参数为空" }), 400 text = data.get('text') if not text: return jsonify({ "success": False, "error": "缺少text参数" }), 400 # 获取可选参数 font_size = data.get('font_size', 60) width = data.get('width') height = data.get('height') background_color = tuple(data.get('background_color', [255, 255, 255])) text_color = tuple(data.get('text_color', [0, 0, 0])) padding = data.get('padding', 20) font_path = data.get('font_path') use_handwriting_effect = data.get('use_handwriting_effect', True) # 生成图片 image = generate_handwriting_image( text=text, font_size=font_size, width=width, height=height, background_color=background_color, text_color=text_color, padding=padding, font_path=font_path, use_handwriting_effect=use_handwriting_effect ) # 将图片保存到内存中的字节流 img_io = io.BytesIO() image.save(img_io, 'PNG') img_io.seek(0) logger.info(f"成功生成文字图片: {text[:20]}...") # 直接返回图片文件 return send_file( img_io, mimetype='image/png', as_attachment=False, download_name='handwriting.png' ) except Exception as e: logger.error(f"接口处理失败: {e}") return jsonify({ "success": False, "error": str(e) }), 500