398 lines
14 KiB
Python
398 lines
14 KiB
Python
"""
|
|
图片生成技能 - 完整版本(支持中文)
|
|
整合多种图片生成功能
|
|
"""
|
|
|
|
import os
|
|
import tempfile
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
import matplotlib.pyplot as plt
|
|
import seaborn as sns
|
|
import qrcode
|
|
from wordcloud import WordCloud
|
|
|
|
# 导入字体工具
|
|
try:
|
|
# 尝试不同的导入方式
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from font_utils import get_chinese_font, setup_matplotlib_chinese
|
|
CHINESE_SUPPORT = True
|
|
except ImportError as e:
|
|
CHINESE_SUPPORT = False
|
|
print(f"⚠️ 字体工具导入失败,中文支持可能受限: {e}")
|
|
|
|
# 设置matplotlib为非交互模式
|
|
plt.switch_backend('Agg')
|
|
sns.set_style("whitegrid")
|
|
|
|
# 尝试设置matplotlib中文支持
|
|
if CHINESE_SUPPORT:
|
|
setup_matplotlib_chinese()
|
|
|
|
class ImageGenerationSkill:
|
|
"""图片生成技能主类"""
|
|
|
|
def __init__(self, config=None):
|
|
self.config = config or {}
|
|
|
|
# 尝试使用中文字体
|
|
if CHINESE_SUPPORT:
|
|
self.default_font = get_chinese_font(14)
|
|
else:
|
|
self.default_font = ImageFont.load_default()
|
|
print("⚠️ 使用默认字体,中文可能显示为方框")
|
|
|
|
print("Image Generation Skill v2.0 (中文支持) 初始化完成")
|
|
|
|
# ========== 基础功能 ==========
|
|
|
|
def create_chart(self, data, chart_type='bar', title='', style='default'):
|
|
"""创建图表
|
|
|
|
Args:
|
|
data: 数据字典 {'labels': [], 'values': []}
|
|
chart_type: 图表类型 ('bar', 'line', 'pie', 'scatter')
|
|
title: 图表标题
|
|
style: 样式 ('default', 'seaborn')
|
|
|
|
Returns:
|
|
图片路径
|
|
"""
|
|
try:
|
|
plt.figure(figsize=(10, 6))
|
|
|
|
if style == 'seaborn':
|
|
sns.set_palette("husl")
|
|
|
|
labels = data.get('labels', [])
|
|
values = data.get('values', [])
|
|
|
|
if chart_type == 'bar':
|
|
if style == 'seaborn':
|
|
ax = sns.barplot(x=labels, y=values)
|
|
# 添加数值标签
|
|
for i, v in enumerate(values):
|
|
ax.text(i, v + 0.1, str(v), ha='center')
|
|
else:
|
|
bars = plt.bar(labels, values)
|
|
for bar in bars:
|
|
height = bar.get_height()
|
|
plt.text(bar.get_x() + bar.get_width()/2., height + 0.1,
|
|
f'{height}', ha='center', va='bottom')
|
|
|
|
elif chart_type == 'line':
|
|
if style == 'seaborn':
|
|
sns.lineplot(x=labels, y=values, marker='o', linewidth=2)
|
|
else:
|
|
plt.plot(labels, values, marker='o', linewidth=2)
|
|
|
|
elif chart_type == 'pie':
|
|
plt.pie(values, labels=labels, autopct='%1.1f%%', startangle=90)
|
|
plt.axis('equal')
|
|
|
|
elif chart_type == 'scatter' and 'x' in data and 'y' in data:
|
|
if style == 'seaborn':
|
|
sns.scatterplot(x=data['x'], y=data['y'])
|
|
else:
|
|
plt.scatter(data['x'], data['y'])
|
|
|
|
if title:
|
|
plt.title(title, fontsize=14, fontweight='bold')
|
|
|
|
plt.tight_layout()
|
|
path = tempfile.mktemp(suffix='.png')
|
|
plt.savefig(path, dpi=120, bbox_inches='tight')
|
|
plt.close()
|
|
|
|
return path
|
|
|
|
except Exception as e:
|
|
plt.close()
|
|
raise Exception(f'图表生成失败: {e}')
|
|
|
|
def create_qrcode(self, content, size=200, output_path=None):
|
|
"""生成二维码"""
|
|
try:
|
|
qr = qrcode.QRCode(
|
|
version=1,
|
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
box_size=10,
|
|
border=4,
|
|
)
|
|
qr.add_data(content)
|
|
qr.make(fit=True)
|
|
|
|
qr_img = qr.make_image(fill_color='black', back_color='white')
|
|
qr_img = qr_img.resize((size, size))
|
|
|
|
if output_path is None:
|
|
output_path = tempfile.mktemp(suffix='.png')
|
|
|
|
qr_img.save(output_path)
|
|
return output_path
|
|
|
|
except Exception as e:
|
|
raise Exception(f'二维码生成失败: {e}')
|
|
|
|
def text_to_image(self, text, width=400, height=200,
|
|
bg_color='white', text_color='black'):
|
|
"""文本转图片"""
|
|
try:
|
|
img = Image.new('RGB', (width, height), color=bg_color)
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
lines = text.split('\n')
|
|
line_height = 30
|
|
y_start = max(20, (height - len(lines) * line_height) // 2)
|
|
|
|
for i, line in enumerate(lines):
|
|
y = y_start + i * line_height
|
|
# 简单居中
|
|
text_width = len(line) * 10
|
|
x = max(20, (width - text_width) // 2)
|
|
draw.text((x, y), line, fill=text_color, font=self.default_font)
|
|
|
|
path = tempfile.mktemp(suffix='.png')
|
|
img.save(path)
|
|
return path
|
|
|
|
except Exception as e:
|
|
raise Exception(f'文本转图片失败: {e}')
|
|
|
|
# ========== 高级功能 ==========
|
|
|
|
def create_wordcloud(self, text, width=800, height=400,
|
|
background_color='white', colormap='viridis'):
|
|
"""创建词云图"""
|
|
try:
|
|
wordcloud = WordCloud(
|
|
width=width,
|
|
height=height,
|
|
background_color=background_color,
|
|
colormap=colormap,
|
|
max_words=200,
|
|
contour_width=1,
|
|
contour_color='steelblue'
|
|
).generate(text)
|
|
|
|
path = tempfile.mktemp(suffix='.png')
|
|
wordcloud.to_file(path)
|
|
return path
|
|
|
|
except Exception as e:
|
|
raise Exception(f'词云图生成失败: {e}')
|
|
|
|
def create_color_palette(self, colors, title='颜色调色板'):
|
|
"""创建颜色调色板"""
|
|
try:
|
|
fig, axes = plt.subplots(1, len(colors), figsize=(len(colors)*2, 3))
|
|
|
|
if len(colors) == 1:
|
|
axes = [axes]
|
|
|
|
for ax, color in zip(axes, colors):
|
|
ax.add_patch(plt.Rectangle((0, 0), 1, 1, color=color))
|
|
ax.text(0.5, -0.2, color, ha='center', transform=ax.transAxes)
|
|
ax.set_xlim(0, 1)
|
|
ax.set_ylim(0, 1)
|
|
ax.axis('off')
|
|
|
|
plt.suptitle(title, fontsize=14, fontweight='bold')
|
|
plt.tight_layout()
|
|
|
|
path = tempfile.mktemp(suffix='.png')
|
|
plt.savefig(path, dpi=100, bbox_inches='tight')
|
|
plt.close()
|
|
|
|
return path
|
|
|
|
except Exception as e:
|
|
plt.close()
|
|
raise Exception(f'颜色调色板生成失败: {e}')
|
|
|
|
def create_data_card(self, title, data_dict, theme='light'):
|
|
"""创建数据卡片"""
|
|
try:
|
|
# 设置颜色主题
|
|
if theme == 'dark':
|
|
bg_color = '#2c3e50'
|
|
text_color = '#ecf0f1'
|
|
accent_color = '#3498db'
|
|
else:
|
|
bg_color = '#ffffff'
|
|
text_color = '#2c3e50'
|
|
accent_color = '#3498db'
|
|
|
|
# 计算卡片尺寸
|
|
num_items = len(data_dict)
|
|
card_width = 500
|
|
card_height = 150 + num_items * 40
|
|
|
|
# 创建卡片
|
|
img = Image.new('RGB', (card_width, card_height), color=bg_color)
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# 标题区域
|
|
draw.rectangle([0, 0, card_width, 60], fill=accent_color)
|
|
draw.text((20, 20), title, fill='white')
|
|
|
|
# 数据项
|
|
y = 80
|
|
for key, value in data_dict.items():
|
|
draw.text((30, y), f'• {key}:', fill=text_color)
|
|
draw.text((200, y), str(value), fill=accent_color)
|
|
|
|
# 如果是百分比,添加进度条
|
|
if isinstance(value, (int, float)) and 0 <= value <= 100:
|
|
bar_width = 200
|
|
bar_height = 20
|
|
bar_x = 250
|
|
|
|
# 进度条背景
|
|
draw.rectangle([bar_x, y, bar_x + bar_width, y + bar_height],
|
|
fill='#ecf0f1' if theme == 'light' else '#34495e')
|
|
|
|
# 进度条前景
|
|
fill_width = int(bar_width * value / 100)
|
|
draw.rectangle([bar_x, y, bar_x + fill_width, y + bar_height],
|
|
fill=accent_color)
|
|
|
|
# 百分比文本
|
|
text_color_bar = 'white' if value > 50 else text_color
|
|
draw.text((bar_x + 5, y), f'{value}%', fill=text_color_bar)
|
|
|
|
y += 40
|
|
|
|
# 边框
|
|
draw.rectangle([10, 70, card_width - 10, card_height - 10],
|
|
outline=accent_color, width=2)
|
|
|
|
path = tempfile.mktemp(suffix='.png')
|
|
img.save(path)
|
|
return path
|
|
|
|
except Exception as e:
|
|
raise Exception(f'数据卡片生成失败: {e}')
|
|
|
|
def create_system_status(self, status_data):
|
|
"""生成系统状态图"""
|
|
try:
|
|
width, height = 600, 400
|
|
img = Image.new('RGB', (width, height), color='#f8f9fa')
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# 标题
|
|
draw.rectangle([0, 0, width, 60], fill='#2c3e50')
|
|
draw.text((20, 20), '🖥️ 系统状态监控', fill='white', font=self.default_font)
|
|
|
|
items = list(status_data.items())
|
|
y_start = 80
|
|
|
|
for i, (label, value) in enumerate(items):
|
|
y = y_start + i * 50
|
|
draw.text((40, y), f'• {label}', fill='#34495e', font=self.default_font)
|
|
draw.text((200, y), str(value), fill='#3498db', font=self.default_font)
|
|
|
|
import datetime
|
|
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
draw.text((40, height-30), f'生成时间: {timestamp}',
|
|
fill='#7f8c8d', font=self.default_font)
|
|
|
|
path = tempfile.mktemp(suffix='.png')
|
|
img.save(path)
|
|
return path
|
|
|
|
except Exception as e:
|
|
raise Exception(f'系统状态图生成失败: {e}')
|
|
|
|
def create_comparison_chart(self, data_sets, labels, chart_type='grouped_bar'):
|
|
"""创建对比图表"""
|
|
try:
|
|
plt.figure(figsize=(12, 7))
|
|
|
|
x = np.arange(len(labels))
|
|
width = 0.8 / len(data_sets)
|
|
|
|
if chart_type == 'grouped_bar':
|
|
for i, data in enumerate(data_sets):
|
|
offset = (i - len(data_sets)/2 + 0.5) * width
|
|
bars = plt.bar(x + offset, data, width, label=f'数据集 {i+1}')
|
|
|
|
# 添加数值标签
|
|
for bar in bars:
|
|
height = bar.get_height()
|
|
plt.text(bar.get_x() + bar.get_width()/2., height + 0.1,
|
|
f'{height}', ha='center', va='bottom', fontsize=8)
|
|
|
|
elif chart_type == 'stacked_bar':
|
|
bottom = np.zeros(len(labels))
|
|
for i, data in enumerate(data_sets):
|
|
plt.bar(x, data, width, bottom=bottom, label=f'数据集 {i+1}')
|
|
bottom += data
|
|
|
|
elif chart_type == 'line':
|
|
for i, data in enumerate(data_sets):
|
|
plt.plot(x, data, marker='o', linewidth=2, label=f'数据集 {i+1}')
|
|
|
|
plt.xlabel('类别', fontsize=12)
|
|
plt.ylabel('数值', fontsize=12)
|
|
plt.title('数据对比分析', fontsize=14, fontweight='bold')
|
|
plt.xticks(x, labels)
|
|
plt.legend()
|
|
plt.grid(True, alpha=0.3)
|
|
|
|
plt.tight_layout()
|
|
path = tempfile.mktemp(suffix='.png')
|
|
plt.savefig(path, dpi=120, bbox_inches='tight')
|
|
plt.close()
|
|
|
|
return path
|
|
|
|
except Exception as e:
|
|
plt.close()
|
|
raise Exception(f'对比图表生成失败: {e}')
|
|
|
|
# 创建技能实例
|
|
skill = ImageGenerationSkill()
|
|
|
|
# 快捷函数
|
|
def create_chart(*args, **kwargs):
|
|
return skill.create_chart(*args, **kwargs)
|
|
|
|
def create_qrcode(*args, **kwargs):
|
|
return skill.create_qrcode(*args, **kwargs)
|
|
|
|
def text_to_image(*args, **kwargs):
|
|
return skill.text_to_image(*args, **kwargs)
|
|
|
|
def create_wordcloud(*args, **kwargs):
|
|
return skill.create_wordcloud(*args, **kwargs)
|
|
|
|
def create_color_palette(*args, **kwargs):
|
|
return skill.create_color_palette(*args, **kwargs)
|
|
|
|
def create_data_card(*args, **kwargs):
|
|
return skill.create_data_card(*args, **kwargs)
|
|
|
|
def create_system_status(*args, **kwargs):
|
|
return skill.create_system_status(*args, **kwargs)
|
|
|
|
def create_comparison_chart(*args, **kwargs):
|
|
return skill.create_comparison_chart(*args, **kwargs)
|
|
|
|
__all__ = [
|
|
'ImageGenerationSkill',
|
|
'skill',
|
|
'create_chart',
|
|
'create_qrcode',
|
|
'text_to_image',
|
|
'create_wordcloud',
|
|
'create_color_palette',
|
|
'create_data_card',
|
|
'create_system_status',
|
|
'create_comparison_chart'
|
|
] |