카테고리 없음

dom element를 이미지로 변환하기 (feat canvas API)

dtLee 2024. 10. 15. 23:13

화면에 생성된 dom을 이미지로 다운로드 하기 위해서는 먼저 blob이나 file객체로 변환해야 한다. 

 

그럼 어떻게 변환할 수 있을까?

 

1. 라이브러리 사용하기

이미 dom을 이미지로 변환하는 라이브러리가 존재한다. 

html2canvas와 dom2image이다

 

그러나 html2canvas는 성능 문제가 있어 이미지로 변환할 때 화면이 멈추는 이슈가 있고

domtoimage는 화질 이슈가 있다. 

그래서 직접 구현하도록 하겠다. 

 

2. 직접구현

dom을 이미지로 구현하기 위해서는 dom을 svg테그안에 넣어주고 그이후 canvas를 통해 렌더링한 후 blob객체로 변환하여 다운로드를 하면 된다. 

 

그럼 시작해 보자!

 

1. canvas생성하기

먼저 canvas를 생성해야한다. 

    const canvas = document.createElement('canvas');

 

그리고 dom의 width와 height를 받아와야 한다. cnavas는 width와 height를 직접 주입해야 하는데 그 값을 dom의 width와 height를 받아와 주입한다. 

    const { width, height } = element.getBoundingClientRect();

 

 

    const context = canvas.getContext('2d');
    canvas.width = width;
    canvas.height = height;

canvas테그가 처음에 생성이 되면 안에 내용이 비어있다. 그래서 getContext를 통해 2d를 렌더링하나다는것을 알려주어야 한다. 

또 canvas의 width와 height에 dom의 width와 height를 주입한다. 

 

그다음 svg데그를 만든다

 const svgData = `
        <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
          <foreignObject width="100%" height="100%">
            ${new XMLSerializer().serializeToString(element)}
          </foreignObject>
        </svg>
    `;

문자열을 통해 svg테그를 만드는데 svg 안에 foreignObject를 통해 dom을 svg로 만든다. dom은 new XMLSerializer().serializeToString()읕 통해 XML 형태로 만들고 그것을 문자열 형태로 변환하여 svg안에 집어넣는다. 

 

   const image = new Image();
    image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData);
    image.onload = () => {
      context?.drawImage(image, 0, 0);
      resolve(canvas);
    };

image테그를 생성하여 src에 방금 만든 svg 문자열을 집어너은 후 canvas의 context안에 drewImage를 통해 화면에 그려주어야 한다. 

 

전체코드

const applyInlineStyles = (element: HTMLElement) => {
  const computedStyle = window.getComputedStyle(element);
  let inlineStyles = '';

  Array.from(computedStyle).forEach(key => {
    inlineStyles += `${key}: ${computedStyle.getPropertyValue(key)}; `;
  });

  element.setAttribute('style', inlineStyles); // 한 번에 적용

  Array.from(element.children).forEach((child: Element) => applyInlineStyles(child as HTMLElement));
};

const domToCanvas = (element: HTMLElement): Promise<HTMLCanvasElement> => {
  applyInlineStyles(element);
  const canvasEl = new Promise<HTMLCanvasElement>(resolve => {
    const { width, height } = element.getBoundingClientRect();
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('');
    canvas.width = width;
    canvas.height = height;

    const svgData = `
        <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
          <foreignObject width="100%" height="100%">
            ${new XMLSerializer().serializeToString(element)}
          </foreignObject>
        </svg>
    `;
    const image = new Image();
    image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData);
    image.onload = () => {
      context?.drawImage(image, 0, 0);
      resolve(canvas);
    };
  });
  return canvasEl;
};

canvas를 생성을 한후에 다른작업이 이루어져야 되기 때문에 promise객체로 생성하였다.