| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563 |
- 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
|