feat: 军事科技每日资讯推送系统 - Docker部署 + 日志系统 + 数据目录重组
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
图片生成模块:创建军事主题网摘图片(单篇/合并长图)
|
||||
"""
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user