From 09c58defa32b2dc874b798f63b5081c07f21d6f6 Mon Sep 17 00:00:00 2001 From: Quentin <1771956812@qq.com> Date: Tue, 29 Sep 2026 15:19:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(symbol):=20=E7=A7=81=E6=9C=89=E5=8C=BA(PUA)?= =?UTF-8?q?=E6=95=B0=E5=AD=A6=E7=AC=A6=E5=8F=B7=E5=A4=84=E7=90=86=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20+=20=E5=8E=BB=E6=8E=89=20jby=20CDN=20=E4=BE=9D?= =?UTF-8?q?=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: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) --- .../com/wordparse/config/WordParseConfig.java | 4 ++ .../com/wordparse/core/WordParseUtil.java | 49 ++++++++++++++++--- .../main/resources/application.yml.example | 1 + 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/java-svc/src/main/java/com/wordparse/config/WordParseConfig.java b/java-svc/src/main/java/com/wordparse/config/WordParseConfig.java index fbc965f..8eb1998 100644 --- a/java-svc/src/main/java/com/wordparse/config/WordParseConfig.java +++ b/java-svc/src/main/java/com/wordparse/config/WordParseConfig.java @@ -13,12 +13,16 @@ public class WordParseConfig { private Map 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 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; } diff --git a/java-svc/src/main/java/com/wordparse/core/WordParseUtil.java b/java-svc/src/main/java/com/wordparse/core/WordParseUtil.java index 425992b..465d0db 100644 --- a/java-svc/src/main/java/com/wordparse/core/WordParseUtil.java +++ b/java-svc/src/main/java/com/wordparse/core/WordParseUtil.java @@ -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 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 = "%s"; - 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 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 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 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("", ""); @@ -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; } diff --git a/java-svc/src/main/resources/application.yml.example b/java-svc/src/main/resources/application.yml.example index e51fcdf..a1ff2f0 100644 --- a/java-svc/src/main/resources/application.yml.example +++ b/java-svc/src/main/resources/application.yml.example @@ -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:}