Skip to content

fix(xlsx): write an image without anchor.to as a oneCellAnchor - #609

Open
ronistrom wants to merge 1 commit into
productdevbook:mainfrom
ronistrom:fix/image-one-cell-anchor
Open

ronistrom wants to merge 1 commit into
productdevbook:mainfrom
ronistrom:fix/image-one-cell-anchor

Conversation

@ronistrom

@ronistrom ronistrom commented Oct 20, 2026 •

Copy link
Copy Markdown

Closes #598

What was wrong

writeDrawing always wrote images as xdr:twoCellAnchor. When anchor.to was absent, it made up the bottom-right corner as from plus 3 columns and 5 rows. width/height only reached the picture's <a:ext>, which Excel and LibreOffice ignore under a two-cell anchor. So the image was stretched over a 3 × 5 cell box whatever size it was given.

This is the issue's reproduction, a 200 × 40 px logo at B2 with no to:

images: [{ data: png, type: "png", anchor: { from: { row: 1, col: 1 } }, width: 200, height: 40 }]
<!-- before -->
<xdr:twoCellAnchor>
  <xdr:from><xdr:col>1</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>1</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
  <xdr:to><xdr:col>4</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>6</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
  <xdr:pic>…</xdr:pic><xdr:clientData/>
</xdr:twoCellAnchor>
<!-- after -->
<xdr:oneCellAnchor>
  <xdr:from><xdr:col>1</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>1</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
  <xdr:ext cx="1905000" cy="381000"/>
  <xdr:pic>…</xdr:pic><xdr:clientData/>
</xdr:oneCellAnchor>

Reading the file back with readXlsx:

before: {"anchor":{"from":{"row":1,"col":1},"to":{"row":6,"col":4}},"width":200,"height":40}
after:  {"anchor":{"from":{"row":1,"col":1}},"width":200,"height":40}

Why the tests didn't catch it

test/images.test.ts had a test called "uses default to anchor when not specified" that asserted the invented to (col 4, row 7 for an image at col 1, row 2). The test pinned the bug. The oneCellAnchor tests in test/one-cell-anchor.test.ts only read hand-built files, so nothing checked what the writer produces for an image without to. The write → read parity register's image probe always passed a to.

What landed

  • src/xlsx/drawing-writer.ts: when anchor.to is absent, the image is written as xdr:oneCellAnchor. It holds xdr:from, then <xdr:ext cx cy> from width/height × 9525 EMU (or the existing default of 3000000 × 2000000 EMU), then the same xdr:pic and <xdr:clientData/>. Both anchor kinds share one pic. When to is given, the twoCellAnchor output is byte-identical to before, and a test pins the full string.
  • The reader is unchanged. parseOneCellAnchor already returns { from } plus the size from xdr:ext, so the image reads back as written. The comment in reader.ts that said hucre always writes twoCellAnchor (hucre's own XLSX output is not fully readable by hucre — eight write-only fields #407) is updated.
  • Doc comments on SheetImage.anchor and width/height now say which anchor each case is written as and where the size goes.
  • Rendering change. An image written without anchor.to now shows at its width/height, or at the default size of about 315 × 210 px. It no longer fills 3 columns × 5 rows, and it no longer resizes with those cells. Callers who want the old stretched layout can pass anchor.to. This is in the commit body, which is where release notes come from.
  • Tests:
    • The default-to test in images.test.ts now expects a oneCellAnchor with the default ext.
    • Seven new tests in one-cell-anchor.test.ts cover:
      • the issue's reproduction, including child order from, ext, pic, clientData
      • read-back with no to
      • the default size when width/height are absent
      • altText/title surviving in the one-cell form
      • the byte-identical twoCellAnchor when to is given
      • both anchor kinds on one sheet, in declaration order
      • openXlsx → saveXlsx
    • The parity register's image probe gains an image without to.

Eight tests fail against main: the corrected images.test.ts test, the parity register, and six of the seven new tests. The seventh is the byte-identical twoCellAnchor guard, which already passed. bun run test passes on Node 24 (lint, typecheck, and 240 files with 10,705 tests).

Not in this PR: text boxes and charts still invent a to when it is absent (+3/+3 and +8/+15). Text boxes need reader work first, because the reader takes text boxes only from twoCellAnchor. Charts have no size field to put in an ext. Both are left for a follow-up.

README and docs/PARITY.md never described the 3 × 5 default, so they don't need changes.

Checked in Excel

In Excel for Mac, with a 200 × 40 px image anchored at B2 with no to, written by main and by this branch. Both files open without a repair prompt. Size and placement are as Excel itself reports them:

  • Before: 159 × 75 pt, from B2 over the invented 3 × 5 cell box, set to "move and size with cells". The a:ext size was ignored, as the issue says.
  • After: 150 × 30 pt at B2. That is 200 × 40 px at 96 dpi, or 5.29 × 1.06 cm. It is set to "move but don't size with cells".

So Excel sizes a oneCellAnchor picture from xdr:ext.

Not verified

  • LibreOffice. Not opened. The claim that it behaves the same way comes from the issue and the schema.
  • Windows Excel. Only Excel for Mac was checked.

Summary by CodeRabbit

  • New Features

    • Images without a destination anchor now retain their specified width and height while using a single-cell placement.
    • Image positions and dimensions are preserved when spreadsheets are opened and saved.
    • Images with explicit destination anchors continue to stretch between the defined cells.
    • Image metadata and placement remain consistent across supported anchor types.
  • Bug Fixes

    • Prevented images from being assigned an unintended destination cell range when no destination anchor is provided.
  • Documentation

    • Clarified how image dimensions are interpreted and stored for single-cell and two-cell placements.

`writeDrawing` always wrote images as `xdr:twoCellAnchor`. When
`anchor.to` was absent it made up the bottom-right corner as `from` plus
3 columns and 5 rows. `width`/`height` only reached the picture's
`<a:ext>`, which Excel and LibreOffice ignore under a two-cell anchor, so
a 200 x 40 px logo was stretched over a 3 x 5 cell box. Reading the file
back returned a `to` the caller never gave.

An image with no `to` is now written as `xdr:oneCellAnchor`: `xdr:from`,
`<xdr:ext cx cy>` from `width`/`height` (or the existing default size),
the same `xdr:pic`, and `xdr:clientData`. This is Excel's "Move but don't
size with cells". The reader already parses `oneCellAnchor`, so the image
reads back with only `from` and the size it was written with. Images
with `anchor.to` are still written as `twoCellAnchor`, byte for byte as
before.

Rendering change: an image written without `anchor.to` now shows at its
`width`/`height` (or the default 3000000 x 2000000 EMU, about 315 x 210
px) instead of filling 3 columns x 5 rows. It also no longer resizes with
those cells. Pass `anchor.to` to keep the old stretched layout.

Text boxes and charts still invent a `to` when it is absent. They are
left for a follow-up: the reader takes text boxes only from
`twoCellAnchor`, and charts have no size field.

Closes productdevbook#598
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2810b96f-3f7c-460c-a2a3-6c48b4899825
📥 Commits

Reviewing files that changed from the base of the PR and between c991ad0 and 2073c41.

📒 Files selected for processing: 6
  • src/_types.ts
  • src/xlsx/drawing-writer.ts
  • src/xlsx/reader.ts
  • test/images.test.ts
  • test/one-cell-anchor.test.ts
  • test/xlsx-write-read-parity.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 5 remain after this review.

📝 Walkthrough

Images without anchor.to now use one-cell anchors with dimensions from width and height. Images with an explicit anchor.to continue to use two-cell anchors. Tests cover writing, reading, and XLSX round trips.

Changes: Image anchor sizing

Layer / File(s) Summary
Write image anchors
src/_types.ts, src/xlsx/drawing-writer.ts, src/xlsx/reader.ts
The writer uses twoCellAnchor when anchor.to is present. Otherwise, it writes a oneCellAnchor with dimensions from the image size. Comments describe the sizing rules.
Validate anchor writing and round trips
test/one-cell-anchor.test.ts, test/images.test.ts, test/xlsx-write-read-parity.test.ts
Tests check anchor type, position, dimensions, XML structure, metadata, and XLSX round trips. They also verify that explicit destination anchors remain two-cell anchors.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: productdevbook

Merge Risk: ⚪ Minimal · up to 2073c

The image-anchor change is mergeable after normal checks. Rendering in LibreOffice and Windows Excel remains unverified, but no failure in those applications has been established.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: images without anchor.to are written as oneCellAnchor elements.
Linked Issues check Passed Issue [#598] requires one-cell serialization when an image has no anchor.to, with dimensions in xdr:ext, preserved from, default dimensions when needed, and unchanged two-cell output when to e…
Out of Scope Changes check Passed The changed writer code, documentation comments, and tests directly support issue [#598]. The changes do not modify text-box or chart fallback behavior. The PR identifies those features as follow-up w…
Docstring Coverage Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 6 files.
Full details: Linked Issues check

Explanation

Issue [#598] requires one-cell serialization when an image has no anchor.to, with dimensions in xdr:ext, preserved from, default dimensions when needed, and unchanged two-cell output when to exists. src/xlsx/drawing-writer.ts implements these branches. The updated image test checks the one-cell anchor, source coordinates, no to, and the default 3000000 × 2000000 EMU size. The PR summary reports coverage for explicit dimensions, read-back, metadata, mixed anchors, and round trips.

Full details: Out of Scope Changes check

Explanation

The changed writer code, documentation comments, and tests directly support issue [#598]. The changes do not modify text-box or chart fallback behavior. The PR identifies those features as follow-up work. No unrelated implementation or test changes are shown.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An image without anchor.to is stretched over 3 × 5 cells, whatever its width/height

1 participant