Files
my-daily/scripts/modules/image_generator.py
T

209 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
图片生成模块:创建军事主题网摘图片(单篇/合并长图)
"""
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