Files
to_docx/server/routers/doc_router.py
T
Chen Xiao 08a05b088a First upload.
Signed-off-by: Chen Xiao <abigwc@gmail.com>
2026-05-09 14:40:49 +08:00

131 lines
5.1 KiB
Python

import os
import tempfile
import pypandoc
from typing import Optional
from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import FileResponse
from pydantic import BaseModel
from starlette.background import BackgroundTask
import docx
from docx.shared import Pt
from docx.oxml.ns import qn
import auth
import db
from formula_processor import preprocess_formulas
router = APIRouter()
class StyleOptions(BaseModel):
base_font_size: Optional[int] = 12
base_font_family: Optional[str] = "宋体"
h1_size: Optional[int] = 18
h2_size: Optional[int] = 16
h3_size: Optional[int] = 14
h4_size: Optional[int] = 12
class DocRequest(BaseModel):
markdown: str
styleOptions: Optional[StyleOptions] = None
def remove_file(path: str):
try:
os.remove(path)
except Exception:
pass
@router.post("/api/convert")
async def convert_md_to_docx(req: DocRequest, current_user: Optional[dict] = Depends(auth.get_optional_user)):
try:
# 如果带有合法登录 token,则记录历史
if current_user:
db.add_history_record(current_user["id"], req.markdown)
try:
pypandoc.get_pandoc_version()
except OSError:
pypandoc.download_pandoc()
# ====== 公式预处理:统一各种定界符格式 ======
# 将 GPT/AI 生成内容中 \(...\)、\[...\]、缺失反斜杠的 [...] 和 (...)
# 全部转换为 Pandoc 可识别的标准 $...$ 和 $$...$$ 格式
processed_markdown = preprocess_formulas(req.markdown)
fd, tmp_path = tempfile.mkstemp(suffix=".docx")
os.close(fd)
md_fd, md_tmp_path = tempfile.mkstemp(suffix=".md")
with os.fdopen(md_fd, 'w', encoding='utf-8') as f:
f.write(processed_markdown)
pypandoc.convert_file(
md_tmp_path,
'docx',
format='markdown+tex_math_dollars',
outputfile=tmp_path
)
os.remove(md_tmp_path)
# ====== 样式后处理 ======
if req.styleOptions:
doc = docx.Document(tmp_path)
def set_font(style_name, font_name, font_size):
try:
style = doc.styles[style_name]
if font_size:
style.font.size = Pt(font_size)
if font_name:
style.font.name = font_name
style._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)
except KeyError:
pass
set_font('Normal', req.styleOptions.base_font_family, req.styleOptions.base_font_size)
set_font('Heading 1', req.styleOptions.base_font_family, req.styleOptions.h1_size)
set_font('Heading 2', req.styleOptions.base_font_family, req.styleOptions.h2_size)
set_font('Heading 3', req.styleOptions.base_font_family, req.styleOptions.h3_size)
set_font('Heading 4', req.styleOptions.base_font_family, req.styleOptions.h4_size)
set_font('Heading 5', req.styleOptions.base_font_family, req.styleOptions.h4_size)
set_font('Heading 6', req.styleOptions.base_font_family, req.styleOptions.h4_size)
# 强制遍历并覆盖所有段落的 run,确保样式生效
for p in doc.paragraphs:
# 判定当前段落是不是标题
current_font_size = req.styleOptions.base_font_size
current_font_family = req.styleOptions.base_font_family
if p.style.name.startswith('Heading'):
level_str = p.style.name.replace('Heading ', '')
try:
level = int(level_str)
if level == 1: current_font_size = req.styleOptions.h1_size
elif level == 2: current_font_size = req.styleOptions.h2_size
elif level == 3: current_font_size = req.styleOptions.h3_size
elif level >= 4: current_font_size = req.styleOptions.h4_size
except:
pass
# 为该段落中每一个具体的 run 设置字体
for run in p.runs:
run.font.name = current_font_family
run._element.rPr.rFonts.set(qn('w:eastAsia'), current_font_family)
if current_font_size:
run.font.size = Pt(current_font_size)
doc.save(tmp_path)
return FileResponse(
tmp_path,
media_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
filename='document.docx',
background=BackgroundTask(remove_file, tmp_path)
)
except Exception as e:
print(f"Error during conversion: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/history")
async def get_history(current_user: dict = Depends(auth.get_current_user)):
history = db.get_history_records(current_user["id"])
return {"history": history}