feat: render images inside Word textboxes (SD-2804)#3207
feat: render images inside Word textboxes (SD-2804)#3207
Conversation
…804) ECMA-376 §20.4.2.38 (CT_TxbxContent) lets a textbox hold rich body-level content — paragraphs whose runs can carry inline w:drawing images. The text-only extractor used to silently skip those drawings, so the textbox rendered empty even though export round-tripped the image untouched. The fix surfaces the inline drawing as a textContent part with kind='image' so the existing shape painter can render it alongside text spans: - TextPart contract gains optional kind/src/width/height/alt fields. - extractTextFromTextBox.handleRun branches on w:drawing, reuses the v3 wp drawing handler (handleImageNode) to resolve rId, then upgrades the path-style src to a data URI from converter.media so the painter can drop it straight into <img>. - DomPainter's createFallbackTextElement renders image parts as inline <img> elements next to existing text spans. Linked: SD-2745 (header-anchored floating textboxes — positions the box where this content now renders).
|
I wasn't granted permissions for the ecma-spec MCP tools, so I reviewed the diff against my knowledge of ECMA-376 (Part 1, §17 WordprocessingML and §20.4 DrawingML-WordprocessingDrawing). Status: PASS The OOXML element handling in this PR is spec-compliant:
One minor non-blocking note: the comment cites "ECMA-376 §20.4.2.38" for |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Hey @tupizz! I found a few things and left as inline comments. Ping me if you have any questions. Thanks! |
Per Luccas's review on PR #3207: - (C1) Skip hidden textbox images. handleImageNode flags wp:docPr hidden="1" via attrs.hidden, but the new image-part branch only checked attrs.src and emitted visible <img>s for them. Top-level hidden drawings are filtered later in the pipeline; image parts bypass that filtering. Gate the textParts.push on imagePm.attrs.hidden !== true so hidden textbox drawings stay hidden, matching the body-level behaviour. - (C2) Drop the duplicated resolveImagePartSrc helper in the importer (it rejected Uint8Array, breaking Y.js binary media). Store the raw path + extension + rId on the image part. pm-adapter's hydrateImageBlocks gains a vectorShape branch that hydrates textContent.parts alongside ImageRuns, so all media path candidates and the Uint8Array → TextDecoder decoding live in a single place. - (C3) Anchored drawings inside textboxes are out of scope — wrap / position / transform metadata isn't carried into the text-parts model. Restrict the textbox-image branch to wp:inline and document the limit in the code comment so a future fixture can extend it intentionally. - (C4) Align inserted images to the text baseline like body inline images do (vertical-align: bottom). ECMA-376 §20.4.2.8 specifies that an inline drawing behaves "like a character glyph of similar size", and the body inline image renderer defaults to vertical-align: bottom (renderer.ts ~L5770, L5847) — the textbox image part used vertical-align: middle, visibly misaligning text next to the image inside a textbox compared to outside it.
|
@luccas-harbour went through all your points and addressed them, please check it again once you're good |
Summary
Renders inline
w:drawingimages inside Word textbox content. Previously, the textbox imported with the image silently stripped — the textbox rendered as an empty box even though export round-tripped the image untouched.Linear: SD-2804
Spec basis
ECMA-376 §20.4.2.38 (
CT_TxbxContent) defines textbox content asEG_BlockLevelElts (1..unbounded)— i.e. a textbox can hold the same content as the document body, with three exclusions: cross-story refs (comments/footnotes/endnotes), VML, and nestedtxbxContent. Notably, paragraphs insidew:txbxContentcarry the sameCT_Pcontent model as body paragraphs, including runs with inlinew:drawingimages.The text-only extractor used in
extractTextFromTextBox.handleRunonly walkedw:t / w:tab / w:br / sd:autoPageNumber / sd:totalPageNumber—w:drawingwas silently ignored.Approach
Minimum surgical change — extend the existing text-parts model with one image part kind:
TextPartcontract gains optionalkind: 'image'plussrc / width / height / alt.extractTextFromTextBox.handleRun) branches onw:drawing, reuses the v3handleImageNodeto resolver:embed → media path, then upgrades the path to a data URI fromconverter.media(the text-parts model has no downstream hydration step like bodyImageRuns do).createFallbackTextElement) renders parts withkind: 'image'as inline<img>next to text spans.No new PM nodes, no new pm-adapter wiring, no schema changes, no NodeHandlerContext threading.
Before / after
Fixture: a DOCX with a textbox-in-header containing a single inline image.
Captured via agent-browser against the dev server: see
/tmp/sd-2804-final3.png.Test plan
textContent.partsfor an inlinew:drawinginside the textbox (encode-image-node-helpers.test.js)Out of scope (deferred)
The fixture's image is
wp:inlineinside a textbox run — the most common case. ECMA-376 also permits richer block-level content inside a textbox: tables, lists, SDTs, hyperlinks, fields, math. The current text-parts model can't represent those; surfacing them would need to floww:txbxContentthrough the same body pipeline (handleParagraphNoderecursion) and likely a container PM node (shapeTextboxschema already exists for the legacy v:pict path).That refactor is intentionally deferred — the supplied SD-2804 fixture has only an image, and Option B was over-engineering for the immediate user-visible bug. Tracking issue / future PR scope for content beyond inline-image-in-textbox.
Related