render.py•33.7 kB
from __future__ import annotations
import asyncio
import base64
import json
import os
import re
import subprocess
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Tuple, Dict, Any, Union
from datetime import datetime
# 延迟导入 requests,避免在未安装时阻断其他后端
try:
import requests # type: ignore
REQUESTS_AVAILABLE = True
except Exception:
REQUESTS_AVAILABLE = False
# 尝试导入PIL作为备选方案
try:
from PIL import Image, ImageDraw, ImageFont
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
# 尝试导入imgkit和markdown
try:
import imgkit
import markdown
from markdown.extensions import codehilite, fenced_code, tables
IMGKIT_AVAILABLE = True
except ImportError:
IMGKIT_AVAILABLE = False
# 默认配置
ASPECT_RATIO = (3, 4)
DEFAULT_WIDTH = 1200
DEFAULT_HEIGHT = 1600
DEFAULT_BG = (255, 255, 255) # 纯白色
DEFAULT_TEXT_COLOR = (0, 0, 0) # 黑色
DEFAULT_ACCENT_COLOR = (70, 130, 180) # 钢蓝色
# 边距比例
SIDE_MARGIN_RATIO = 0.08
TOP_BOTTOM_MARGIN_RATIO = 0.08
# 渲染后端优先级
BACKEND_PRIORITY = [
"imgkit-wkhtmltopdf",
"markdown-pdf-cli",
"md-to-image",
"pil"
]
# 支持的输出格式
SUPPORTED_FORMATS = ["png", "jpg", "jpeg", "webp"]
# 最大尺寸限制
MAX_WIDTH = 4000
MAX_HEIGHT = 6000
@dataclass
class RenderOptions:
"""Options for rendering markdown to image with detailed configuration."""
width: int = 1200
height: int = 1600
align: str = "center"
bold: bool = False
background_color: str = "#FFFFFF"
text_color: str = "#000000"
accent_color: str = "#4682B4"
font_family: str = "Microsoft YaHei, PingFang SC, Helvetica Neue, Arial, sans-serif"
font_size: int = 20
line_height: float = 1.6
header_scale: float = 1.5
theme: str = "default"
shadow: bool = True
watermark: bool = False
watermark_text: str = "Generated by word2img-mcp"
output_format: str = "png"
quality: int = 95
backend_preference: str = "auto"
backend_used: Optional[str] = None
def get_available_backends() -> List[str]:
"""Get list of available rendering backends."""
available = []
# Check imgkit-wkhtmltopdf
try:
import imgkit
available.append("imgkit-wkhtmltopdf")
except ImportError:
pass
# Check markdown-pdf-cli
try:
result = subprocess.run(["npx", "markdown-pdf", "--version"],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
available.append("markdown-pdf-cli")
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
# Check md-to-image
try:
import md_to_image
available.append("md-to-image")
except ImportError:
pass
# Check PIL (always available)
available.append("pil")
return available
def get_renderer_status() -> Dict[str, bool]:
"""Get status of all rendering backends."""
backends = get_available_backends()
return {
"imgkit-wkhtmltopdf": "imgkit-wkhtmltopdf" in backends,
"markdown-pdf-cli": "markdown-pdf-cli" in backends,
"md-to-image": "md-to-image" in backends,
"pil": True, # PIL is always available
"total_available": len(backends)
}
def get_backend_details() -> Dict[str, Dict]:
"""Get detailed information about each backend."""
details = {}
# imgkit-wkhtmltopdf details
try:
import imgkit
details["imgkit-wkhtmltopdf"] = {
"available": True,
"type": "html-to-image",
"quality": "high",
"speed": "medium",
"features": ["css_styling", "custom_fonts", "complex_layouts"]
}
except ImportError:
details["imgkit-wkhtmltopdf"] = {"available": False}
# markdown-pdf-cli details
try:
result = subprocess.run(["npx", "markdown-pdf", "--version"],
capture_output=True, text=True, timeout=5)
details["markdown-pdf-cli"] = {
"available": result.returncode == 0,
"type": "markdown-to-pdf",
"quality": "medium",
"speed": "slow",
"features": ["markdown_support", "basic_styling"]
}
except (subprocess.TimeoutExpired, FileNotFoundError):
details["markdown-pdf-cli"] = {"available": False}
# md-to-image details
try:
import md_to_image
details["md-to-image"] = {
"available": True,
"type": "markdown-to-image",
"quality": "medium",
"speed": "fast",
"features": ["simple_rendering", "fast_processing"]
}
except ImportError:
details["md-to-image"] = {"available": False}
# PIL details
details["pil"] = {
"available": True,
"type": "text-to-image",
"quality": "basic",
"speed": "very_fast",
"features": ["basic_text", "simple_graphics", "fallback"]
}
return details
class MarkdownRenderer:
"""Markdown渲染器 - 支持多种后端"""
def __init__(self):
self.backends = ['imgkit-wkhtmltopdf', 'markdown-pdf-cli', 'md-to-image-cli', 'md-to-image-api', 'pil-fallback']
self.md_to_image_api_url = "http://localhost:3000/convert" # 可配置的API地址
def render(self, text: str, options: RenderOptions) -> str:
"""渲染Markdown为图片"""
for backend in self.backends:
try:
if backend == 'imgkit-wkhtmltopdf':
return self._render_with_imgkit(text, options)
elif backend == 'markdown-pdf-cli':
return self._render_with_markdown_pdf(text, options)
elif backend == 'md-to-image-cli':
return self._render_with_cli(text, options)
elif backend == 'md-to-image-api':
return self._render_with_api(text, options)
elif backend == 'pil-fallback':
return self._render_with_pil(text, options)
except Exception as e:
print(f"⚠️ {backend} 渲染失败: {e}")
continue
raise RuntimeError("所有渲染后端都失败了")
def _render_with_imgkit(self, text: str, options: RenderOptions) -> str:
"""使用imgkit/wkhtmltopdf渲染"""
if not IMGKIT_AVAILABLE:
raise RuntimeError("imgkit不可用")
# 将Markdown转换为HTML
html_content = self._markdown_to_html(text, options)
# 配置 wkhtmltopdf 可执行文件路径
config = imgkit.config()
# 尝试常见的 wkhtmltopdf 安装路径
possible_paths = [
r"C:\Program Files\wkhtmltopdf\bin\wkhtmltoimage.exe",
r"C:\Program Files (x86)\wkhtmltopdf\bin\wkhtmltoimage.exe",
"/usr/local/bin/wkhtmltoimage",
"/usr/bin/wkhtmltoimage",
"wkhtmltoimage" # 如果在 PATH 中
]
wkhtmltoimage_path = None
for path in possible_paths:
if os.path.exists(path):
wkhtmltoimage_path = path
break
if wkhtmltoimage_path:
config = imgkit.config(wkhtmltoimage=wkhtmltoimage_path)
# 设置wkhtmltoimage选项 (注意:wkhtmltoimage 支持的参数与 wkhtmltopdf 不同)
wkhtmltoimage_options = {
'width': options.width,
'height': options.height,
'quality': 95,
'format': options.output_format.upper() if options.output_format.lower() in ['png', 'jpg', 'jpeg'] else 'PNG',
}
# 生成输出文件路径
output_dir = Path("outputs")
output_dir.mkdir(exist_ok=True)
output_file = output_dir / f"imgkit_{os.getpid()}_{hash(text[:100]) % 10000}.{options.output_format}"
try:
# 使用imgkit渲染为图片
imgkit.from_string(
html_content,
str(output_file),
options=wkhtmltoimage_options,
config=config
)
if output_file.exists():
return str(output_file)
else:
raise RuntimeError("图片文件未生成")
except Exception as e:
raise RuntimeError(f"imgkit渲染失败: {e}")
def _markdown_to_html(self, text: str, options: RenderOptions) -> str:
"""将Markdown转换为带样式的HTML"""
# 配置markdown扩展
extensions = [
'markdown.extensions.tables',
'markdown.extensions.fenced_code',
'markdown.extensions.codehilite',
'markdown.extensions.nl2br',
'markdown.extensions.toc',
'markdown.extensions.attr_list',
]
# 转换markdown为HTML
md = markdown.Markdown(extensions=extensions)
html_body = md.convert(text)
# 生成完整的HTML文档
html_template = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown to Image</title>
<style>
{self._generate_css(options)}
</style>
</head>
<body>
<div class="container">
{html_body}
</div>
{self._generate_watermark(options) if options.watermark else ''}
</body>
</html>
"""
return html_template.strip()
def _generate_css(self, options: RenderOptions) -> str:
"""生成CSS样式"""
bg_color = options.background_color
text_color = options.text_color
accent_color = options.accent_color
text_align = options.align
if text_align == "center":
text_align = "center"
elif text_align == "right":
text_align = "right"
else:
text_align = "left"
css = f"""
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: {options.font_family};
font-size: {options.font_size}px;
line-height: {options.line_height};
color: {text_color};
background-color: {bg_color};
width: {options.width}px;
min-height: {options.height}px;
margin: 0;
padding: 0;
}}
.container {{
padding: {int(options.height * TOP_BOTTOM_MARGIN_RATIO)}px {int(options.width * SIDE_MARGIN_RATIO)}px;
text-align: {text_align};
height: 100%;
width: 100%;
box-sizing: border-box;
}}
h1, h2, h3, h4, h5, h6 {{
color: {accent_color};
margin: 0.5em 0;
font-weight: bold;
}}
h1 {{ font-size: {int(options.font_size * options.header_scale * 2)}px; }}
h2 {{ font-size: {int(options.font_size * options.header_scale * 1.7)}px; }}
h3 {{ font-size: {int(options.font_size * options.header_scale * 1.4)}px; }}
h4 {{ font-size: {int(options.font_size * options.header_scale * 1.2)}px; }}
h5 {{ font-size: {int(options.font_size * options.header_scale * 1.1)}px; }}
h6 {{ font-size: {int(options.font_size * options.header_scale)}px; }}
p {{
margin: 0.6em 0;
text-align: {text_align};
}}
strong, b {{
font-weight: bold;
color: {accent_color};
}}
em, i {{
font-style: italic;
}}
ul, ol {{
margin: 0.5em 0;
padding-left: 2em;
}}
li {{
margin: 0.3em 0;
}}
blockquote {{
border-left: 4px solid {accent_color};
margin: 1em 0;
padding: 0.5em 1em;
background: rgba({options.accent_color[0]}, {options.accent_color[1]}, {options.accent_color[2]}, 0.1);
font-style: italic;
}}
code {{
background-color: rgba(0, 0, 0, 0.1);
padding: 2px 4px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: {int(options.font_size * 0.9)}px;
}}
pre {{
background-color: rgba(0, 0, 0, 0.1);
padding: 1em;
border-radius: 5px;
overflow-x: auto;
margin: 1em 0;
}}
pre code {{
background: none;
padding: 0;
}}
table {{
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}}
th, td {{
border: 1px solid {accent_color};
padding: 0.5em 1em;
text-align: left;
}}
th {{
background-color: {accent_color};
color: {bg_color};
font-weight: bold;
}}
hr {{
border: none;
height: 2px;
background-color: {accent_color};
margin: 1.5em 0;
}}
a {{
color: {accent_color};
text-decoration: underline;
}}
img {{
max-width: 100%;
height: auto;
margin: 1em 0;
}}
"""
if options.shadow:
css += f"""
h1, h2, h3, h4, h5, h6 {{
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}}
"""
return css
def _generate_watermark(self, options: RenderOptions) -> str:
"""生成水印HTML"""
if not options.watermark:
return ""
return f"""
<div style="
position: fixed;
bottom: 20px;
right: 20px;
font-size: 12px;
color: rgba(128, 128, 128, 0.7);
font-family: Arial, sans-serif;
">
{options.watermark_text}
</div>
"""
def _render_with_markdown_pdf(self, text: str, options: RenderOptions) -> str:
"""使用markdown-pdf CLI渲染"""
# 创建临时Markdown文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
f.write(text)
md_file = f.name
try:
# 生成输出文件路径
output_dir = Path("outputs")
output_dir.mkdir(exist_ok=True)
# 先生成PDF
pdf_file = output_dir / f"md_pdf_{os.getpid()}_{hash(text[:100]) % 10000}.pdf"
# 构建命令
cmd = [
'npx', 'markdown-pdf',
md_file,
'--out', str(pdf_file),
'--paper-format', 'A4',
'--paper-orientation', 'portrait'
]
# 执行命令
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
timeout=60 # 60秒超时
)
if not pdf_file.exists():
raise RuntimeError("PDF文件未生成")
# 如果输出格式是PDF,直接返回
if options.output_format.lower() == 'pdf':
return str(pdf_file)
# 否则需要将PDF转换为图片
# 这里可以使用PIL来转换PDF到图片
if PIL_AVAILABLE:
try:
from pdf2image import convert_from_path # type: ignore
images = convert_from_path(str(pdf_file))
if images:
# 取第一页
img = images[0]
# 调整尺寸
img = img.resize((options.width, options.height), Image.Resampling.LANCZOS)
# 保存为指定格式
output_file = output_dir / f"md_pdf_{os.getpid()}_{hash(text[:100]) % 10000}.{options.output_format}"
if options.output_format.lower() == 'png':
img.save(output_file, format="PNG", optimize=True)
else:
img.save(output_file, format="JPEG", quality=95, optimize=True)
# 清理PDF文件
try:
os.unlink(pdf_file)
except:
pass
return str(output_file)
except ImportError:
print("⚠️ pdf2image不可用,无法转换PDF到图片")
# 如果没有pdf2image,返回PDF文件路径
return str(pdf_file)
# 如果PIL不可用,返回PDF文件路径
return str(pdf_file)
except subprocess.TimeoutExpired:
raise RuntimeError("渲染超时")
except subprocess.CalledProcessError as e:
raise RuntimeError(f"CLI执行失败: {e.stderr}")
finally:
# 清理临时文件
try:
os.unlink(md_file)
except:
pass
def _render_with_cli(self, text: str, options: RenderOptions) -> str:
"""使用md-to-image CLI渲染"""
# 创建临时Markdown文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
f.write(text)
md_file = f.name
try:
# 生成输出文件路径
output_dir = Path("outputs")
output_dir.mkdir(exist_ok=True)
output_file = output_dir / f"md_cli_{os.getpid()}_{hash(text[:100]) % 10000}.{options.output_format}"
# 构建命令
cmd = [
'md-to-image',
md_file,
'--output', str(output_file),
'--width', str(options.width),
'--height', str(options.height)
]
# 添加主题选项
if options.theme != "default":
cmd.extend(['--theme', options.theme])
# 执行命令
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
timeout=60 # 60秒超时
)
if output_file.exists():
return str(output_file)
else:
raise RuntimeError("输出文件未生成")
except subprocess.TimeoutExpired:
raise RuntimeError("渲染超时")
except subprocess.CalledProcessError as e:
raise RuntimeError(f"CLI执行失败: {e.stderr}")
finally:
# 清理临时文件
try:
os.unlink(md_file)
except:
pass
def _render_with_api(self, text: str, options: RenderOptions) -> str:
"""使用HTTP API渲染"""
if not REQUESTS_AVAILABLE:
raise RuntimeError("requests 不可用,无法使用 md-to-image API 后端")
try:
# 准备请求数据
payload = {
'markdown': text,
'format': options.output_format,
'width': options.width,
'height': options.height,
'theme': options.theme,
'background': options.background,
'text_color': options.text_color,
'accent_color': options.accent_color
}
# 发送请求
response = requests.post(
self.md_to_image_api_url,
json=payload,
timeout=60
)
if response.status_code == 200:
# 保存返回的图片
output_dir = Path("outputs")
output_dir.mkdir(exist_ok=True)
output_file = output_dir / f"md_api_{os.getpid()}_{hash(text[:100]) % 10000}.{options.output_format}"
with open(output_file, 'wb') as f:
f.write(response.content)
return str(output_file)
else:
raise RuntimeError(f"API调用失败: {response.status_code} - {response.text}")
except Exception as e:
raise RuntimeError(f"API请求失败: {e}")
def _render_with_pil(self, text: str, options: RenderOptions) -> str:
"""使用PIL作为备选方案"""
if not PIL_AVAILABLE:
raise RuntimeError("PIL不可用")
# 解析Markdown
segments = self._parse_markdown(text)
# 创建图片
img = Image.new("RGB", (options.width, options.height), options.background)
draw = ImageDraw.Draw(img)
# 计算基础字体大小
base_font_size = max(16, min(80, options.font_size))
y_offset = int(options.height * TOP_BOTTOM_MARGIN_RATIO)
x_left = int(options.width * SIDE_MARGIN_RATIO)
max_width = int(options.width * (1 - 2 * SIDE_MARGIN_RATIO))
for segment in segments:
# 处理表格
if segment.get('is_table'):
font_size = base_font_size - 4
font = self._load_font(font_size, False)
table_height = self._render_table(draw, segment['table_data'], font,
x_left, y_offset, max_width, options.text_color)
y_offset += table_height + int(base_font_size * 0.6)
continue
# 根据段落类型确定字体大小和样式
if segment.get('is_header'):
font_size = min(base_font_size + (4 - segment['header_level']) * 8, 80)
is_bold = True
elif segment.get('is_bold'):
font_size = base_font_size
is_bold = True
else:
font_size = base_font_size
is_bold = False
# 加载字体
font = self._load_font(font_size, is_bold)
# 文字换行处理
wrapped_lines = self._wrap_text(draw, segment['text'], font, max_width)
# 绘制文本
line_height = font.getbbox("Hg")[3] - font.getbbox("Hg")[1]
line_spacing = int(font_size * 0.3)
if options.align == "center":
# 计算整个文本块的宽度,以此居中整个段落
max_line_width = max(self._measure_text(draw, line, font)[0] for line in wrapped_lines) if wrapped_lines else 0
block_x = (options.width - max_line_width) // 2
for line in wrapped_lines:
draw.text((block_x, y_offset), line, fill=options.text_color, font=font)
y_offset += line_height + line_spacing
else:
# 左对齐
for line in wrapped_lines:
draw.text((x_left, y_offset), line, fill=options.text_color, font=font)
y_offset += line_height + line_spacing
# 段落间距
y_offset += int(font_size * 0.4)
# 添加水印
if options.watermark:
watermark_font = self._load_font(12, False)
draw.text((options.width - 150, options.height - 40),
options.watermark_text, fill=(128, 128, 128), font=watermark_font)
# 保存图片
output_dir = Path("outputs")
output_dir.mkdir(exist_ok=True)
output_file = output_dir / f"pil_{os.getpid()}_{hash(text[:100]) % 10000}.{options.output_format}"
if options.output_format.lower() == 'png':
img.save(output_file, format="PNG", optimize=True)
else:
img.save(output_file, format="JPEG", quality=95, optimize=True)
return str(output_file)
def _load_font(self, size: int, bold: bool = False):
"""加载字体"""
font_candidates = [
"C:\\Windows\\Fonts\\msyh.ttc", # 微软雅黑
"C:\\Windows\\Fonts\\msyhbd.ttc", # 微软雅黑粗体
"C:\\Windows\\Fonts\\simhei.ttf", # 黑体
"C:\\Windows\\Fonts\\simsun.ttc", # 宋体
"C:\\Windows\\Fonts\\simkai.ttf", # 楷体
"C:\\Windows\\Fonts\\simfang.ttf", # 仿宋
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/STHeiti Light.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
]
if bold:
bold_candidates = [
"C:\\Windows\\Fonts\\msyhbd.ttc",
"C:\\Windows\\Fonts\\simhei.ttf",
]
for path in bold_candidates:
if os.path.exists(path):
try:
return ImageFont.truetype(path, size)
except Exception:
continue
for path in font_candidates:
if os.path.exists(path):
try:
return ImageFont.truetype(path, size)
except Exception:
continue
try:
return ImageFont.load_default(size=size)
except:
return ImageFont.load_default()
def _parse_markdown(self, text: str) -> List[Dict]:
"""解析Markdown文本"""
segments = []
lines = text.split('\n')
i = 0
while i < len(lines):
line = lines[i].strip()
if not line:
i += 1
continue
# 处理标题
if line.startswith('#'):
header_match = re.match(r'^(#{1,6})\s*(.*)', line)
if header_match:
level = len(header_match.group(1))
text_content = header_match.group(2)
segments.append({
'text': text_content,
'is_header': True,
'header_level': level
})
i += 1
continue
# 检测表格
if '|' in line:
table_data = []
while i < len(lines) and lines[i].strip():
table_line = lines[i].strip()
if '|' in table_line:
cells = [cell.strip() for cell in table_line.split('|')]
if cells and not cells[0]:
cells = cells[1:]
if cells and not cells[-1]:
cells = cells[:-1]
if not all('-' in cell or not cell.strip() for cell in cells):
table_data.append(cells)
else:
break
i += 1
if table_data:
segments.append({
'text': "",
'is_table': True,
'table_data': table_data
})
continue
# 处理加粗文本
parts = re.split(r'(\*\*[^*]+\*\*)', line)
for part in parts:
if part.startswith('**') and part.endswith('**') and len(part) > 4:
bold_text = part[2:-2]
if bold_text.strip():
segments.append({
'text': bold_text,
'is_bold': True
})
elif part.strip():
segments.append({
'text': part
})
i += 1
return segments
def _wrap_text(self, draw, text: str, font, max_width: int) -> List[str]:
"""文本换行"""
lines = []
current = ""
for ch in text.replace('\r', ''):
if ch == '\n':
lines.append(current)
current = ""
continue
probe = current + ch
w, _ = self._measure_text(draw, probe, font)
if w <= max_width:
current = probe
else:
if current:
lines.append(current)
current = ch
else:
lines.append(probe)
current = ""
if current:
lines.append(current)
return lines
def _measure_text(self, draw, text: str, font) -> Tuple[int, int]:
"""测量文本尺寸"""
try:
bbox = draw.textbbox((0, 0), text, font=font)
return bbox[2] - bbox[0], bbox[3] - bbox[1]
except Exception:
bbox = font.getbbox(text)
return bbox[2] - bbox[0], bbox[3] - bbox[1]
def _render_table(self, draw, table_data: List[List[str]], font,
start_x: int, start_y: int, max_width: int, text_color: Tuple[int, int, int]) -> int:
"""渲染表格"""
if not table_data:
return 0
col_count = max(len(row) for row in table_data) if table_data else 0
col_widths = []
for col in range(col_count):
max_col_width = 0
for row in table_data:
if col < len(row):
cell_text = row[col]
w, _ = self._measure_text(draw, cell_text, font)
max_col_width = max(max_col_width, w)
col_widths.append(max_col_width)
padding = 20
total_table_width = sum(col_widths) + padding * (col_count - 1)
if total_table_width > max_width:
scale_factor = max_width / total_table_width
col_widths = [int(w * scale_factor) for w in col_widths]
total_table_width = max_width
line_height = font.getbbox("Hg")[3] - font.getbbox("Hg")[1]
row_height = line_height + 10
current_y = start_y
for row_idx, row in enumerate(table_data):
current_x = start_x
for col_idx in range(col_count):
if col_idx < len(row):
cell_text = row[col_idx]
if row_idx == 0:
cell_font = self._load_font(font.size, bold=True)
else:
cell_font = font
draw.text((current_x, current_y), cell_text, fill=text_color, font=cell_font)
if col_idx < len(col_widths):
current_x += col_widths[col_idx] + padding
current_y += row_height
return current_y - start_y
def render_markdown_text_to_image(md_text: str, options: Optional[RenderOptions] = None) -> str:
"""渲染Markdown文本为图片文件"""
if options is None:
options = RenderOptions()
# 创建渲染器
renderer = MarkdownRenderer()
# 渲染图片
return renderer.render(md_text, options)
# 保留原有的接口兼容性
def render_markdown_text_to_image_legacy(md_text: str, options: Optional[RenderOptions] = None):
"""兼容原有接口的渲染函数"""
return render_markdown_text_to_image(md_text, options)
def get_available_backends():
"""获取可用的渲染后端"""
backends = {}
# 检查 imgkit
try:
import imgkit
backends['imgkit'] = {'available': True, 'version': 'installed'}
except ImportError:
backends['imgkit'] = {'available': False, 'error': 'not installed'}
# 检查 PIL
backends['pil'] = {'available': PIL_AVAILABLE}
# 检查 markdown
backends['markdown'] = {'available': IMGKIT_AVAILABLE}
return backends
def get_renderer_status():
"""获取渲染器状态"""
return {
'default_backend': 'imgkit-wkhtmltopdf',
'fallback_available': PIL_AVAILABLE,
'total_backends': len(['imgkit-wkhtmltopdf', 'markdown-pdf-cli', 'md-to-image-cli', 'md-to-image-api', 'pil-fallback'])
}
def get_backend_details():
"""获取后端详细信息"""
return {
'imgkit': {
'name': 'imgkit/wkhtmltopdf',
'priority': 1,
'available': IMGKIT_AVAILABLE,
'description': 'HTML rendering engine with wkhtmltopdf'
},
'pil': {
'name': 'PIL/Pillow',
'priority': 4,
'available': PIL_AVAILABLE,
'description': 'Python Imaging Library fallback'
}
}