diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js index bf52f52c57..3c03d1f974 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js @@ -345,7 +345,7 @@ export function handleImageNode(node, params, isAnchor) { const spPr = picture.elements.find((el) => el.name === 'pic:spPr'); if (spPr) { - const xfrm = spPr.elements.find((el) => el.name === 'a:xfrm'); + const xfrm = spPr.elements?.find((el) => el.name === 'a:xfrm'); if (xfrm?.attributes) { transformData = { ...transformData, @@ -370,6 +370,7 @@ export function handleImageNode(node, params, isAnchor) { const { elements } = relationships || []; const rel = elements?.find((el) => el.attributes['Id'] === rEmbed); + if (!rel) { return null; } diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.test.js index 9429ca477e..8abe320d3d 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.test.js @@ -1460,4 +1460,195 @@ describe('getVectorShape', () => { expect(result.attrs.textContent.parts[0].text).toBe('[[notspace]] [other]'); }); }); + + describe('IT-632: docx-templates duplicate pic:cNvPr id and non-standard rIds', () => { + /** + * docx-templates generates images with: + * 1. All pic:cNvPr id="0" (duplicate, non-conformant per OOXML spec §20.1.2.2.8) + * 2. All wp:docPr id="0" (also duplicated from template cloning) + * 3. Non-standard relationship IDs like "img{hash}" instead of "rId{n}" + * 4. Different relationship targets for each image + * + * This test verifies each image resolves to a unique src path. + */ + + const makeDocxTemplatesImageNode = ({ rEmbed, docPrName, picCNvPrName }) => ({ + attributes: { + distT: '0', + distB: '0', + distL: '0', + distR: '0', + }, + elements: [ + { name: 'wp:extent', attributes: { cx: '5000000', cy: '3000000' } }, + { + name: 'a:graphic', + elements: [ + { + name: 'a:graphicData', + attributes: { uri: 'pic' }, + elements: [ + { + name: 'pic:pic', + elements: [ + { + name: 'pic:nvPicPr', + elements: [ + { + name: 'pic:cNvPr', + attributes: { id: '0', name: picCNvPrName }, + }, + ], + }, + { + name: 'pic:blipFill', + elements: [ + { + name: 'a:blip', + attributes: { 'r:embed': rEmbed }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + // wp:docPr also duplicated with id="0" + { name: 'wp:docPr', attributes: { id: '0', name: docPrName } }, + ], + }); + + const makeDocxTemplatesParams = () => ({ + filename: 'document.xml', + docx: { + 'word/_rels/document.xml.rels': { + elements: [ + { + name: 'Relationships', + elements: [ + { + name: 'Relationship', + attributes: { + Id: 'img2073076884', + Target: 'media/template_document.xml_img2073076884.jpg', + }, + }, + { + name: 'Relationship', + attributes: { + Id: 'img3891234567', + Target: 'media/template_document.xml_img3891234567.jpg', + }, + }, + { + name: 'Relationship', + attributes: { + Id: 'img5678901234', + Target: 'media/template_document.xml_img5678901234.jpg', + }, + }, + ], + }, + ], + }, + }, + }); + + it('should produce distinct src paths for images with duplicate pic:cNvPr id=0', () => { + const params = makeDocxTemplatesParams(); + + const image1 = makeDocxTemplatesImageNode({ + rEmbed: 'img2073076884', + docPrName: 'image1.jpg', + picCNvPrName: 'image1.jpg', + }); + const image2 = makeDocxTemplatesImageNode({ + rEmbed: 'img3891234567', + docPrName: 'image2.jpg', + picCNvPrName: 'image2.jpg', + }); + const image3 = makeDocxTemplatesImageNode({ + rEmbed: 'img5678901234', + docPrName: 'image3.jpg', + picCNvPrName: 'image3.jpg', + }); + + const result1 = handleImageNode(image1, params, false); + const result2 = handleImageNode(image2, params, false); + const result3 = handleImageNode(image3, params, false); + + // All should produce valid image nodes + expect(result1).not.toBeNull(); + expect(result2).not.toBeNull(); + expect(result3).not.toBeNull(); + + // Each should have a DISTINCT src path + expect(result1.attrs.src).toBe('word/media/template_document.xml_img2073076884.jpg'); + expect(result2.attrs.src).toBe('word/media/template_document.xml_img3891234567.jpg'); + expect(result3.attrs.src).toBe('word/media/template_document.xml_img5678901234.jpg'); + + // Verify all three are different + const srcs = [result1.attrs.src, result2.attrs.src, result3.attrs.src]; + expect(new Set(srcs).size).toBe(3); + + // rIds should also be distinct + expect(result1.attrs.rId).toBe('img2073076884'); + expect(result2.attrs.rId).toBe('img3891234567'); + expect(result3.attrs.rId).toBe('img5678901234'); + }); + + it('should handle empty pic:spPr element (SD-2085)', () => { + const params = makeDocxTemplatesParams(); + + // pic:spPr as a self-closing empty element — valid per ECMA-376 §20.2.2.6 + // (all CT_ShapeProperties children are optional) + const imageWithEmptySpPr = { + ...makeDocxTemplatesImageNode({ + rEmbed: 'img2073076884', + docPrName: 'image1.jpg', + picCNvPrName: 'image1.jpg', + }), + }; + + // Add empty pic:spPr to the pic:pic element (no elements array) + const graphicData = imageWithEmptySpPr.elements + .find((el) => el.name === 'a:graphic') + .elements.find((el) => el.name === 'a:graphicData'); + const picPic = graphicData.elements.find((el) => el.name === 'pic:pic'); + picPic.elements.push({ name: 'pic:spPr', attributes: {} }); + + const result = handleImageNode(imageWithEmptySpPr, params, false); + + expect(result).not.toBeNull(); + expect(result.attrs.src).toBe('word/media/template_document.xml_img2073076884.jpg'); + expect(result.attrs.rId).toBe('img2073076884'); + }); + + it('should handle images where all wp:docPr ids are "0"', () => { + const params = makeDocxTemplatesParams(); + + const image1 = makeDocxTemplatesImageNode({ + rEmbed: 'img2073076884', + docPrName: 'image1.jpg', + picCNvPrName: 'image1.jpg', + }); + const image2 = makeDocxTemplatesImageNode({ + rEmbed: 'img3891234567', + docPrName: 'image2.jpg', + picCNvPrName: 'image2.jpg', + }); + + const result1 = handleImageNode(image1, params, false); + const result2 = handleImageNode(image2, params, false); + + // Both have id="0" from wp:docPr — this should NOT cause deduplication + expect(result1.attrs.id).toBe('0'); + expect(result2.attrs.id).toBe('0'); + + // But src should still be different + expect(result1.attrs.src).not.toBe(result2.attrs.src); + }); + }); });