newspaper_infographic.py 9.67 KB
#!/usr/bin/env python3
"""newspaper_infographic.py - 复古报纸风格信息图生成器 v2

Features:
- 标题区域:群名 + 日期 + 版次
- 分板块:按行业分类(AI Agent、大模型、AI硬件、行业动态等)
- 每条新闻:标题 + 简短描述
- 配图:用色块和图标代替真实图片

Usage:
    python3 newspaper_infographic.py --report report.md --output infographic.png
"""

import argparse
import os
from datetime import datetime
from PIL import Image, ImageDraw, ImageFont


# 颜色方案(复古报纸)
PAPER_BG = "#f5f0e1"      # 米黄色纸张
PAPER_DARK = "#e8dcc8"    # 深色纸张
TEXT_BLACK = "#1a1a1a"    # 黑色文字
TEXT_RED = "#8b0000"      # 深红色(标题强调)
TEXT_BLUE = "#1a3a5c"     # 深蓝色(板块标题)
BORDER_COLOR = "#2c2c2c"  # 边框色
ACCENT_COLOR = "#c9a84c"  # 金色强调

# 板块配色
CATEGORY_COLORS = {
    "AI Agent": "#e8d5b7",
    "大模型": "#d4e5d0",
    "AI硬件": "#d0d8e8",
    "行业动态": "#e8d0d0",
    "开源项目": "#d0e8e0",
    "其他": "#e0d0e8"
}


def load_fonts():
    """加载字体,返回不同大小的字体对象"""
    font_paths = [
        "/data/data/com.termux/files/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
        "/system/fonts/NotoSansCJK-Regular.ttc",
        "/data/data/com.termux/files/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
    ]
    
    font_regular = None
    for path in font_paths:
        if os.path.exists(path):
            try:
                font_regular = ImageFont.truetype(path, 14)
                font_bold = ImageFont.truetype(path, 16)
                font_title = ImageFont.truetype(path, 28)
                font_headline = ImageFont.truetype(path, 20)
                font_small = ImageFont.truetype(path, 11)
                font_large = ImageFont.truetype(path, 36)
                return font_regular, font_bold, font_title, font_headline, font_small, font_large
            except:
                continue
    
    # 回退到默认字体
    default = ImageFont.load_default()
    return default, default, default, default, default, default


def draw_text_wrapped(draw, text, x, y, max_width, font, fill, line_height=20):
    """绘制自动换行的文本,返回绘制后的底部 y 坐标"""
    words = text
    lines = []
    current_line = ""
    
    for char in words:
        test_line = current_line + char
        bbox = draw.textbbox((0, 0), test_line, font=font)
        if bbox[2] - bbox[0] > max_width and current_line:
            lines.append(current_line)
            current_line = char
        else:
            current_line = test_line
    
    if current_line:
        lines.append(current_line)
    
    for i, line in enumerate(lines):
        draw.text((x, y + i * line_height), line, fill=fill, font=font)
    
    return y + len(lines) * line_height


def parse_report(report_text):
    """解析报告内容,提取结构化数据"""
    sections = []
    current_section = None
    current_articles = []
    
    lines = report_text.split('\n')
    i = 0
    while i < len(lines):
        line = lines[i].strip()
        
        # 检测板块标题(### 开头)
        if line.startswith('### '):
            if current_section and current_articles:
                sections.append({
                    'category': current_section,
                    'articles': current_articles
                })
            current_section = line[4:]
            current_articles = []
            i += 1
            continue
        
        # 检测文章标题(- ** 开头)
        if line.startswith('- **') or line.startswith('1. **') or line.startswith('2. **'):
            # 提取标题
            title = line.split('**')[1] if '**' in line else line
            desc = ""
            
            # 尝试读取下一行作为描述
            if i + 1 < len(lines):
                next_line = lines[i + 1].strip()
                if next_line.startswith('  ') or next_line.startswith('\t'):
                    desc = next_line.strip()
                    i += 1
            
            current_articles.append({
                'title': title,
                'desc': desc
            })
        
        i += 1
    
    # 添加最后一个板块
    if current_section and current_articles:
        sections.append({
            'category': current_section,
            'articles': current_articles
        })
    
    return sections


def create_newspaper_infographic(report_text, output_path, group_name="8-TG0 AI AGI"):
    """创建复古报纸风格信息图"""
    
    WIDTH = 900
    MARGIN = 30
    
    # 加载字体
    font_regular, font_bold, font_title, font_headline, font_small, font_large = load_fonts()
    
    # 解析报告
    sections = parse_report(report_text)
    
    # 计算总高度
    total_height = 200  # 标题区域
    total_height += 60   # 统计栏
    
    for section in sections:
        total_height += 50  # 板块标题
        for article in section['articles']:
            total_height += 25  # 文章标题
            if article['desc']:
                total_height += 35  # 描述(换行后)
            total_height += 10  # 间距
        total_height += 20  # 板块间距
    
    total_height += 60  # 页脚
    total_height = max(total_height, 1200)  # 最小高度
    
    # 创建画布
    img = Image.new('RGB', (WIDTH, total_height), PAPER_BG)
    draw = ImageDraw.Draw(img)
    
    # === 报纸边框 ===
    draw.rectangle([3, 3, WIDTH-3, total_height-3], outline=BORDER_COLOR, width=3)
    draw.rectangle([8, 8, WIDTH-8, total_height-8], outline=BORDER_COLOR, width=1)
    
    y = 20
    
    # === 报头 ===
    # 报纸名称
    draw.text((WIDTH//2, y), "AI AGI 日报", fill=TEXT_BLACK, font=font_large, anchor="mm")
    y += 50
    
    # 装饰线
    draw.line([(MARGIN, y), (WIDTH-MARGIN, y)], fill=BORDER_COLOR, width=2)
    y += 12
    
    # 日期、群名、版次
    date_str = datetime.now().strftime("%Y年%m月%d日")
    edition_str = "第1版"
    
    draw.text((MARGIN, y), date_str, fill=TEXT_BLACK, font=font_regular)
    draw.text((WIDTH//2, y), f"{group_name}", fill=TEXT_RED, font=font_bold, anchor="mm")
    draw.text((WIDTH-MARGIN, y), edition_str, fill=TEXT_BLACK, font=font_regular, anchor="ra")
    y += 28
    
    # 装饰线
    draw.line([(MARGIN, y), (WIDTH-MARGIN, y)], fill=BORDER_COLOR, width=1)
    y += 20
    
    # === 统计栏 ===
    total_articles = sum(len(s['articles']) for s in sections)
    stats_text = f"今日精选:{total_articles} 篇文章 | {len(sections)} 个板块"
    
    # 统计框背景
    draw.rectangle([MARGIN, y, WIDTH-MARGIN, y+45], fill=PAPER_DARK, outline=BORDER_COLOR)
    draw.text((WIDTH//2, y+22), stats_text, fill=TEXT_BLACK, font=font_bold, anchor="mm")
    y += 60
    
    # === 板块内容 ===
    for section_idx, section in enumerate(sections):
        category = section['category']
        articles = section['articles']
        
        # 板块背景色
        cat_color = CATEGORY_COLORS.get(category, PAPER_DARK)
        
        # 板块标题区域
        section_y = y
        draw.rectangle([MARGIN, y, WIDTH-MARGIN, y+40], fill=cat_color, outline=BORDER_COLOR)
        
        # 板块图标(用色块代替)
        icon_colors = ["#c9a84c", "#8b0000", "#1a3a5c", "#2e7d32", "#6a1b9a", "#d84315"]
        icon_color = icon_colors[section_idx % len(icon_colors)]
        draw.rectangle([MARGIN+10, y+8, MARGIN+30, y+32], fill=icon_color)
        
        # 板块标题
        draw.text((MARGIN+40, y+20), f"【{category}】", fill=TEXT_BLACK, font=font_headline, anchor="lm")
        y += 50
        
        # 文章列表
        for article in articles:
            title = article['title']
            desc = article['desc']
            
            # 文章标题(带序号)
            title_text = f"• {title}"
            draw.text((MARGIN+15, y), title_text, fill=TEXT_BLACK, font=font_bold)
            y += 22
            
            # 描述(缩进,自动换行)
            if desc:
                desc_text = desc[:80] + "..." if len(desc) > 80 else desc
                y = draw_text_wrapped(
                    draw, desc_text, 
                    MARGIN+35, y, 
                    WIDTH - 2*MARGIN - 50, 
                    font_regular, TEXT_BLACK, 
                    line_height=18
                )
                y += 8
            
            y += 5
        
        y += 15
        
        # 板块分隔线
        if section_idx < len(sections) - 1:
            draw.line([(MARGIN+20, y), (WIDTH-MARGIN-20, y)], fill=BORDER_COLOR, width=1)
            y += 15
    
    # === 页脚 ===
    y += 20
    footer_text = f"由 AI 智能体生成 | {datetime.now().strftime('%H:%M')} | 数据来源:{group_name}"
    draw.text((WIDTH//2, y), footer_text, fill=TEXT_BLACK, font=font_small, anchor="mm")
    
    # 保存
    img.save(output_path, quality=95)
    print(f"Newspaper infographic saved: {output_path}")
    print(f"Size: {WIDTH}x{total_height}, Sections: {len(sections)}, Articles: {total_articles}")
    return output_path


def main():
    parser = argparse.ArgumentParser(description='Generate newspaper-style infographic')
    parser.add_argument('--report', required=True, help='Path to report markdown file')
    parser.add_argument('--output', default='/sdcard/WeChatDigest/infographic.png', 
                        help='Output image path')
    parser.add_argument('--group', default='8-TG0 AI AGI', help='Group name')
    args = parser.parse_args()
    
    # 读取报告
    with open(args.report, 'r', encoding='utf-8') as f:
        report_text = f.read()
    
    # 生成信息图
    create_newspaper_infographic(report_text, args.output, args.group)


if __name__ == '__main__':
    main()