Repository navigation
fix(xlsx): read a worksheet serialized with a namespace prefix - #1927
Merged
Merged
Conversation
Signed-off-by: Yonghye Kwon <developer.0hye@gmail.com>
A worksheet may bind the SpreadsheetML URI to a prefix instead of making it the default namespace. Excel writes and accepts both spellings, and the two packages are equivalent XML: identical expanded element names, attributes and text. The dependency's worksheet reader matched literal `e.name()` values such as `row` and `headerFooter`, so `s:row` and `s:headerFooter` matched nothing. Every sheet element was skipped, the converter reported success, and the page came out blank — body cells, header and footer all gone. The fix is a streaming `NsReader` adapter in the dependency (developer0hye/umya-spreadsheet#14, upstream MathNya/umya-spreadsheet#371) that resolves each event's namespace and hands the legacy sub-readers the unqualified local name when, and only when, the URI is SpreadsheetML. Elements in a foreign namespace take an internal prefix so the legacy parser cannot mistake their local names for worksheet elements. It buffers one event, so worksheet streaming is preserved, and the stored source XML is untouched. Only the lock's `source` line moves; the fork delta over the previous pin is that adapter, its wiring and its tests. The regression test pins the rule rather than the reported input: it reads the committed `s:`-prefixed fixture and four more prefixes generated from the default-namespace control, including `row:`, which a substring-matching reader would still get wrong. Both the whole-document and the streaming parse paths are checked for every body value, the header, and the footer's three sections with their page-number fields. The prefixed and default-namespace packages now convert to a byte-identical PDF, and the default-namespace output is unchanged. Related: #1803 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Yonghye Kwon <developer.0hye@gmail.com>
Page 1 of the native Excel export, the pre-fix blank output and the post-fix output for the prefixed worksheet, all at 150 DPI, with the layout audit and the strict render-cluster report against the same pair. Every one of the 16 diff clusters is a deviation the default-namespace path already had; none is introduced here, and the dispositions route them to #1731, #1721, #1728 and the #1874 tracker. Related: #1803 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Yonghye Kwon <developer.0hye@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
File submission policy
Before attaching or committing files, read the submission policy.
Summary
A worksheet may bind the SpreadsheetML URI to a prefix instead of making it the default namespace. Excel writes and accepts both spellings, and the two packages are equivalent XML — identical expanded element names, attributes and text. The dependency's worksheet reader matched literal
e.name()values such asrowandheaderFooter, sos:rowands:headerFootermatched nothing: every sheet element was skipped, the CLI reported success, and the page came out blank with body cells, header and footer all gone.The fix is a streaming
NsReaderadapter in the dependency (developer0hye/umya-spreadsheet#14, upstream MathNya/umya-spreadsheet#371, still open) that resolves each event's namespace and hands the legacy sub-readers the unqualified local name when, and only when, the URI is SpreadsheetML. An element in a foreign namespace takes an internal prefix so the legacy parser cannot mistake its local name for a worksheet element. The adapter buffers one event, so worksheet streaming survives, and the stored source XML is untouched.Only the lock's
sourceline moves, from10efd229tofa350180on the samefix/panic-safety-v2fork branch.git diff 10efd229 fa350180in the fork is exactly that adapter, its wiring insrc/reader/xlsx.rsandsrc/reader/xlsx/worksheet.rs, andtests/worksheet_namespaces.rs— 4 files, no other dependency. It is our own fork, not a registry release, so the 7-day crates.io age check does not apply; every command here ran with--locked.The measured outcome is stronger than "the content comes back": the prefixed and default-namespace packages now convert to a byte-identical PDF (
8ec36306…), and the default-namespace output is unchanged frommain. Namespace serialization no longer reaches the output at all.Related issue
Related: #1803
Testing
worksheet_namespace_prefixes_preserve_body_and_header_footerfails withleft: []against the five expected body values for thes:package, while the default-namespace control in the same test passes. After the pin it passes.s:fixture it generates four more serializations from the default-namespace control —x:,ns0:,spreadsheetml:androw:. The last would still fail a substring-matching reader. Each is checked through bothparseandparse_streaming, for all five body values, the header, and the footer's three sections including its page-number and total-pages fields.shasumof the prefixed and control outputs is identical.cargo test --locked --workspace --profile ci: 3335 lib tests and every integration suite pass. (A first run reported 8 failures inooxml-package; they were a stale test binary in the shared target directory, baked with a removed worktree'sCARGO_MANIFEST_DIR. Forcing a rebuild turned them green — no source change.)cargo fmt --all -- --checkclean;cargo clippy --locked --workspace --all-targetsclean on stable and on 1.97 (CI's newer toolchain).compare_text_layer.pyGT vs after: no codepoint-class delta, normalized content identical. GT vs before:-6spaces and114characters against0.Visual impact
Visual audit
tests/visual_audits/issue-1803/prefixed.xlsx(unmodified)mutool draw -F tracefor every position quoted belowfixassets/bugfixes/issue-1803/layout-audit.jsonassets/bugfixes/issue-1803/render-clusters-page-1.jsonQuarterly report, theQuarterly summarytitle, theResearch/120andDevelopment/240rows with the values right-aligned in the same column, and the three footer sectionsInternal,Generated by Example Reporting Systemand1 / 1. The crops show matching Arial faces at matching weight with no italic, no underline, no clipping and no overflow on either side; nothing on this synthetic page is bold, coloured, rotated, dashed or filled, and neither side draws a single rule or rectangle (the layout audit counts 0 rectangles on both sides), so the hairline and emphasis inventories are empty by construction rather than by assumption. In the diff mask every lit pixel is a thin outline hugging a glyph edge; no region is solidly filled, which is what a sub-2pt displacement of otherwise identical text looks like. Measuring the traces rather than the raster: the body is uniformly 1.0pt high (baselines 84/113/128 against 85/114/129) withxmatching exactly at 53.000 and 246.000; the centred header is 1.6pt high (50.400 against 52.000) and 0.487pt right (267.487 against 267.000); the footer baseline matches exactly at 767.000 on all three sections, withInternal1.000pt left (50.000 against 51.000), the centred section 0.495pt left (204.505 against 205.000) and1 / 10.596pt right (540.596 against 540.000) on a 1.39% wider run. Every one of these is also present in the default-namespace control, whose output this change leaves byte-identical, so none of them is introduced here.assets/bugfixes/issue-1803/gt.jpgassets/bugfixes/issue-1803/before.jpgassets/bugfixes/issue-1803/after.jpgassets/bugfixes/issue-1803/compare.jpgVisual comparison
Required inspection
Deviation audit
Internal, a plain uniform-run left footer section, starts at 50.000 against native 51.000 on a 0.7in margin), #1874 (centred header 0.487pt right, centred footer 0.495pt left,1 / 10.596pt right on a 1.39% wider run). 0 large shifts at 5pt; 8 fine shifts at 0.5pt, all listed here.compare_text_layer.pyreported 114 GT characters against 0 before; after, no codepoint-class delta and identical normalized content.Quarterly summary,ResearchandDevelopmentat x 53.000 on both sides,120and240at 246.000 on both, header and centre footer centred,1 / 1right-aligned. Residual horizontal offsets are the #1728 and #1874 rows above.1 / 1run of #1874.Checklist
Signed-off-byline🤖 Generated with Claude Code