服务: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 默认密钥已清空
186 lines
5.7 KiB
Python
186 lines
5.7 KiB
Python
"""
|
|
Text processing utilities for file I/O and LaTeX formatting.
|
|
|
|
This module provides functions for reading/writing text files and converting
|
|
various space characters to appropriate LaTeX spacing commands.
|
|
"""
|
|
|
|
import os
|
|
|
|
|
|
def read_text(path: str, encoding: str = "utf-8") -> str:
|
|
"""Read text from a file.
|
|
|
|
Args:
|
|
path: Path to the file to read
|
|
encoding: Text encoding (default: utf-8)
|
|
|
|
Returns:
|
|
File contents as string
|
|
|
|
Example:
|
|
>>> content = read_text('input.tex')
|
|
"""
|
|
with open(path, "r", encoding=encoding) as f:
|
|
return f.read()
|
|
|
|
|
|
def write_text(path: str, content: str, encoding: str = "utf-8") -> None:
|
|
"""Write text to a file, creating parent directories if needed.
|
|
|
|
Args:
|
|
path: Path to the file to write
|
|
content: Text content to write
|
|
encoding: Text encoding (default: utf-8)
|
|
|
|
Example:
|
|
>>> write_text('output.tex', '\\\\sin x')
|
|
"""
|
|
dirpath = os.path.dirname(path)
|
|
if dirpath:
|
|
os.makedirs(dirpath, exist_ok=True)
|
|
with open(path, "w", encoding=encoding) as f:
|
|
f.write(content)
|
|
|
|
|
|
def convert_spaces_to_latex(text: str) -> str:
|
|
"""Convert space characters in text to appropriate LaTeX spacing commands.
|
|
|
|
Handles:
|
|
- U+00A0 (NBSP, \\xa0) → ~ (tilde, non-breaking space in LaTeX)
|
|
- U+0020 (regular space) → \\\\ (backslash-space) for each space
|
|
|
|
Args:
|
|
text: Input text possibly containing various space characters
|
|
|
|
Returns:
|
|
Text with spaces converted to LaTeX commands
|
|
|
|
Example:
|
|
>>> convert_spaces_to_latex('sin x')
|
|
'sin\\\\ x'
|
|
>>> convert_spaces_to_latex('sin\\xa0x') # NBSP
|
|
'sin~x'
|
|
"""
|
|
if not text:
|
|
return text
|
|
|
|
# Quick check: if no spaces present, return as-is
|
|
if '\xa0' not in text and ' ' not in text:
|
|
return text
|
|
|
|
result = []
|
|
i = 0
|
|
|
|
while i < len(text):
|
|
char = text[i]
|
|
|
|
if char == '\xa0': # U+00A0: Non-breaking space
|
|
# Always use tilde for NBSP in LaTeX
|
|
result.append('~')
|
|
i += 1
|
|
|
|
elif char == ' ': # U+0020: Regular space
|
|
# Count consecutive spaces
|
|
space_count = 1
|
|
while i + space_count < len(text) and text[i + space_count] == ' ':
|
|
space_count += 1
|
|
|
|
# Convert each space to backslash-space (backslash followed by space)
|
|
# For N spaces, produce "\\ " repeated N times: "\\ \\ \\ "
|
|
# Each '\\ ' is already a complete unit, so use empty join (not space join)
|
|
result.append('\\ ' * space_count)
|
|
|
|
i += space_count # Skip all processed spaces
|
|
|
|
else:
|
|
# Regular character
|
|
result.append(char)
|
|
i += 1
|
|
|
|
# Join results but preserve trailing spaces that are part of LaTeX commands
|
|
result_str = ''.join(result)
|
|
# Don't strip if the string ends with LaTeX space command '\\ '
|
|
# The trailing space in '\\ ' is semantically significant
|
|
if result_str.endswith('\\ '):
|
|
return result_str
|
|
else:
|
|
return result_str.rstrip() # Remove trailing space from last element
|
|
|
|
|
|
def escape_latex_special_chars(text: str, preserve_nbsp: bool = True) -> str:
|
|
"""Escape special characters that have meaning in LaTeX.
|
|
|
|
LaTeX special characters that need escaping:
|
|
- $ (math mode delimiter)
|
|
- ^ (superscript)
|
|
- _ (subscript)
|
|
- # (parameter token)
|
|
- @ (letter category code)
|
|
- ! (negative thin space)
|
|
- % (comment)
|
|
- & (alignment tab)
|
|
- * (discretionary multiplication)
|
|
- ~ (tilde) → \\sim in math mode
|
|
|
|
Args:
|
|
text: Input text possibly containing special characters
|
|
preserve_nbsp: If True, preserve NBSP (\\xa0) for later conversion to ~
|
|
If False, convert NBSP to ~ before escaping
|
|
|
|
Returns:
|
|
Text with special characters escaped
|
|
|
|
Example:
|
|
>>> escape_latex_special_chars('$100 ^ 2')
|
|
'\\$100 \\^ 2'
|
|
>>> escape_latex_special_chars('H_2O')
|
|
'H\\_2O'
|
|
>>> escape_latex_special_chars('a ~ b')
|
|
'a \\sim b'
|
|
"""
|
|
if not text:
|
|
return text
|
|
|
|
# CRITICAL: Preserve marker strings used for brace resolution
|
|
# These markers distinguish source braces from generated LaTeX braces
|
|
if text in ('__LBRACE__', '__RBRACE__', '__DELIM__'):
|
|
return text
|
|
|
|
# Use a temporary placeholder for NBSP to preserve it during escaping
|
|
# This allows NBSP to be converted to ~ later for LaTeX spacing
|
|
NBSP_PLACEHOLDER = '\x00NBSP\x00' # Using null bytes as unlikely to appear in text
|
|
|
|
if preserve_nbsp and '\xa0' in text:
|
|
# Temporarily replace NBSP with placeholder
|
|
text = text.replace('\xa0', NBSP_PLACEHOLDER)
|
|
|
|
# Mapping of special characters to their escaped versions
|
|
# NOTE: In math mode, * is a valid operator and doesn't need escaping
|
|
# NOTE: { and } are NOT escaped here - they're handled via __LBRACE__/__RBRACE__ markers
|
|
# to distinguish source braces from generated LaTeX braces (e.g., \mathrm{...})
|
|
ESCAPE_MAP = {
|
|
'$': r'\$',
|
|
'%': r'\%',
|
|
'#': r'\#',
|
|
'&': r'\&',
|
|
'_': r'\_',
|
|
'^': r'\^',
|
|
# NOTE: '~' is NOT escaped here - it's handled via UNICODE_TO_LATEX mapping to \sim
|
|
'@': r'\@',
|
|
'!': r'\!',
|
|
# NOTE: '*' is NOT escaped - it's a valid multiplication operator in math mode
|
|
'{': r'\{',
|
|
'}': r'\}',
|
|
}
|
|
|
|
result = text
|
|
for char, escaped in ESCAPE_MAP.items():
|
|
result = result.replace(char, escaped)
|
|
|
|
# Restore NBSP placeholder back to NBSP for later processing
|
|
if preserve_nbsp and NBSP_PLACEHOLDER in result:
|
|
result = result.replace(NBSP_PLACEHOLDER, '\xa0')
|
|
|
|
return result
|