Files
abot/utils/markdown_to_image.py
2025-05-26 14:02:05 +08:00

229 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import markdown
from playwright.async_api import async_playwright
import os
import asyncio
def playwright_browser_installed():
"""检查 Playwright 的 Chromium 浏览器是否已安装"""
chromium_path = os.path.expanduser("~/.cache/ms-playwright/chromium")
return os.path.exists(chromium_path)
# linux 下需要安装字体
# sudo apt-get install -y fonts-noto-cjk fonts-noto-cjk-extra
# sudo apt-get install -y fonts-noto-color-emoji fonts-noto-cjk fonts-wqy-microhei
# 将 Markdown 字符串转换为 HTML
def md_str_to_html(md_content, output_html):
"""
将 Markdown 字符串转换为 HTML 文件,并添加支持中文和 Emoji 的样式。
:param md_content: 输入的 Markdown 字符串
:param output_html: 输出的 HTML 文件路径
"""
# 转换 Markdown 为 HTML启用额外功能如表格、代码高亮
html_content = markdown.markdown(md_content, extensions=['extra', 'codehilite'])
# 添加基本的 HTML 结构和样式,支持中文和 Emoji
css = """
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', 'Noto Sans CJK SC', 'Microsoft YaHei', sans-serif;
padding: 20px 25px;
line-height: 1.6;
max-width: 750px;
margin: 0 auto;
background-color: #f9f9f9;
color: #333;
border: 1px solid #f0f0f0;
font-size: 16px;
}
h1, h2, h3, h4, h5, h6 {
color: #222;
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.3;
}
h1 {
font-size: 2.2em;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
text-align: center;
margin-bottom: 25px;
color: #1a1a1a;
}
h2 {
font-size: 1.8em;
padding-bottom: 10px;
margin-top: 30px;
border-bottom: 1px solid #eee;
color: #2c3e50;
}
h3 {
font-size: 1.5em;
margin-top: 25px;
padding-left: 12px;
border-left: 4px solid #ddd;
color: #34495e;
}
pre, code {
background-color: #f5f5f5;
padding: 12px;
border-radius: 4px;
font-family: 'Courier New', Courier, monospace;
font-size: 0.95em;
border: 1px solid #eee;
}
table {
border-collapse: collapse;
width: 100%;
margin: 18px 0;
background-color: white;
}
th, td {
border: 1px solid #eee;
padding: 10px 12px;
text-align: left;
}
th {
background-color: #fafafa;
font-weight: 600;
}
/* 确保 Emoji 正确渲染 */
span, p, li {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', 'Noto Sans CJK SC', 'Microsoft YaHei', sans-serif;
font-size: 16px;
}
p {
margin: 16px 0;
color: #333;
line-height: 1.8;
font-size: 16px;
}
ul, ol {
padding-left: 25px;
margin: 18px 0;
}
li {
margin: 8px 0;
line-height: 1.7;
font-size: 16px;
}
blockquote {
margin: 18px 0;
padding: 12px 18px;
background-color: #f8f8f8;
border-left: 5px solid #ddd;
color: #555;
font-size: 1em;
}
/* 强调样式 */
strong {
color: #222;
font-weight: 600;
}
/* 链接样式 */
a {
color: #3498db;
text-decoration: none;
}
a:hover {
text-decoration: underline;
color: #2980b9;
}
/* 星级评分样式 */
h3 em {
color: #fa8c16;
font-style: normal;
font-size: 1.1em;
}
/* 时间和标签样式 */
.time, .tag {
color: #777;
font-size: 0.95em;
}
/* 底部署名样式 */
.signature {
margin-top: 35px;
text-align: right;
color: #777;
font-size: 0.95em;
font-style: italic;
}
</style>
"""
# 写入 HTML 文件
with open(output_html, 'w', encoding='utf-8') as f:
f.write('<html><head>')
f.write('<meta charset="UTF-8">') # 确保 UTF-8 编码
f.write(css)
f.write('</head><body>')
f.write(html_content)
f.write('</body></html>')
# 使用 Playwright 将 HTML 渲染并截图(异步版)
async def html_to_image(html_file, output_image):
"""
使用 Playwright 加载 HTML 文件并截图(异步)。
"""
try:
async with async_playwright() as p:
# Windows 系统
if os.name == 'nt':
possible_chrome_paths = [
r"C:\Users\Liu_WIN10\AppData\Local\Google\Chrome\Application\chrome.exe",
r"C:\Users\Liu-OPEN\AppData\Local\Google\Chrome\Application\chrome.exe",
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
]
browser_path = next((p for p in possible_chrome_paths if os.path.exists(p)), None)
if browser_path:
print(f"找到浏览器路径: {browser_path}")
browser = await p.chromium.launch(executable_path=browser_path)
else:
print("未找到 Chrome尝试使用 Playwright 自带浏览器")
if not playwright_browser_installed():
print("Playwright 浏览器未安装,正在安装...")
import subprocess
subprocess.run(["playwright", "install", "chromium"], check=True)
print("安装完成")
browser = await p.chromium.launch()
else:
if not playwright_browser_installed():
print("Playwright 浏览器未安装,正在安装...")
import subprocess
subprocess.run(["playwright", "install", "chromium"], check=True)
print("安装完成")
browser = await p.chromium.launch()
page = await browser.new_page()
await page.goto(f'file://{os.path.abspath(html_file)}')
await page.set_viewport_size({"width": 750, "height": 800})
await page.wait_for_timeout(500)
await page.screenshot(path=output_image, full_page=True)
await browser.close()
except Exception as e:
print(f"浏览器操作失败: {e}")
if "Executable doesn't exist" in str(e):
print("请运行 'playwright install' 命令安装必要的浏览器组件")
raise
# 主函数:从字符串转换 Markdown 到图片(异步版)
async def convert_md_str_to_image(md_content, output_image):
"""
将 Markdown 字符串转换为图片(异步)。
"""
temp_html = 'temp_output.html'
md_str_to_html(md_content, temp_html)
await html_to_image(temp_html, output_image)
os.remove(temp_html)
print(f"图片已生成:{output_image}")
return os.path.abspath(output_image)
# 示例使用
if __name__ == "__main__":
# 示例 Markdown 字符串(包含中文和 Emoji
md_content = """### Python的类型\n1. **数字类型**\n - **整数int**:用于表示整数,例如`5`、`-10`。在Python 3中整数的长度不受限制可以表示任意大小的整数。\n - **浮点数float**:用于表示带有小数部分的数字,例如`3.14`、`-0.5`。它采用IEEE 754标准来存储可能会存在精度问题。\n - **复数complex**:由实数部分和虚数部分组成,例如`3 + 4j`,其中`j`表示虚数单位。\n2. **序列类型**\n - **字符串str**:用于表示文本,是不可变的字符序列,例如`'Hello, World!'`、`\"Python\"`。可以通过索引和切片操作访问其中的字符。\n - **列表list**:是可变的有序序列,可以包含不同类型的元素,例如`[1, 'apple', 3.14]`。支持添加、删除、修改元素等操作。\n - **元组tuple**:与列表类似,但它是不可变的有序序列,例如`(1, 'apple', 3.14)`。一旦创建,其元素不能被修改。\n3. **映射类型**\n - **字典dict**:是一种无序的键值对集合,用于存储和检索数据,例如`{'name': 'Alice', 'age': 25}`。键必须是唯一且不可变的,值可以是任意类型。\n4. **集合类型**\n - **集合set**:是一个无序的不重复元素集合,例如`{1, 2, 3}`。常用于去重和数学集合运算,如并集、交集、差集等。\n - **冻结集合frozenset**:与集合类似,但它是不可变的,一旦创建就不能修改。\n\n### 将时间戳转换为str\n在Python中可以使用`datetime`模块来完成时间戳转换为字符串的操作。以下是示例代码:\n\n```python\nimport datetime\n\n\n# 假设时间戳为秒级时间戳\ntimestamp = 1672531200\n# 将时间戳转换为datetime对象\ndt = datetime.datetime.fromtimestamp(timestamp)\n# 将datetime对象格式化为字符串\nstr_time = dt.strftime('%Y-%m-%d %H:%M:%S')\nprint(str_time)\n```\n\n在上述代码中:\n1. `import datetime`导入`datetime`模块。\n2. `datetime.datetime.fromtimestamp(timestamp)`将时间戳(这里假设为秒级时间戳)转换为`datetime`对象。\n3. `dt.strftime('%Y-%m-%d %H:%M:%S')`使用`strftime`方法将`datetime`对象格式化为指定格式的字符串,`%Y`表示四位数的年份,`%m`表示两位数的月份,`%d`表示两位数的日期,`%H`表示24小时制的小时`%M`表示分钟,`%S`表示秒。\n\n如果时间戳是毫秒级的,需要先将其转换为秒级,例如:\n\n```python\nimport datetime\n\n\n# 假设时间戳为毫秒级时间戳\ntimestamp = 1672531200000\n# 将毫秒级时间戳转换为秒级\ntimestamp_seconds = timestamp / 1000\n# 将时间戳转换为datetime对象\ndt = datetime.datetime.fromtimestamp(timestamp_seconds)\n# 将datetime对象格式化为字符串\nstr_time = dt.strftime('%Y-%m-%d %H:%M:%S')\nprint(str_time)\n"""
spath = asyncio.run(convert_md_str_to_image(md_content, "output.png"))
print(spath)