Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Loading