Dandy Now!
  • [React.js] jsPDF를 이용한 웹 화면 PDF 내보내기 중 이슈: 페이지 오버플로우 이미지 잘림 문제
    2024년 07월 24일 11시 06분 31초에 업로드 된 글입니다.
    작성자: DandyNow
    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
    반응형
    댓글