curvenote / prosemirror-docx

Export a prosemirror document to a Microsoft Word file, using docx.
MIT License
96 stars 13 forks source link

Return a promise for imagebuffer #5

Open rowanc1 opened 2 years ago

rowanc1 commented 2 years ago

When we actually create an ImageRun in docx, 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 a Promise<Buffer>.

That makes more sense with most ways you load an image, but does make the internals a bit more complicated.

const opts = {
  async getImageBuffer(src: string) {
    const imgBuffer = await fetchImageBuffer(src);
    return imgBuffer;
  }
}

Another alternative is to upstream the change to docx and allow them to take the promise. I might start there?

devgeni commented 2 years ago

@rowanc1 Hey, hope you're well! Any thoughts on how this could be implemented?

rowanc1 commented 2 years ago

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?

devgeni commented 2 years ago

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?

dan-cooke commented 1 month ago

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);
    });
  }, []);
}