# OnlyOffice xlsx 行高、合并单元格截断与渲染核查（2026-06-17 万达表确立）

汇总表的「校区整体风险分析」段是横跨整行的合并单元格（如 A11:L11），内容长达 800~1200+ 字、30~47 个逻辑行。OnlyOffice 渲染这类超长合并单元格时极易**截断显示**，而 Maggie 对「文字必须完整显示不截断」是硬要求。本文件记录踩过的坑、真实成因、以及可复用的核查方法。

## 一、行高 409.5/409.6pt 不是格式天花板，是 OnlyOffice 网页编辑器的 clamp 值

这是反复出现的谜题：**自己设了 760pt 的行，交付后再打开变成了 409.6pt。** 实测厘清三层行为，别再被误导：

| 层 | 行为 | 实测结论 |
|---|---|---|
| **xlsx 文件格式本身** | openpyxl 写 `row_dimensions[N].height = 900` → 存盘 → 读回 = 900 | ✅ 文件层**不限制**行高，900pt 能正常写入并保留 |
| **OnlyOffice x2t 批量引擎** | xlsx(900pt) 经容器内 x2t 做 xlsx→xlsx round-trip → 读回 row 仍 = 900 | ✅ **不 clamp**。从文件层写的高行高，x2t 渲染/转换链路认 |
| **OnlyOffice 网页版编辑器** | 在浏览器里打开编辑、点保存 | ❌ **会把超过 ~409.5pt 的行高压回 409.5/409.6**。这就是「我的 760 变成 409.6」的真凶 |

**结论与操作要点：**
- 「调高行高让全部内容显示」在技术上**可行**——只要**从文件层（openpyxl 脚本）写入并经交付链路上传**，不要让结果再被网页编辑器存盘一次。
- 一旦用户/自己在 OnlyOffice **网页端编辑并保存**过，超限行高就被打回 409.5。若交付后还要网页端再编辑，高行高保不住。
- 设了 `height` 即自动 `customHeight=True`（openpyxl 中 `customHeight` 无 setter，不要试图直接赋值，会 `AttributeError`）。
- **若 Maggie 明确说「就按当前最高行距，不用调」→ 不折腾，保持现状交付。** 别因为自己测出能调高就擅自改——方案≠授权。

## 二、数据完整 ≠ 视图不截断（核查别只看 pdftotext）

OnlyOffice/x2t 导 PDF 时，**被行高裁掉的文字仍会写进 PDF 文本层**。所以：
- `pdftotext` 提取到尾部锚点句 = 数据在文件里（867 字符全在），**但不等于在编辑器视图里可见**。
- 真正的「截断」是**视图层**问题（行高 < 内容所需高度时底部被裁），不是数据丢失。
- 因此「数据完整性」用 pdftotext 验，「视图是否截断」要靠**行高是否 ≥ 内容估算高度**来判断（见下方脚本），或渲染成图片肉眼看。
- ⚠️ 另一个坑：x2t 导 PDF 时**对超长合并单元格本身也会截断渲染**（PDF 里只画出前一部分，如只渲染「12,000」后面没了）——这是导出视图的固有限制，不代表数据丢。判断数据完整以 openpyxl 读单元格 `.value` 字符数为准。

## 三、可复用核查工具

### 行高分析脚本（再跑用）
`scripts/xlsx-rowheight-analyze.py <文件.xlsx>`：只读分析每个 sheet 的长内容单元格，按合并宽度 + 字号估算所需视觉行数和建议行高，与当前行高对比，标出「可能截断」的行。不改文件。中文每字≈2.1 宽度单位、每视觉行≈15.5pt（10pt 字）是经验系数。

### x2t round-trip 测 clamp / 渲染（本地 OnlyOffice 实例）
容器 `nextcloud-onlyoffice-1`，x2t 路径 `/var/www/onlyoffice/documentserver/server/FileConverter/bin/x2t`。
```bash
# xlsx→xlsx round-trip（测网页引擎是否 clamp 行高；format 257 = xlsx）
docker cp in.xlsx nextcloud-onlyoffice-1:/tmp/t/in.xlsx
docker exec nextcloud-onlyoffice-1 bash -c 'cat > /tmp/t/c.xml <<EOF
<?xml version="1.0" encoding="utf-8"?>
<TaskQueueDataConvert xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<m_sFileFrom>/tmp/t/in.xlsx</m_sFileFrom><m_sFileTo>/tmp/t/out.xlsx</m_sFileTo>
<m_nFormatTo>257</m_nFormatTo><m_bIsNoBase64>true</m_bIsNoBase64>
</TaskQueueDataConvert>
EOF
LD_LIBRARY_PATH=/var/www/onlyoffice/documentserver/server/FileConverter/bin /var/www/onlyoffice/documentserver/server/FileConverter/bin/x2t /tmp/t/c.xml'
# format 513 = PDF（渲染成 PDF 看版面）；之后主机 pdftoppm -png -r 110 out.pdf 转图
```
- x2t 退出码 `88` = 目标 format code 不对（不是文件坏）。xlsx→xlsx 用 257，xlsx→PDF 用 513；不要走 8193 bin 中间格式做 xlsx round-trip。
- 主机有 `soffice`/`libreoffice`、`pdftoppm`/`pdfinfo`/`pdftotext`、中文字体 wqy-zenhei，可做降级渲染。

### 截断检测（文本锚点法）
取目标长单元格**尾部**几句独特锚点，去掉空格后在 PDF 全文 grep——但记住第二节：命中只证明数据在，视图是否截断仍以行高估算为准。

## 四、一句话 SOP
1. 估算：`scripts/xlsx-rowheight-analyze.py 文件.xlsx` → 看哪些行 `当前行高 < 建议行高`。
2. 若需调高且用户同意：openpyxl 设 `row_dimensions[N].height = 建议值`，从文件层写、走交付链路上传，**别再用网页端编辑保存**。
3. 若用户说保持当前最高 → 不动。
4. 数据完整性单独用 openpyxl 读 `.value` 字符数确认，不靠 PDF。
