언어·프레임워크/React.js

[React.js] jsPDF를 이용한 웹 화면 PDF 내보내기 중 이슈: 페이지 오버플로우 이미지 잘림 문제

DandyNow 2024. 7. 24. 11:06
728x90
반응형

1. 화면에 렌더링 된 테이블 페이지 오버플로우 문제

jsPDF를 이용해 웹 화면을 PDF로 내보내기 기능을 구현했다. 에러 없이 작동하였으나 [그림 1]과 같이 페이지 오버플로우로 그림이 잘린 경우 앞 페이지 하단, 뒷 페이지 상단 여백이 전혀 없는 pdf 파일이 생성되었다.

[그림 1] 페이지 오버플로우로 그림이 잘린 경우 앞 페이지 하단, 뒷 페이지 상단 여백이 없다.

 

2. canvas 이용하여 해결

페이지 오버플로우가 발생할 경우 이미지를 페이지 크기로 잘라 새로운 캔버스에 그리고 새로운 캔버스를 PDF에 추가하는 방식으로 해결하였다. [그림 2]는 최종 결과물이다.

[그림 2] 각 페이지의 하단, 상단에 여백이 잘 적용되었다.

 

작성한 전체 코드는 아래와 같다.

import jsPDF from 'jspdf';
import font from './font/NanumGothic-normal';
import { format } from 'date-fns';

const PdfDownloader = async (images, period, orientation = 'p') => {
  const [openDate, closeDate] = period;

  const startDate = format(openDate, 'yyyy.MM.dd').toString();
  const endDate = format(closeDate, 'yyyy.MM.dd').toString();

  const periodStr = `${startDate} ~ ${endDate}`;

  try {
    const marginLeft = 10; // 왼쪽 마진 값 (mm)
    const marginRight = 10; // 오른쪽 마진 값 (mm)
    const marginTop = 15; // 상단 마진 값 (mm)
    const marginBottom = 10; // 하단 마진 값 (mm)
    const imageMargin = 5; // 이미지 사이의 여백 (mm)

    // PDF 문서 준비
    const doc = new jsPDF(orientation, 'mm', 'a4', true);

    // PDF 페이지의 가로 세로 사이즈
    const pageWidth =
      doc.internal.pageSize.getWidth() - (marginLeft + marginRight);
    const pageHeight =
      doc.internal.pageSize.getHeight() -
      (marginTop + marginBottom + imageMargin);

    // 한글 폰트 추가
    doc.addFileToVFS('NanumGothic.ttf', font);
    doc.addFont('NanumGothic.ttf', 'NanumGothic', 'normal');
    doc.setFont('NanumGothic');

    // "보고서" 문구 추가 (중앙 정렬)
    doc.setFontSize(24);
    const reportTextWidth =
      (doc.getStringUnitWidth('보  고  서') * doc.internal.getFontSize()) /
      doc.internal.scaleFactor;
    const reportTextX = (pageWidth - reportTextWidth) / 2 + marginLeft;
    doc.text('보  고  서', reportTextX, marginTop + 10);

    // 기간 정보 추가 (왼쪽 정렬)
    doc.setFontSize(10);
    const periodText = `◯ 기간: ${periodStr}`;
    doc.text(periodText, marginLeft, marginTop + 25);

    await images.reduce(async (promise, pdfObj, index, array) => {
      await promise; // Wait for the previous iteration to complete

      const { canvas, image } = pdfObj;

      console.log('canvas : ', canvas);
      console.log('image : ', image);

      // 이미지의 길이와 PDF 페이지의 가로 길이를 기준으로 비율을 구함
      const widthRatio = pageWidth / canvas.width;

      // 비율에 따른 이미지 높이
      const customHeight = canvas.height * widthRatio;

      // 첫 페이지에만 marginTop + 15 적용, 나머지는 marginTop만 적용
      const topMargin = index === 0 ? marginTop + 30 : marginTop;

      // 캔버스를 사용하여 이미지를 페이지 크기로 자르기
      let heightLeft = customHeight; // 남은 이미지 높이
      let position = 0; // 이미지 자를 위치

      while (heightLeft > 0) {
        const sliceHeight = Math.min(pageHeight, heightLeft);

        // 새로운 캔버스 생성
        const newCanvas = document.createElement('canvas');
        newCanvas.width = canvas.width;
        newCanvas.height = sliceHeight / widthRatio;

        // 잘라낸 이미지 부분을 새로운 캔버스에 그림
        const newCtx = newCanvas.getContext('2d');
        newCtx.drawImage(
          canvas,
          0,
          position / widthRatio,
          canvas.width,
          sliceHeight / widthRatio,
          0,
          0,
          canvas.width,
          sliceHeight / widthRatio,
        );

        // 새로운 캔버스의 이미지를 PDF에 추가
        const newImage = newCanvas.toDataURL('image/jpeg');
        doc.addImage(
          newImage,
          'JPEG',
          marginLeft,
          topMargin,
          pageWidth,
          sliceHeight,
        );

        // 남은 이미지 높이와 자를 위치 업데이트
        heightLeft -= sliceHeight;
        position += sliceHeight;

        // 페이지가 남아있는 경우 새 페이지 추가
        if (heightLeft > 0) {
          doc.addPage();
        }
      }

      // 리스트의 마지막 요소가 아닌 경우에만 페이지를 추가
      if (index !== array.length - 1) {
        doc.addPage();
      }
    }, Promise.resolve()); // Initial value for reduce()

    // PDF 문서 저장
    doc.save(
      `report_${format(openDate, 'yyyyMMdd').toString()}_${format(
        closeDate,
        'yyyyMMdd',
      ).toString()}.pdf`,
    );
  } catch (error) {
    console.log('error :: ', error);
    throw error;
  }
};
export default PdfDownloader;

2024-12-09 추가

3. 폰트(ttf) 파일 base64 인코딩

한글 폰트의 경우 base64로 인코딩 된 폰트 파일을 적용하지 않으면 한글 깨짐 현상이 발생한다. 위 예제의 경우 ttf 폰트 파일을 인코딩한 코드를 "NanumGothic-normal.js" 모듈로 만들어 import 하였다. ttf 폰트 파일의 인코딩은 아래 웹사이트를 이용하면 [그림 3]과 같이 쉽게 가능하다.

 

📌 Base64 Encoder: https://www.giftofspeed.com/base64-encoder/

 

[그림 3] Base64 Encoder을 이용해 ttf 파일을 base64로 인코딩

 

728x90
반응형