openai / openai/codex

SpreadsheetFile.render() multiplies imported rich-text font sizes by 100 before XLSX export

Open
#42,394 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app bug
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

What issue are you seeing?

SpreadsheetFile.render() mutates explicit rich-text run font sizes in an imported XLSX. If the same workbook instance is exported after rendering, SpreadsheetML font sizes are multiplied by 100.

In the synthetic repro below, the source rich-text runs use <sz val="11"/>. A plain import/export round trip preserves 11, but calling render() before export writes <x:sz val="1100"/> for both runs.

This is a silent file-corruption issue: the internal preview can still look normal, while Excel-compatible viewers such as WPS may show blank or extremely large text. The observed 1100 pt value also exceeds SpreadsheetML's valid Office font-size range.

What steps can reproduce the bug?

Environment:

  • Codex App: 26.831.21537 (build 7579)
  • Codex CLI: 0.152.1
  • Spreadsheets plugin: 26.826.12353
  • @oai/artifact-tool: 2.8.52
  • @oai/walnut: 0.1.245
  • Bundled Node.js: 24.19.0
  • Platform: macOS 26.4 arm64
  1. Decode the fully synthetic workbook included below as minimal_source.xlsx.
  2. Run:
import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";

const source = "minimal_source.xlsx";

const control = await SpreadsheetFile.importXlsx(await FileBlob.load(source));
const controlOutput = await SpreadsheetFile.exportXlsx(control);
await controlOutput.save("roundtrip_without_render.xlsx");

const rendered = await SpreadsheetFile.importXlsx(await FileBlob.load(source));
await rendered.render({
  sheetName: "Repro",
  range: "A1:D4",
  scale: 1,
  format: "png",
});
const renderedOutput = await SpreadsheetFile.exportXlsx(rendered);
await renderedOutput.save("roundtrip_after_render.xlsx");
  1. Inspect the rich-text sizes:
for f in minimal_source.xlsx roundtrip_without_render.xlsx roundtrip_after_render.xlsx; do
  printf '%s: ' "$f"
  unzip -p "$f" xl/sharedStrings.xml | grep -oE 'sz val="[0-9.]+"' | tr '\n' ' '
  printf '\n'
done

Actual result:

minimal_source.xlsx:            sz val="11" sz val="11"
roundtrip_without_render.xlsx:  sz val="11" sz val="11"
roundtrip_after_render.xlsx:    sz val="1100" sz val="1100"

The source contains one merged and wrapped rich-text cell (B2:C2) with an explicit row height. This report does not claim that merge/wrap is required; it records the minimized structure that reproduces the failure.

What is the expected behavior?

Rendering must not mutate workbook data used by a later export. Both round trips should preserve the two explicit run sizes as 11 pt.

If rendering requires an internal hundredths-of-a-point representation, that value should be converted back to SpreadsheetML point units before export.

Additional information
  • The workbook is fully synthetic and contains no user, company, project, or repository data.
  • It contains one sheet, one merged range, one shared string, no formulas, no macros, and no external links.
  • All three XLSX ZIP packages pass unzip -t.
  • SHA-256:
    • source: 08ce01420be55289bba73b5ae0ebe110e1b91165e401d6ca3bad1f06859f541d
    • without render: 44430e75ef5539223d248f27702473f412d64f9ab32bfe52deb75a0cf34a604e
    • after render: 92cd5eeeb5f6dff4ff65ee83dd06940303b2576d91ebee92dfb35c2e94084e3c
  • A likely explanation is a points-versus-hundredths-of-a-point unit leak from the render path. This is an inference from the exact 11 -> 1100 boundary, not a source-code-confirmed root cause.
  • Current safe workaround: export the edited workbook before rendering, then re-import the exported XLSX into a disposable QA instance for rendering and never export that QA instance.
Sanitized minimal XLSX (base64)

Decode:

python3 -c 'import base64,pathlib; pathlib.Path("minimal_source.xlsx").write_bytes(base64.b64decode(pathlib.Path("repro.b64").read_text()))'

Save this block as repro.b64:

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the supplied JavaScript reproduction with minimal_source.xlsx, using SpreadsheetFile.importXlsx(), render(), and exportXlsx(). Compare xl/sharedStrings.xml in the three output packages and trace the render-to-export path responsible for converting the explicit 11 pt rich-text sizes. Done means rendering no longer changes the exported sizes, which remain 11 pt and pass XLSX integrity checks.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.