""" 图片生成模块:创建军事主题网摘图片(单篇/合并长图) """ import re from datetime import datetime try: from PIL import Image, ImageDraw, ImageFont except ImportError: Image, ImageDraw, ImageFont = None, None, None from .utils import wrap_text from .logger import get_logger from .get_chinese_font import get_chinese_font def _parse_webzine_line(line): """解析网摘单行文本,返回 (前缀类型, 前缀原文, 内容文本)""" if not line.strip(): return ('empty', '', '') for prefix in ['标题:', '原标题:', '发布日期:', '正文:', '价值点:']: if line.startswith(prefix): return (prefix.rstrip(':'), prefix, line[len(prefix):]) return ('normal', '', line) def _load_webzine_fonts(): """加载网摘图片所需的所有字体""" return { 'banner': get_chinese_font(size=40, bold=True), 'article_title': get_chinese_font(size=36, bold=True), 'bold': get_chinese_font(size=32, bold=True), 'content': get_chinese_font(size=32, bold=False) } def _build_render_commands(webzine_content, fonts, content_width, temp_draw, skip_empty=False): """将网摘文本转换为渲染命令列表""" commands = [] for line in webzine_content.split('\n'): line_type, prefix, content = _parse_webzine_line(line) if line_type == 'empty': if not skip_empty: commands.append(('empty',)) continue if line_type == '标题': full_title = prefix + content wrapped = wrap_text(full_title, fonts['article_title'], content_width, temp_draw) for wl in wrapped: commands.append(('article_title', wl)) elif line_type in ('原标题', '发布日期', '正文', '价值点'): test_line = prefix + content test_bbox = temp_draw.textbbox((0, 0), test_line, font=fonts['content']) test_width = test_bbox[2] - test_bbox[0] if test_width <= content_width: commands.append(('prefix_sameline', prefix, content)) else: commands.append(('prefix_only', prefix)) wrapped = wrap_text(content, fonts['content'], content_width, temp_draw) for wl in wrapped: commands.append(('content_indented', wl)) else: wrapped = wrap_text(content, fonts['content'], content_width, temp_draw) for wl in wrapped: commands.append(('normal', wl)) return commands def _build_combined_render_commands(webzine_texts, fonts, content_width, temp_draw): """将多篇网摘文本合并转换为渲染命令列表(篇间以分隔线分隔)""" commands = [] for idx, wz in enumerate(webzine_texts, 1): if idx > 1: commands.append(('separator',)) commands.extend(_build_render_commands(wz, fonts, content_width, temp_draw, skip_empty=True)) return commands def _execute_render_commands(draw, commands, fonts, padding, line_height, content_width): """执行渲染命令列表,在指定画布上绘制内容,返回最终y坐标""" y = 135 for cmd in commands: cmd_type = cmd[0] if cmd_type == 'separator': y += 30 draw.line([(padding, y), (content_width + padding, y)], fill=(200, 200, 200), width=1) y += 30 elif cmd_type == 'empty': y += line_height elif cmd_type == 'normal': draw.text((padding, y), cmd[1], fill=(0, 0, 0), font=fonts['content']) y += line_height elif cmd_type == 'article_title': draw.text((padding, y), cmd[1], fill=(0, 0, 0), font=fonts['article_title']) y += line_height elif cmd_type == 'prefix_sameline': label_bbox = draw.textbbox((0, 0), cmd[1], font=fonts['bold']) label_width = label_bbox[2] - label_bbox[0] draw.text((padding, y), cmd[1], fill=(0, 0, 0), font=fonts['bold']) draw.text((padding + label_width, y), cmd[2], fill=(0, 0, 0), font=fonts['content']) y += line_height elif cmd_type == 'prefix_only': draw.text((padding, y), cmd[1], fill=(0, 0, 0), font=fonts['bold']) y += line_height elif cmd_type == 'content_indented': draw.text((padding, y), cmd[1], fill=(0, 0, 0), font=fonts['content']) y += line_height return y def create_webzine_image(webzine_content, output_path, title_text): """将网摘文本内容渲染为PNG图片(800px宽,自适应高度)""" logger = get_logger() if Image is None: logger.warning("PIL 未安装,跳过图片生成") return None try: width = 800 line_height = 40 padding = 50 content_width = width - padding * 2 date_match = re.search(r'\d{4}年\d{2}月\d{2}日', webzine_content) if date_match: date_str = date_match.group(0).replace('年', '.').replace('月', '.').replace('日', '') else: date_str = datetime.now().strftime('%Y.%m.%d') banner_text = f'网摘 {date_str}' fonts = _load_webzine_fonts() temp_img = Image.new('RGB', (100, 100), color=(255, 255, 255)) temp_draw = ImageDraw.Draw(temp_img) commands = _build_render_commands(webzine_content, fonts, content_width, temp_draw) actual_lines_count = len([c for c in commands if c[0] != 'separator']) img_height = 140 + actual_lines_count * line_height + padding * 2 img_height = int(img_height * 1.15) img = Image.new('RGB', (width, img_height), color=(255, 255, 255)) draw = ImageDraw.Draw(img) draw.rectangle([(0, 0), (width, 90)], fill=(26, 72, 144)) draw.text((padding, 25), banner_text, fill=(255, 255, 255), font=fonts['banner']) draw.line([(padding, 100), (width - padding, 100)], fill=(180, 180, 180), width=2) _execute_render_commands(draw, commands, fonts, padding, line_height, content_width) img.save(output_path, 'PNG', quality=95) logger.info("网摘图片已保存: %s (%d条命令, %dpx)", output_path, len(commands), img_height) return output_path except Exception as e: logger.warning("图片生成失败: %s", e) return None def create_combined_webzine_image(webzine_texts, output_path, now): """将已生成的网摘文本合并渲染为一张3800px长图""" logger = get_logger() if Image is None: logger.warning("PIL 未安装,跳过图片生成") return None try: width = 800 padding = 50 content_width = width - padding * 2 line_height = 45 fonts = _load_webzine_fonts() temp_img = Image.new('RGB', (100, 100), color=(255, 255, 255)) temp_draw = ImageDraw.Draw(temp_img) commands = _build_combined_render_commands(webzine_texts, fonts, content_width, temp_draw) fixed_height = 3800 img = Image.new('RGB', (width, fixed_height), color=(255, 255, 255)) draw = ImageDraw.Draw(img) draw.rectangle([(0, 0), (width, 90)], fill=(26, 72, 144)) date_str = now.strftime('%Y.%m.%d') draw.text((padding, 25), f'今日必看TOP3网摘 {date_str}', fill=(255, 255, 255), font=fonts['banner']) draw.line([(padding, 100), (width - padding, 100)], fill=(180, 180, 180), width=2) _execute_render_commands(draw, commands, fonts, padding, line_height, content_width) img.save(output_path, 'PNG', quality=95) logger.info("合并长图已保存: %s (%d条指令, %dpx)", output_path, len(commands), fixed_height) return output_path except Exception as e: logger.warning("合并长图生成失败: %s", e) return None