文件
nahaichuan/tests/generate_article_covers.py

271 行
15 KiB
Python

# -*- coding: utf-8 -*-
"""
生成文章资讯专属现代中式极简矢量封面图 (Concise & Advanced Article Covers Generator)
彻底杜绝文字介绍类图片,纯粹以现代中式浅色专业风空间几何、流光、细线窗棂与单字印章呈现。
尺寸 600x380,浅色专业底色 #FFFFFF / #F8FAFC,主色 #2563EB,辅色 #06B6D4 / #7C3AED。
"""
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TARGET_DIR = os.path.join(BASE_DIR, 'public', 'assets', 'addons', 'official', 'images', 'articles')
os.makedirs(TARGET_DIR, exist_ok=True)
COMMON_DEFS = '''
<linearGradient id="artBgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFFFFF"/>
<stop offset="100%" stop-color="#F8FAFC"/>
</linearGradient>
<linearGradient id="artBlue" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#2563EB"/>
<stop offset="100%" stop-color="#1D4ED8"/>
</linearGradient>
<linearGradient id="artCyan" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#06B6D4"/>
<stop offset="100%" stop-color="#0EA5E9"/>
</linearGradient>
<linearGradient id="artPurple" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#7C3AED"/>
<stop offset="100%" stop-color="#6366F1"/>
</linearGradient>
<linearGradient id="artGlass" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFFFFF" stop-opacity="0.92"/>
<stop offset="100%" stop-color="#F1F5F9" stop-opacity="0.7"/>
</linearGradient>
<pattern id="artLattice" width="24" height="24" patternUnits="userSpaceOnUse">
<path d="M 24 0 L 0 0 0 24" fill="none" stroke="#E2E8F0" stroke-width="0.75" stroke-dasharray="3,3"/>
</pattern>
<filter id="artShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="6" stdDeviation="10" flood-color="#0F172A" flood-opacity="0.08"/>
</filter>
<filter id="artGlow" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="8" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
'''
def make_art_seal(x, y, char):
return f'''
<g transform="translate({x}, {y})">
<rect width="24" height="24" rx="4" fill="#FEF2F2" stroke="#DC2626" stroke-width="1.2"/>
<rect x="2" y="2" width="20" height="20" rx="2" fill="none" stroke="#DC2626" stroke-width="0.5" stroke-opacity="0.5"/>
<text x="12" y="17" fill="#DC2626" font-family="'Noto Serif SC', 'SimSun', serif" font-size="12" font-weight="700" text-anchor="middle">{char}</text>
</g>
'''
def create_base_svg(seal_char, visual_elements):
return f'''<svg width="600" height="380" viewBox="0 0 600 380" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
{COMMON_DEFS}
</defs>
<rect width="600" height="380" rx="8" fill="url(#artBgGrad)"/>
<rect width="600" height="380" rx="8" fill="url(#artLattice)" opacity="0.6"/>
<rect x="1" y="1" width="598" height="378" rx="7" fill="none" stroke="#E2E8F0" stroke-width="1.5"/>
{make_art_seal(554, 20, seal_char)}
<g transform="translate(300, 190)">
{visual_elements}
</g>
</svg>'''
articles = {
# 首页 4 篇技术深度文章专属抽象封面
# 1. 缓存穿透、击穿与雪崩《三阶重滤与抗压漏斗》
'article_cache.svg': ('盾', '''
<ellipse cx="0" cy="70" rx="210" ry="75" fill="none" stroke="#E2E8F0" stroke-width="1.2" stroke-dasharray="5,4"/>
<!-- 布隆过滤器多阶防御漏斗与位图拦截阵列 -->
<polygon points="0,-100 110,-55 0,-20 -110,-55" fill="url(#artGlass)" stroke="#2563EB" stroke-width="1.5" filter="url(#artShadow)"/>
<polygon points="0,-45 75,-10 0,18 -75,-10" fill="url(#artGlass)" stroke="#06B6D4" stroke-width="1.2"/>
<polygon points="0,5 45,26 0,46 -45,26" fill="url(#artBlue)"/>
<line x1="0" y1="-100" x2="0" y2="75" stroke="url(#artBlue)" stroke-width="2.5" stroke-dasharray="4,3"/>
<!-- 底部安全落盘数据持久层 -->
<circle cx="0" cy="75" r="22" fill="url(#artGlass)" stroke="#2563EB" stroke-width="1.5"/>
<circle cx="0" cy="75" r="8" fill="#2563EB" filter="url(#artGlow)"/>
<!-- 拦截击溃的异常请求粒子 -->
<circle cx="-85" cy="-35" r="4.5" fill="#DC2626"/>
<circle cx="85" cy="-35" r="4.5" fill="#DC2626"/>
<circle cx="-50" cy="15" r="3.5" fill="#06B6D4"/>
<circle cx="50" cy="15" r="3.5" fill="#2563EB"/>
'''),
# 2. MySQL LIMIT 深度分页《B+Tree 跳跃指针与延迟关联》
'article_mysql_page.svg': ('跃', '''
<ellipse cx="0" cy="65" rx="200" ry="70" fill="none" stroke="#E2E8F0" stroke-width="1.2" stroke-dasharray="4,4"/>
<!-- B+Tree 索引阶梯基准线 -->
<line x1="-150" y1="45" x2="150" y2="45" stroke="#CBD5E1" stroke-width="1.5"/>
<g transform="translate(-100, 20)">
<rect x="-18" y="-18" width="36" height="36" rx="5" fill="url(#artGlass)" stroke="#CBD5E1" stroke-width="1.2"/>
<circle cx="0" cy="0" r="4" fill="#94A3B8"/>
</g>
<g transform="translate(-25, -5)">
<rect x="-18" y="-18" width="36" height="36" rx="5" fill="url(#artGlass)" stroke="#93C5FD" stroke-width="1.5"/>
<circle cx="0" cy="0" r="4" fill="#2563EB"/>
</g>
<!-- 高性能延迟关联跳跃抛物线 (由 3.8s 降至 12ms) -->
<path d="M -100 5 C -40 -85 50 -85 100 -15" fill="none" stroke="url(#artCyan)" stroke-width="3" stroke-linecap="round"/>
<g transform="translate(100, -15)" filter="url(#artShadow)">
<rect x="-24" y="-24" width="48" height="48" rx="8" fill="url(#artBlue)"/>
<circle cx="0" cy="0" r="8" fill="#FFFFFF" filter="url(#artGlow)"/>
<circle cx="0" cy="0" r="3" fill="#1D4ED8"/>
</g>
<circle cx="0" cy="-70" r="5.5" fill="#06B6D4" filter="url(#artGlow)"/>
'''),
# 3. XXL-JOB 分布式分片广播《全域广播与等距波纹》
'article_xxl_job.svg': ('播', '''
<!-- 同心全域广播波纹 -->
<ellipse cx="0" cy="0" rx="170" ry="85" fill="none" stroke="#E2E8F0" stroke-width="1.2" stroke-dasharray="4,4"/>
<ellipse cx="0" cy="0" rx="115" ry="58" fill="none" stroke="#CBD5E1" stroke-width="1.2"/>
<ellipse cx="0" cy="0" rx="60" ry="30" fill="none" stroke="#93C5FD" stroke-width="1.5"/>
<!-- 中心分布式任务调度器 -->
<g filter="url(#artShadow)">
<circle cx="0" cy="0" r="30" fill="url(#artBlue)"/>
<circle cx="0" cy="0" r="12" fill="#FFFFFF"/>
<circle cx="0" cy="0" r="5" fill="#1D4ED8"/>
</g>
<!-- 环绕分片广播执行节点集群 -->
<circle cx="-150" cy="0" r="9" fill="url(#artGlass)" stroke="#06B6D4" stroke-width="1.8"/>
<circle cx="150" cy="0" r="9" fill="url(#artGlass)" stroke="#06B6D4" stroke-width="1.8"/>
<circle cx="0" cy="-75" r="8" fill="url(#artGlass)" stroke="#7C3AED" stroke-width="1.8"/>
<circle cx="0" cy="75" r="8" fill="url(#artGlass)" stroke="#7C3AED" stroke-width="1.8"/>
<circle cx="-105" cy="-45" r="6" fill="#2563EB"/>
<circle cx="105" cy="45" r="6" fill="#2563EB"/>
<circle cx="-105" cy="45" r="6" fill="#06B6D4"/>
<circle cx="105" cy="-45" r="6" fill="#7C3AED"/>
'''),
# 4. Redis 集群脑裂防御《双子星隔离与法定仲裁》
'article_redis_split.svg': ('防', '''
<!-- 网络分区虚线隔断 (Network Partition Boundary) -->
<line x1="0" y1="-115" x2="0" y2="115" stroke="#CBD5E1" stroke-width="1.8" stroke-dasharray="6,4"/>
<!-- 左侧:健康多数派集群节点 (Quorum Majority) -->
<g transform="translate(-85, 0)" filter="url(#artShadow)">
<circle cx="0" cy="0" r="52" fill="#EFF6FF" opacity="0.85"/>
<circle cx="0" cy="0" r="38" fill="url(#artBlue)"/>
<circle cx="0" cy="0" r="16" fill="#FFFFFF"/>
<circle cx="0" cy="0" r="7" fill="#1D4ED8"/>
<circle cx="-42" cy="-42" r="6" fill="#06B6D4"/>
<circle cx="42" cy="-42" r="6" fill="#06B6D4"/>
<circle cx="0" cy="52" r="6" fill="#2563EB"/>
</g>
<!-- 右侧:孤立少数派自动休眠 (Standby Sleep Mode) -->
<g transform="translate(85, 0)">
<circle cx="0" cy="0" r="44" fill="none" stroke="#E2E8F0" stroke-width="1.5" stroke-dasharray="4,4"/>
<circle cx="0" cy="0" r="28" fill="url(#artGlass)" stroke="#CBD5E1" stroke-width="1.5"/>
<circle cx="0" cy="0" r="10" fill="#94A3B8"/>
</g>
'''),
# 既有文章 8 篇
'article_microservice.svg': ('枢', '''
<ellipse cx="0" cy="50" rx="190" ry="75" fill="none" stroke="#E2E8F0" stroke-width="1.2" stroke-dasharray="5,4"/>
<g transform="translate(-70, 0)" filter="url(#artShadow)">
<polygon points="0,-28 35,-10 0,10 -35,-10" fill="url(#artBlue)"/>
<polygon points="-35,-10 0,10 0,34 -35,14" fill="#1D4ED8"/>
<polygon points="0,10 35,-10 35,14 0,34" fill="#1E40AF"/>
</g>
<g transform="translate(70, 0)" filter="url(#artShadow)">
<polygon points="0,-28 35,-10 0,10 -35,-10" fill="url(#artCyan)"/>
<polygon points="-35,-10 0,10 0,34 -35,14" fill="#0891B2"/>
<polygon points="0,10 35,-10 35,14 0,34" fill="#0E7490"/>
</g>
<g transform="translate(0, -50)" filter="url(#artShadow)">
<polygon points="0,-24 28,-8 0,8 -28,-8" fill="url(#artGlass)" stroke="#2563EB" stroke-width="1.2"/>
<circle cx="0" cy="0" r="4" fill="#2563EB"/>
</g>
<path d="M -70 0 Q 0 -40 70 0" fill="none" stroke="url(#artBlue)" stroke-width="2" stroke-linecap="round"/>
<path d="M 70 15 Q 0 55 -70 15" fill="none" stroke="url(#artCyan)" stroke-width="1.8" stroke-dasharray="4,3"/>
'''),
'article_cross_app.svg': ('端', '''
<ellipse cx="0" cy="60" rx="180" ry="65" fill="none" stroke="#E2E8F0" stroke-width="1.2" stroke-dasharray="4,4"/>
<g transform="translate(-65, -10)" filter="url(#artShadow)">
<rect x="-45" y="-65" width="90" height="130" rx="6" fill="url(#artGlass)" stroke="#CBD5E1" stroke-width="1.2"/>
<rect x="-35" y="-55" width="70" height="95" rx="3" fill="#FFFFFF" stroke="#E2E8F0"/>
<circle cx="0" cy="52" r="3" fill="#94A3B8"/>
</g>
<g transform="translate(60, 10)" filter="url(#artShadow)">
<rect x="-38" y="-75" width="76" height="150" rx="10" fill="#FFFFFF" stroke="#2563EB" stroke-width="1.5"/>
<circle cx="0" cy="-25" r="14" fill="url(#artBlue)"/>
</g>
<path d="M -20 -30 Q 15 -60 45 -40" fill="none" stroke="url(#artCyan)" stroke-width="2" stroke-linecap="round"/>
<path d="M 45 35 Q 15 65 -20 40" fill="none" stroke="url(#artPurple)" stroke-width="2" stroke-linecap="round" stroke-dasharray="4,3"/>
'''),
'article_ai_agent.svg': ('智', '''
<circle cx="0" cy="0" r="100" fill="none" stroke="#E2E8F0" stroke-width="1.2" stroke-dasharray="5,4"/>
<polygon points="0,-65 56,-32 56,32 0,65 -56,32 -56,-32" fill="none" stroke="#C4B5FD" stroke-width="1.2"/>
<circle cx="0" cy="0" r="28" fill="url(#artPurple)" filter="url(#artShadow)"/>
<circle cx="0" cy="0" r="16" fill="url(#artBlue)"/>
<circle cx="0" cy="0" r="7" fill="#FFFFFF"/>
<circle cx="0" cy="-65" r="4.5" fill="#2563EB"/>
<circle cx="56" cy="-32" r="4.5" fill="#06B6D4"/>
<circle cx="56" cy="32" r="4.5" fill="#7C3AED"/>
<circle cx="0" cy="65" r="4.5" fill="#2563EB"/>
<circle cx="-56" cy="32" r="4.5" fill="#06B6D4"/>
<circle cx="-56" cy="-32" r="4.5" fill="#7C3AED"/>
'''),
'article_pos_offline.svg': ('核', '''
<circle cx="0" cy="0" r="115" fill="none" stroke="#E2E8F0" stroke-width="1.2" stroke-dasharray="4,4"/>
<ellipse cx="-55" cy="0" rx="55" ry="50" fill="none" stroke="#CBD5E1" stroke-width="1.2"/>
<ellipse cx="55" cy="0" rx="55" ry="50" fill="none" stroke="#CBD5E1" stroke-width="1.2"/>
<circle cx="-55" cy="0" r="26" fill="url(#artBlue)" filter="url(#artShadow)"/>
<circle cx="-55" cy="0" r="10" fill="#FFFFFF"/>
<circle cx="55" cy="0" r="26" fill="url(#artCyan)" filter="url(#artShadow)"/>
<circle cx="55" cy="0" r="10" fill="#FFFFFF"/>
<path d="M -30 -10 C -10 -25 10 -25 30 -10" fill="none" stroke="url(#artBlue)" stroke-width="2" stroke-linecap="round"/>
<path d="M 30 10 C 10 25 -10 25 -30 10" fill="none" stroke="url(#artCyan)" stroke-width="2" stroke-linecap="round"/>
'''),
'article_erp_bom.svg': ('构', '''
<circle cx="0" cy="0" r="110" fill="none" stroke="#E2E8F0" stroke-width="1.2" stroke-dasharray="5,4"/>
<line x1="-90" y1="0" x2="90" y2="0" stroke="#CBD5E1" stroke-width="1"/>
<line x1="0" y1="-90" x2="0" y2="90" stroke="#CBD5E1" stroke-width="1"/>
<g filter="url(#artShadow)">
<polygon points="0,-35 32,-12 32,22 0,42 -32,22 -32,-12" fill="url(#artGlass)" stroke="#2563EB" stroke-width="1.2"/>
<circle cx="0" cy="5" r="16" fill="url(#artBlue)"/>
<circle cx="0" cy="5" r="7" fill="#FFFFFF"/>
</g>
<circle cx="0" cy="-80" r="4.5" fill="#2563EB"/>
<circle cx="80" cy="0" r="4.5" fill="#06B6D4"/>
<circle cx="0" cy="80" r="4.5" fill="#7C3AED"/>
<circle cx="-80" cy="0" r="4.5" fill="#2563EB"/>
'''),
'article_security_whitepaper.svg': ('固', '''
<path d="M 0 -85 L 60 -50 V 0 C 60 50 0 85 0 85 C 0 85 -60 50 -60 0 V -50 Z" fill="url(#artGlass)" stroke="#2563EB" stroke-width="1.5" filter="url(#artShadow)"/>
<circle cx="0" cy="0" r="26" fill="url(#artBlue)"/>
<circle cx="0" cy="0" r="12" fill="#FFFFFF"/>
<circle cx="0" cy="0" r="5" fill="#1D4ED8"/>
'''),
'article_southwest_digital.svg': ('融', '''
<ellipse cx="0" cy="40" rx="180" ry="70" fill="none" stroke="#E2E8F0" stroke-width="1.2" stroke-dasharray="4,4"/>
<path d="M -120 40 Q -60 -40 0 20 T 120 10" fill="none" stroke="url(#artBlue)" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="-60" cy="-10" r="6" fill="#2563EB" filter="url(#artGlow)"/>
<circle cx="0" cy="20" r="7" fill="#06B6D4" filter="url(#artGlow)"/>
<circle cx="60" cy="-15" r="6" fill="#7C3AED" filter="url(#artGlow)"/>
'''),
'article_tdd_standards.svg': ('规', '''
<rect x="-85" y="-85" width="170" height="170" rx="6" fill="none" stroke="#E2E8F0" stroke-width="1.2" stroke-dasharray="4,4"/>
<circle cx="0" cy="0" r="65" fill="none" stroke="#CBD5E1" stroke-width="1"/>
<polygon points="0,-40 38,25 -38,25" fill="url(#artGlass)" stroke="#2563EB" stroke-width="1.5" filter="url(#artShadow)"/>
<circle cx="0" cy="5" r="14" fill="url(#artBlue)"/>
<circle cx="0" cy="5" r="6" fill="#FFFFFF"/>
'''),
}
def generate_all():
for filename, (seal_char, elements) in articles.items():
svg_content = create_base_svg(seal_char, elements)
filepath = os.path.join(TARGET_DIR, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(svg_content.strip())
print(f"Generated concise article cover: {filename} ({len(svg_content)} bytes)")
if __name__ == '__main__':
generate_all()