diff --git a/packages/super-editor/src/core/DocxZipper.js b/packages/super-editor/src/core/DocxZipper.js index 9c5a03aff..4ba50a94c 100644 --- a/packages/super-editor/src/core/DocxZipper.js +++ b/packages/super-editor/src/core/DocxZipper.js @@ -48,12 +48,15 @@ class DocxZipper { name: zipEntry.name, content, }); - } else if (zipEntry.name.startsWith('word/media') && zipEntry.name !== 'word/media/') { + } else if ( + (zipEntry.name.startsWith('word/media') && zipEntry.name !== 'word/media/') || + (zipEntry.name.startsWith('media') && zipEntry.name !== 'media/') + ) { // If we are in node, we need to convert the buffer to base64 if (isNode) { const buffer = await zipEntry.async('nodebuffer'); const fileBase64 = buffer.toString('base64'); - mediaObjects[zipEntry.name] = fileBase64; + this.mediaFiles[zipEntry.name] = fileBase64; } // If we are in the browser, we can use the base64 directly @@ -215,9 +218,9 @@ class DocxZipper { zip.file(key, content); }); - Object.keys(media).forEach((name) => { - const binaryData = Buffer.from(media[name], 'base64'); - zip.file(`word/media/${name}`, binaryData); + Object.keys(media).forEach((path) => { + const binaryData = Buffer.from(media[path], 'base64'); + zip.file(path, binaryData); }); // Export font files @@ -252,8 +255,8 @@ class DocxZipper { unzippedOriginalDocx.file(key, updatedDocs[key]); }); - Object.keys(media).forEach((name) => { - unzippedOriginalDocx.file(`word/media/${name}`, media[name]); + Object.keys(media).forEach((path) => { + unzippedOriginalDocx.file(path, media[path]); }); await this.updateContentTypes(unzippedOriginalDocx, media); diff --git a/packages/super-editor/src/core/super-converter/SuperConverter.js b/packages/super-editor/src/core/super-converter/SuperConverter.js index 902d3e4a5..6a1b5562b 100644 --- a/packages/super-editor/src/core/super-converter/SuperConverter.js +++ b/packages/super-editor/src/core/super-converter/SuperConverter.js @@ -712,8 +712,7 @@ class SuperConverter { const processedData = {}; for (const filePath in media) { if (typeof media[filePath] !== 'string') continue; - const name = filePath.split('/').pop(); - processedData[name] = await getArrayBufferFromUrl(media[filePath], editor.options.isHeadless); + processedData[filePath] = await getArrayBufferFromUrl(media[filePath], editor.options.isHeadless); } this.convertedXml.media = { diff --git a/packages/super-editor/src/core/super-converter/exporter.js b/packages/super-editor/src/core/super-converter/exporter.js index 8a235ae46..5e59ad6f4 100644 --- a/packages/super-editor/src/core/super-converter/exporter.js +++ b/packages/super-editor/src/core/super-converter/exporter.js @@ -1556,7 +1556,7 @@ function translateImageNode(params, imageSize) { const src = attrs.src || attrs.imageSrc; const { originalWidth, originalHeight } = getPngDimensions(src); - const imageName = params.node.type === 'image' ? src?.split('word/media/')[1] : attrs.fieldId?.replace('-', '_'); + const imageName = params.node.type === 'image' ? src.split('/').pop() : attrs.fieldId?.replace('-', '_'); let size = attrs.size ? { diff --git a/packages/super-editor/src/core/super-converter/v2/importer/imageImporter.js b/packages/super-editor/src/core/super-converter/v2/importer/imageImporter.js index 4220bbf6b..e9b8814c4 100644 --- a/packages/super-editor/src/core/super-converter/v2/importer/imageImporter.js +++ b/packages/super-editor/src/core/super-converter/v2/importer/imageImporter.js @@ -116,8 +116,12 @@ export function handleImageImport(node, currentFileName, params) { if (!rel) return null; const { attributes: relAttributes } = rel; + const targetPath = relAttributes['Target']; - const path = `word/${relAttributes['Target']}`; + let path = `word/${targetPath}`; + + // Some images may appear out of the word folder + if (targetPath.startsWith('/word') || targetPath.startsWith('/media')) path = targetPath.substring(1); return { type: 'image', diff --git a/packages/super-editor/src/dev/components/DeveloperPlayground.vue b/packages/super-editor/src/dev/components/DeveloperPlayground.vue index ef11f2f58..2baa99b60 100644 --- a/packages/super-editor/src/dev/components/DeveloperPlayground.vue +++ b/packages/super-editor/src/dev/components/DeveloperPlayground.vue @@ -3,6 +3,7 @@ import '@/style.css'; import '@harbour-enterprises/common/styles/common-styles.css'; import { ref, shallowRef, computed, onMounted } from 'vue'; +import { NMessageProvider } from 'naive-ui'; import { SuperEditor } from '@/index.js'; import { getFileObject } from '@harbour-enterprises/common/helpers/get-file-object'; import { DOCX } from '@harbour-enterprises/common'; @@ -158,7 +159,9 @@ onMounted(async () => {
- + + +
diff --git a/packages/super-editor/src/tests/data/image-out-of-folder.docx b/packages/super-editor/src/tests/data/image-out-of-folder.docx new file mode 100644 index 000000000..7e1e1274e Binary files /dev/null and b/packages/super-editor/src/tests/data/image-out-of-folder.docx differ diff --git a/packages/super-editor/src/tests/export/export-helpers/export-helpers.js b/packages/super-editor/src/tests/export/export-helpers/export-helpers.js index 5bfb6a17c..12b07a3de 100644 --- a/packages/super-editor/src/tests/export/export-helpers/export-helpers.js +++ b/packages/super-editor/src/tests/export/export-helpers/export-helpers.js @@ -96,6 +96,37 @@ export const getExportedResult = async (name, comments = []) => { return result; }; +export const getExportMediaFiles = async (name, comments = []) => { + const buffer = await getTestDataAsBuffer(name); + const [docx, media, mediaFiles, fonts] = await Editor.loadXmlData(buffer, true); + + const editor = new Editor({ + isHeadless: true, + extensions: getStarterExtensions(), + documentId: 'test-doc', + content: docx, + mode: 'docx', + media, + mediaFiles, + fonts, + annotations: true, + }); + + const json = editor.getUpdatedJson(); + await editor.converter.exportToDocx( + json, + editor.schema, + editor.storage.image.media, + true, + 'external', + comments, + editor, + false, + null, + ); + return editor.converter.addedMedia; +}; + export const getExportedResultForAnnotations = async (isFinalDoc) => { const buffer = await getTestDataAsBuffer('annotations_import.docx'); const [docx, media, mediaFiles, fonts] = await Editor.loadXmlData(buffer, true); diff --git a/packages/super-editor/src/tests/export/imageNodeExporter.test.js b/packages/super-editor/src/tests/export/imageNodeExporter.test.js index 83717f70d..08c32d4d4 100644 --- a/packages/super-editor/src/tests/export/imageNodeExporter.test.js +++ b/packages/super-editor/src/tests/export/imageNodeExporter.test.js @@ -1,4 +1,4 @@ -import { getExportedResult } from './export-helpers/index'; +import { getExportedResult, getExportMediaFiles } from './export-helpers/index'; describe('ImageNodeExporter', async () => { window.URL.createObjectURL = vi.fn().mockImplementation((file) => { @@ -68,3 +68,17 @@ describe('ImageNodeExporter anchor image', async () => { expect(anchorNode.elements[5].attributes.wrapText).toBe('bothSides'); }); }); + +describe('ImageNodeExporter images with absolute path', async () => { + window.URL.createObjectURL = vi.fn().mockImplementation((file) => { + return file.name; + }); + + const fileName = 'image-out-of-folder.docx'; + const result = await getExportMediaFiles(fileName); + + it('exports image with absolute path correctly', async () => { + expect(result).toHaveProperty('word/media/image1.jpeg'); + expect(result).toHaveProperty('media/image.png'); + }); +}); diff --git a/packages/super-editor/src/tests/import/imageImporter.test.js b/packages/super-editor/src/tests/import/imageImporter.test.js index e9851e317..2c27c3a74 100644 --- a/packages/super-editor/src/tests/import/imageImporter.test.js +++ b/packages/super-editor/src/tests/import/imageImporter.test.js @@ -54,4 +54,31 @@ describe('ImageNodeImporter', () => { expect(anchorData).toHaveProperty('alignH', 'left'); expect(anchorData).toHaveProperty('alignV', 'top'); }); + + it('imports image with absolute path in Target correctly', async () => { + const dataName = 'image-out-of-folder.docx'; + const docx = await getTestDataByFileName(dataName); + const documentXml = docx['word/document.xml']; + + const doc = documentXml.elements[0]; + const body = doc.elements[0]; + const content = body.elements; + console.log(content[6].elements[2]); + + const { nodes } = handleParagraphNode({ nodes: [content[0]], docx, nodeListHandler: defaultNodeListHandler() }); + + let paragraphNode = nodes[0]; + let drawingNode = paragraphNode.content[0]; + const { attrs } = drawingNode; + expect(attrs.src).toBe('media/image.png'); + + const { nodes: nodes1 } = handleParagraphNode({ + nodes: [content[5]], + docx, + nodeListHandler: defaultNodeListHandler(), + }); + paragraphNode = nodes1[0]; + drawingNode = paragraphNode.content[1]; + expect(drawingNode.attrs.src).toBe('word/media/image1.jpeg'); + }); });