209 lines
7.7 KiB
Python
209 lines
7.7 KiB
Python
"""
|
||
图片生成模块:创建军事主题网摘图片(单篇/合并长图)
|
||
"""
|
||
|
||
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
|