Files
Quentin 3271fee5cc init: WordFileParse docx→HTML 解析服务(含 Word EQ 域公式修复)
服务:Spring Boot 3.2 + docx4j 8.3.9,POST /api/v3/server_office_file/file/parse
      接受 .docx,返回 HTML(公式 → LaTeX,图片 → RustFS 外链)

公式支持三类来源:
- OMML(Word 2007+ 公式)      → Python omml_mathml2latex
- MathType OLE 对象            → Ruby mathtype2mathml → MathML → LaTeX
- Word EQ 域(老教辅主力)      → EqToLatexUtil(本次新增)

本次改动:
1. 新增 java-svc/.../core/EqToLatexUtil.java:
   移植自 jby backend-services office-core,EQ 语法 → LaTeX
   (\f→\dfrac、\r→\sqrt、\i→\int/\sum/\prod、\s→^{}/_{}、\b→\left\right、
     \a→array、\x→\boxed/\overline、\o\al 堆叠、圈码、化学符号、极限、连等式)
   品牌标记 JBYU* → WPU*,与采集端一致
2. CtoMathFinder:接上 EQ 分支(原为 not-ported 桩),域代码经
   EqToLatexUtil.convert 转 LaTeX 写入 eqLatexMap(WPEQNUM 占位符)
3. WordParseUtil.dealPlaceholder:EQ 的 LaTeX 同样加 \( \) 包裹
   (原先因 WPEQNUM 例外不加包裹,前端/MathJax 不渲染)

修前症状:教辅 Word 里分式/方程组/上下标整段丢失(如 {−1/3, 1} 变成 {-,1})
修后实测:46 份教辅 2908 处 EQ 域全部还原,公式总数 2954 处、零未包裹残留

安全:application.yml(含 RustFS 密钥)不入库,提供 application.yml.example;
      OssService 的 @Value 默认密钥已清空
2026-09-29 09:43:13 +08:00

144 lines
4.6 KiB
Python

"""
DOCX parser for Microsoft Word documents.
This parser handles .docx files, extracting OMML equations and converting
them to MathML format using the existing DOCX processing pipeline.
"""
import os
from typing import List
from ..core.interfaces import Parser
class DocxParser(Parser):
"""
Parser for Microsoft Word .docx files.
This parser handles .docx files by extracting OMML equations and
converting them to MathML using the existing DOCX processing pipeline.
Examples:
Parse DOCX file:
>>> parser = DocxParser()
>>> results = parser.parse("equations.docx")
>>> isinstance(results, list)
True
"""
def __init__(self):
"""Initialize the DOCX parser."""
pass
def parse(self, source: str) -> List[str]:
"""
Parse DOCX file and return list of MathML equation strings.
Args:
source: Path to .docx file
Returns:
List of MathML equation strings (extracted from DOCX)
Raises:
FileNotFoundError: If DOCX file doesn't exist
ValueError: If file is not a valid .docx file
RuntimeError: If DOCX processing fails
Examples:
>>> parser = DocxParser()
>>> # Assuming test.docx exists with equations
>>> results = parser.parse("test.docx")
>>> all(isinstance(eq, str) for eq in results)
True
"""
# Validate file exists
if not os.path.exists(source):
raise FileNotFoundError(f"DOCX file not found: {source}")
# Validate file extension
if not source.lower().endswith('.docx'):
raise ValueError(f"File must have .docx extension: {source}")
# Import here to avoid circular dependencies
from ..processing import OMMLExtractor
from ..core import omml_to_mathml
from ..utils.styling_postprocessor import apply_styling_to_mathml_from_omml
# Get XSL path (in core module)
current_dir = os.path.dirname(os.path.abspath(__file__))
core_dir = os.path.join(os.path.dirname(current_dir), 'core')
xsl_path = os.path.join(core_dir, 'OMML2MML.xsl')
try:
# Extract OMML from DOCX using OMMLExtractor
extractor = OMMLExtractor()
equation_dicts = extractor.extract_from_docx(source)
if not equation_dicts:
# No equations found
return []
# Convert each OMML to MathML
results = []
for eq_dict in equation_dicts:
try:
# Extract the cleaned OMML string from the dictionary
omml_str = eq_dict.get('cleaned_omml', eq_dict.get('omml', ''))
if not omml_str:
continue
# Step 1: XSLT transformation
mathml_str = omml_to_mathml(omml_str, xsl_path)
# Step 2: Post-process to fix table structure and add styling
# CRITICAL: This fixes the OMML2MML.xsl bug where <mtr><mrow> should be <mtr><mtd>
mathml_str = apply_styling_to_mathml_from_omml(omml_str, mathml_str)
if mathml_str and mathml_str.strip():
results.append(mathml_str)
except Exception as e:
# Log error but continue with other equations
print(f"Warning: Failed to convert OMML equation: {e}")
continue
return results
except Exception as e:
raise RuntimeError(f"DOCX processing failed: {e}")
def can_handle(self, source: str) -> bool:
"""
Check if this parser can handle the given input source.
Args:
source: Input source to check
Returns:
True if source is a .docx file, False otherwise
Examples:
>>> parser = DocxParser()
>>> parser.can_handle("equations.docx")
True
>>> parser.can_handle("<math><mi>x</mi></math>")
False
"""
# Check file extension
if source.lower().endswith('.docx'):
return True
# Check if it's an existing file with .docx extension
if os.path.exists(source) and source.lower().endswith('.docx'):
return True
return False
@property
def parser_type(self) -> str:
"""Get the parser type identifier."""
return "docx"
def __repr__(self) -> str:
"""String representation for debugging."""
return "DocxParser()"