xlsx: get /<Sheet> reports freeze from topLeftCell, ignoring xSplit/ySplit
Summary
officecli get <file> /<SheetName> reports the freeze property by echoing the <pane> element's topLeftCell attribute instead of deriving it from xSplit / ySplit. Whenever a workbook was saved while scrolled away from the frozen boundary — the normal state of any real spreadsheet — the reported freeze position is wrong.
Writing is correct; only the readback is affected.
Environment
- officecli 1.0.149 (win-x64), Windows 11
- Reproduced on a file authored by Excel 16.0 and on a file created by officecli itself
Reproduction
Self-contained, no external file needed:
officecli create repro.xlsx
officecli set repro.xlsx /Sheet1/A1 --prop value="header"
officecli set repro.xlsx /Sheet1 --prop freeze=A2
officecli close repro.xlsx
officecli get repro.xlsx /Sheet1
# /Sheet1 (sheet) freeze=A2 <- correct
officecli raw repro.xlsx /Sheet1 | grep -o '<x:pane[^>]*>'
# <x:pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/>
# Simulate what Excel writes when the sheet is saved while scrolled down.
# Only topLeftCell changes; ySplit="1" is untouched, so the freeze is still row 1.
officecli raw-set repro.xlsx /Sheet1 --xpath "//x:pane" --action setattr --xml 'topLeftCell="A50"'
officecli close repro.xlsx
officecli get repro.xlsx /Sheet1
# /Sheet1 (sheet) freeze=A50 <- WRONG, should still be A2
officecli raw repro.xlsx /Sheet1 | grep -o '<x:pane[^>]*>'
# <x:pane ySplit="1" topLeftCell="A50" activePane="bottomLeft" state="frozen"/>Expected vs actual
<pane> |
Excel's actual freeze | get /Sheet1 reports |
|---|---|---|
ySplit="1" topLeftCell="A2" |
A2 | freeze=A2 ✅ |
ySplit="1" topLeftCell="A50" |
A2 | freeze=A50 ❌ |
ySplit="1" topLeftCell="A329" |
A2 | freeze=A329 ❌ |
The third row is from a real 369-row workbook saved by Excel with the cursor near the bottom. It reports freeze=A329 while the sheet is, and always was, frozen at A2.
Root cause
topLeftCell is the scroll position of the scrolled pane — it drifts every time the user saves while scrolled. The freeze boundary is defined solely by xSplit / ySplit. They coincide only in a freshly created file, which is why round-tripping through officecli alone looks correct and hides the bug.
Correct derivation:
freezeCell = columnLetter(xSplit + 1) + (ySplit + 1)with absent attributes treated as 0. Also worth guarding state: state="split" is a split pane, not a freeze, and state="frozenSplit" differs again.
The write path is already correct
set freeze=A5 -> <x:pane ySplit="4" topLeftCell="A5" activePane="bottomLeft" state="frozen"/>
set freeze=B2 -> <x:pane xSplit="1" ySplit="1" topLeftCell="B2" activePane="bottomRight" state="frozen"/>
set freeze=C10 -> <x:pane xSplit="2" ySplit="9" topLeftCell="C10" activePane="bottomRight" state="frozen"/>So the fix is confined to the getter.
Impact
The readback is silently and plausibly wrong — it returns a valid-looking cell reference, so there is no error to notice. An agent inspecting a workbook will report a wrong freeze configuration with full confidence, and a round-trip that reads freeze and writes it back would move a correct freeze pane to wherever the file happened to be scrolled.
Source: iOfficeAI/OfficeCLI