Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return a promise for imagebuffer #5

Open
rowanc1 opened this issue Nov 4, 2021 · 5 comments
Open

Return a promise for imagebuffer #5

rowanc1 opened this issue Nov 4, 2021 · 5 comments

Comments

@rowanc1
Copy link
Member

rowanc1 commented Nov 4, 2021

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
Copy link

devgeni commented Mar 17, 2022

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

@rowanc1
Copy link
Member Author

rowanc1 commented Mar 17, 2022

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
Copy link

devgeni commented Mar 21, 2022

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
Copy link
Collaborator

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

@condorheroblog
Copy link

Hello folks,

Sorry to bother you guys, I strongly agree to add promise to the return value of getImageBuffer, because requesting images through fetch API is a good practice whether in the browser or Node.js environment. Its code should be as follows:

{
  async getImageBuffer(src: string) {
    const arrayBuffer = await fetch(src).then((res) => res.arrayBuffer());
    return new Uint8Array(arrayBuffer);
  },
},

I have implemented this feature and I will submit a PR when the time is right.


c3c31d0

It looks like the code is running fine.

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants