Open rowanc1 opened 2 years ago
@rowanc1 Hey, hope you're well! Any thoughts on how this could be implemented?
Been a bit since I looked at it. I still think that an upstream contribution to docx is best. The maintainer is usually pretty good about getting changes turned around fast! Do you want to open an issue asking if that would be accepted on that repo?
Hmm, not sure... I'm thinking of another approach:
Modifying editor.state.doc
before feeding it to docxSerializer.serialize(prosemirrorNode, options)
.
So I'm thinking to go through all images of the doc and change their src
to base64. What do you think?
While a promise would be nice, just want to share my solution for anyone coming here in future
import { Node } from '@tiptap/pm/model';
import { findChildren } from '@tiptap/react';
import {
DocxSerializer,
NodeSerializer,
MarkSerializer,
defaultMarks,
defaultNodes,
writeDocx,
} from 'prosemirror-docx';
import { useCallback } from 'react';
const nodeSerializer: NodeSerializer = {
...defaultNodes,
superImage: (state, node) => {
const width = Number(node.attrs.width.replace('px', ''));
const alignment = node.attrs.textAlign;
// TODO: need to handle margins
const widthPercent = (width / 793) * 100;
state.image(node.attrs.src, widthPercent, alignment);
state.closeBlock(node);
},
paragraph: (state, node) => {
state.renderInline(node);
state.addParagraphOptions({
alignment: node.attrs.textAlign,
});
state.closeBlock(node);
},
};
const markSerializer: MarkSerializer = {
...defaultMarks,
textStyle: (state, node, mark) => {
const attrs = mark.attrs;
return {
color: attrs.color,
size: attrs.fontSize,
font: attrs.fontFamily,
};
},
};
const serializer = new DocxSerializer(nodeSerializer, markSerializer);
export function useExportToWord() {
return useCallback(async (doc: Node, fileName: string) => {
const imageNodes = findChildren(doc, (node) => {
if (node.type.name === 'superImage') {
return true;
}
});
const srcBufferMap = new Map<string, Buffer>();
const getImageBuffer = (id: string): Buffer => {
const img = document.querySelector(
`[data-id="${id}"]`,
) as HTMLImageElement;
img.crossOrigin = 'anonymous';
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Could not create canvas context');
}
if (!img.complete) {
throw new Error('Image is not fully loaded');
}
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0, canvas.width, canvas.height);
const dataUrl = canvas.toDataURL('image/jpeg');
const base64 = dataUrl?.split(',')[1];
// Convert base64 to buffer
const buffer = Buffer.from(base64, 'base64');
return buffer;
};
for (const node of imageNodes) {
const src = node.node.attrs.src;
if (!srcBufferMap.has(src)) {
srcBufferMap.set(src, getImageBuffer(node.node.attrs.id));
}
}
const word = serializer.serialize(doc, {
getImageBuffer: (src) => {
return srcBufferMap.get(src) || Buffer.from('');
},
});
writeDocx(word, (blob) => {
const blobURL = window.URL.createObjectURL(new Blob([blob]));
const tempLink = document.createElement('a');
tempLink.style.display = 'none';
tempLink.href = blobURL;
tempLink.download = fileName;
tempLink.setAttribute('target', '_blank');
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
window.URL.revokeObjectURL(blobURL);
});
}, []);
}
When we actually create an
ImageRun
indocx
, the data must be there. I think, however, we can delay creating the XML node until later in the process and make our implementation either take a Buffer or aPromise<Buffer>
.That makes more sense with most ways you load an image, but does make the internals a bit more complicated.
Another alternative is to upstream the change to docx and allow them to take the promise. I might start there?