fix(symbol): 私有区(PUA)数学符号处理修复 + 去掉 jby CDN 依赖
问题:docx 里的私有区符号(教辅排版软件把 ⊆/⇌ 等写成 PUA)输出成豆腐块。
实测语料:46 份教辅里 \ue089 共 11 处(3 份文件)全部丢失。
根因:
1. SYMBOL 数组沿用反编译产物的字面量写法 {"\\ue0fc", "\\ue100", "\\ue101"}
(反斜杠+ue0fc 共 7 字符),而文档里是 1 个 PUA 字符,replace 永远不命中
→ 整个符号占位符机制从未生效;连带 sym.substring(2) 在真字符下取不到编号。
2. 只有 3 个硬编码码位,没有通用 PUA→Unicode 映射。
修复:
- SYMBOL 改为真 PUA 字符;编号统一走 symbolCode()(%04X,与 static/symbols 文件名一致)
- 新增 PUA_TO_UNICODE(证据驱动):\ue089 → ⊆
- 新增 warnUnknownPua():扫描 E000–F8FF 未映射码位并 WARN,便于持续扩充
- 新增配置 wordparse.symbol-base-url(默认我们自己的 RustFS
https://fs.code-lab.cn/temp/wordparse-symbols/),不再引用 resource.xinjiaoyu.com
- ⇌(\ue225\ue226)替换移出循环体
验证:含真 PUA 字节的 docx 过服务后 —— E0FC/E100/E101 输出我们托管的 svg 图、
\ue089→⊆、\ue225\ue226→\(\rightleftharpoons\)、标准 ⊆ 原样保留、残留 PUA=0;
3 份受影响教辅已重新生成并覆盖线上(exercise-html3)
This commit is contained in:
@@ -13,12 +13,16 @@ public class WordParseConfig {
|
||||
private Map<String, String[]> cleanStyleMap = Map.of();
|
||||
private String transformScriptsPath = "/opt/transform";
|
||||
private String pythonCommand = "python3";
|
||||
/** 私有区符号图的基址(默认我们自己的 RustFS,不依赖 jby CDN) */
|
||||
private String symbolBaseUrl = "https://fs.code-lab.cn/temp/wordparse-symbols/";
|
||||
|
||||
public String getTmpPath() { return tmpPath; }
|
||||
public Double getImageScalingFactor() { return imageScalingFactor; }
|
||||
public Map<String, String[]> getCleanStyleMap() { return cleanStyleMap; }
|
||||
public String getTransformScriptsPath() { return transformScriptsPath; }
|
||||
public String getPythonCommand() { return pythonCommand; }
|
||||
public String getSymbolBaseUrl() { return symbolBaseUrl; }
|
||||
public void setSymbolBaseUrl(String symbolBaseUrl) { this.symbolBaseUrl = symbolBaseUrl; }
|
||||
|
||||
public void setTmpPath(String v) { this.tmpPath = v; }
|
||||
public void setImageScalingFactor(Double v) { this.imageScalingFactor = v; }
|
||||
|
||||
@@ -69,8 +69,39 @@ public class WordParseUtil {
|
||||
this.ossService = ossService;
|
||||
}
|
||||
|
||||
/** PUA 字符 → 4 位十六进制编号(与 static/symbols 下的文件名一致) */
|
||||
private static String symbolCode(String sym) {
|
||||
return String.format("%04X", (int) sym.charAt(0));
|
||||
}
|
||||
|
||||
/** 记录未映射的私有区字符,便于后续扩充 PUA_TO_UNICODE */
|
||||
private static void warnUnknownPua(String content) {
|
||||
java.util.Set<String> unknown = new java.util.TreeSet<>();
|
||||
for (int i = 0; i < content.length(); i++) {
|
||||
char c = content.charAt(i);
|
||||
if (c >= 0xE000 && c <= 0xF8FF) {
|
||||
String s = String.valueOf(c);
|
||||
if (!PUA_TO_UNICODE.containsKey(s) && !java.util.Arrays.asList(SYMBOL).contains(s)) {
|
||||
unknown.add(String.format("U+%04X", (int) c));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!unknown.isEmpty()) {
|
||||
log.warn("存在未映射的私有区符号(建议补充 PUA_TO_UNICODE): {}", String.join(",", unknown));
|
||||
}
|
||||
}
|
||||
|
||||
private static final String MATH_PLACEHOLDER = "<w:rPr><w:rFonts w:hint=\"eastAsia\"/><w:lang w:val=\"en-US\" w:eastAsia=\"zh-CN\"/></w:rPr><w:t>%s</w:t>";
|
||||
private static final String[] SYMBOL = {"\\ue0fc", "\\ue100", "\\ue101"};
|
||||
private static final String[] SYMBOL = {"\ue0fc", "\ue100", "\ue101"};
|
||||
|
||||
/**
|
||||
* 私有区(PUA)符号码位 → 标准 Unicode。
|
||||
* 教辅排版软件把 ⊆ 等数学符号写成 PUA,docx4j 会原样输出,前端显示成豆腐块。
|
||||
* 证据驱动映射:未收录的码位由 warnUnknownPua 记日志,确认含义后再补。
|
||||
*/
|
||||
private static final Map<String, String> PUA_TO_UNICODE = Map.of(
|
||||
"\ue089", "⊆" // 10阶段滚动检测(一):B?A / {2,3}?A,与同文档内真 ⊆ 用法完全一致
|
||||
);
|
||||
|
||||
// ---------- Class-level constants (brand-neutral replacements for jby Constants) ----------
|
||||
private static final Set<String> RETAIN_CLASS_VALUE_SET = Set.of(
|
||||
@@ -158,8 +189,13 @@ public class WordParseUtil {
|
||||
content = content.replace("\u00A0", " ");
|
||||
content = content.replace("\uf028", "(");
|
||||
content = content.replace("\uf029", ")");
|
||||
// PUA 符号 → 标准 Unicode(先于 SYMBOL 占位符,避免冲突)
|
||||
for (Map.Entry<String, String> pua : PUA_TO_UNICODE.entrySet()) {
|
||||
content = content.replace(pua.getKey(), pua.getValue());
|
||||
}
|
||||
warnUnknownPua(content);
|
||||
for (String sl : SYMBOL) {
|
||||
content = content.replace(sl, "@@@" + sl.substring(2).toUpperCase() + "@@@");
|
||||
content = content.replace(sl, "@@@" + symbolCode(sl) + "@@@");
|
||||
}
|
||||
content = content.replace("<w:shd w:val=\"pct15\" w:color=\"auto\" w:fill=\"FFFFFF\"/>",
|
||||
"<w:shd w:val=\"clear\" w:color=\"FFFFFF\" w:fill=\"D9D9D9\"/>");
|
||||
@@ -1200,14 +1236,13 @@ public class WordParseUtil {
|
||||
"$1");
|
||||
for (String sym : SYMBOL) {
|
||||
Element imgElement = new Element("img");
|
||||
imgElement.attr(HtmlAttr.SRC.getValue(),
|
||||
"https://resource.xinjiaoyu.com/prod/office/unicode-symbol/"
|
||||
+ sym.substring(2).toUpperCase() + ".svg");
|
||||
imgElement.attr(HtmlAttr.SRC.getValue(), config.getSymbolBaseUrl() + symbolCode(sym) + ".svg");
|
||||
imgElement.attr(HtmlAttr.WIDTH.getValue(), "14");
|
||||
imgElement.attr(HtmlAttr.HEIGHT.getValue(), "14");
|
||||
result = result.replace("@@@" + sym.substring(2).toUpperCase() + "@@@", imgElement.outerHtml());
|
||||
result = result.replace("\ue225\ue226", "\\(\\rightleftharpoons\\)");
|
||||
result = result.replace("@@@" + symbolCode(sym) + "@@@", imgElement.outerHtml());
|
||||
}
|
||||
// ⇌(U+E225 U+E226 成对出现)直接转 LaTeX
|
||||
result = result.replace("\ue225\ue226", "\\(\\rightleftharpoons\\)");
|
||||
writeHtml(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ wordparse:
|
||||
image-scaling-factor: 2.0
|
||||
transform-scripts-path: /opt/transform
|
||||
python-command: python3
|
||||
symbol-base-url: https://fs.code-lab.cn/temp/wordparse-symbols/
|
||||
oss:
|
||||
endpoint: https://fs.code-lab.cn
|
||||
access-key: ${WORDPARSE_OSS_AK:}
|
||||
|
||||
Reference in New Issue
Block a user