본문으로 바로가기

html -> pdf 다운로드 (html2canvas + jsPDF)

category Frontend/Javascript 2021. 2. 26. 15:28
반응형

데이터의 결과를 나타내는 페이지를 보고서 형식 비슷하게 출력을 해야하는데 html 내용을 pdf로 다운로드 할 수 있는 라이브러리가 있어서 정리하는 글이다.

html2canvas

html2canvas 라이브러리는 이름 대로 html에 있는 내용을 canvas에 옮겨담아 사진처럼 찍어내주는 라이브러리이다. html2canvas.js

공식 홈페이지에서 볼 수 있듯이 샘플 예제도 심플하다. (다운로드를 하기위해 스크립트를 일부 추가)

<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
<div id="capture" style="padding: 10px; background: #f5da55">
    <h4 style="color: #000; ">Hello world!</h4>
</div>
function saveAs(uri, fileName) {
  const link = document.createElement("a");
  link.href = uri;
  link.download = fileName;

  document.body.appendChild(link);

  link.click();

  document.body.removeChild(link);
}
html2canvas(document.querySelector("#capture")).then(canvas => {
    saveAs(canvas.toDataURL(), 'fileName.png');
});

위의 소스로 실행하면 바로 이미지가 다운로드된다.

참고로 IE에서는 Promise가 먹히지 않기 때문에 bluebirdjs 나 polifill 같은 것을 이용해야하지만 글 작성 기준 2021-02-26 2개다 되지 않았음 (IE 11 테스트)

CDN 방식을 원하지 않는다면 아래 파일을 다운로드 받는다.(최신 버전과 파일의 버전이 다를 수 있음)

html2canvas.min.js
60.8 kB


jsPDF

jsPDF는 자바스크립트로 html의 내용을 pdf로 다운로드 할 수 있게 도와주는 라이브러리이다.jsPDF

Promise 관련 오류가 발생할 수 있으니 bluebirdjs 나 샘플 스크립트를 추가하도록 한다.

bluebird.min.js
79.5 kB

<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.js"></script>
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.4/jspdf.min.js"></script>

jsPDF의 CDN 방식이 싫다면 아래 파일을 다운받아서 쓰도록한다. (최신 버전과 파일의 버전이 다를 수 있음)

jspdf.min.js
307.6 kB

이 녀석의 대중적인 샘플 소스를 구글링해서 찾아보면 다음과 같다.(onrendered 이거는 이제 사용하지 않는다)

html2canvas(document.querySelector("#capture")).then(canvas => {
  // base64 url 로 변환
  var imgData = canvas.toDataURL('image/jpeg');

  var imgWidth = 210; // 이미지 가로 길이(mm) A4 기준
  var pageHeight = imgWidth * 1.414; // 출력 페이지 세로 길이 계산 A4 기준
  var imgHeight = canvas.height * imgWidth / canvas.width;
  var heightLeft = imgHeight;
  var margin = 20;

  var doc = new jsPDF('p', 'mm', 'a4');
  var position = 0;

  // 첫 페이지 출력
  doc.addImage(imgData, 'jpeg', margin, position, imgWidth, imgHeight);
  heightLeft -= pageHeight;

  // 한 페이지 이상일 경우 루프 돌면서 출력
  while (heightLeft >= 20) {
    position = heightLeft - imgHeight;
    doc.addPage();
    doc.addImage(imgData, 'jpeg', margin, position, imgWidth, imgHeight);
    heightLeft -= pageHeight;
  }

  // 파일 저장
  doc.save('sample.pdf');
});

그러면 사진과 같이 pdf로 다운로드가 잘 되는걸 볼 수 있다.


페이지를 나눌 때 생긴 문제

하지만 페이지를 나눌 때 html 내용이 길면 a4 크기만큼만 출력되고 나머지는 잘림 현상이 나타난다.

이미지에서 볼수 있듯이 차트 그림이 전 페이지에 포함되어있었으나 다음 페이지에 또 나오고 전 페이지는 잘렸다.

하루종일 구글링을 했지만 방법을 못찾던 찰나 구세주의 블로그를 발견했다. 출처 블로그

출처 블로그를 따라가면 js 소스가 있는데 여태 본 소스와는 다르다. 설명을 하자면 이미지로 만들 태그들의 그룹을 배열로 생성하여 렌더링을 먼저 한다음 이미지 URL을 전부 배열에 추가하고 그것들을 반복문 돌면서 현재 출력할 이미지의 높이가 a4 높이에 맞게 남은 공간이면 출력하고 아니면 페이지를 추가한다음 이미지를 추가하는 형식이다.

<div class="pdf_page">1</div>
<div class="pdf_page">2</div>
<div class="pdf_page">3</div>
<div class="pdf_page">4</div>
<div class="pdf_page">5</div>

예를 들어 pdf_page 라는 클래스의 div 태그 5개를 이미지화 하여 pdf로 출력하고자 할 때, 첫 번째 div의 높이가 a4의 60%를 차지하고 두 번째 div의 높이가 a4의 50%를 차지한다고 하면 이미 110%여서 오버 상태이기 때문에 위의 이미지 처럼 잘릴 것이다.

하지만 출처 블로그의 샘플 소스는 이를 체크하여 a4 높이에 맞게 남은 공간이 이미지 높이보다 작을 경우 페이지를 추가하고 이미지를 추가하는 형식이다.

$(fucntion() {
  $("#savePdf").click(function() { // pdf저장 button id
      //$(".ad_i, .wrap_chart_btn").css("display", "none"); // 불필요한 태그 숨김
    document.getElementById("loading").style.display = "block"; // 로딩 이미지 보이기
    //$("#loading").show(); jquery 사용할 경우
      //$("#main_frame", parent.document).parents("body").find("#loading").show(); // 부모 frame에 있는 loading 보이기

    // setTImeout을 하는 이유는 html2canvas를 불러오는게 너무 빨라서 앞의 js가 먹혀도 반영되지 않은 것처럼 보임
    // 따라서 0.1 초 지연 발생 시킴
      setTimeout(function() {
        createPdf();
      }, 100);
  });
});

var renderedImg = new Array;

var contWidth = 200, // 너비(mm) (a4에 맞춤)
    padding = 5; //상하좌우 여백(mm)

function createPdf() { //이미지를 pdf로 만들기
  document.getElementById("loading").style.display = "block"; // 로딩 이미지 보이기

  var lists = document.querySelectorAll(".pdf_page"),
      deferreds = [],
      doc = new jsPDF("p", "mm", "a4"),
      listsLeng = lists.length;

  for (var i = 0; i < listsLeng; i++) { // pdf_page 적용된 태그 개수만큼 이미지 생성
    var deferred = $.Deferred();
    deferreds.push(deferred.promise());
    generateCanvas(i, doc, deferred, lists[i]);
  }

  $.when.apply($, deferreds).then(function () { // 이미지 렌더링이 끝난 후
    var sorted = renderedImg.sort(function(a,b){return a.num < b.num ? -1 : 1;}), // 순서대로 정렬
        curHeight = padding, //위 여백 (이미지가 들어가기 시작할 y축)
        sortedLeng = sorted.length;

    for (var i = 0; i < sortedLeng; i++) {
      var sortedHeight = sorted[i].height, //이미지 높이
          sortedImage = sorted[i].image; //이미지

      if( curHeight + sortedHeight > 297 - padding * 2 ){ // a4 높이에 맞게 남은 공간이 이미지높이보다 작을 경우 페이지 추가
        doc.addPage(); // 페이지를 추가함
        curHeight = padding; // 이미지가 들어갈 y축을 초기 여백값으로 초기화
        doc.addImage(sortedImage, 'jpeg', padding , curHeight, contWidth, sortedHeight); //이미지 넣기
        curHeight += sortedHeight; // y축 = 여백 + 새로 들어간 이미지 높이
      } else { // 페이지에 남은 공간보다 이미지가 작으면 페이지 추가하지 않음
        doc.addImage(sortedImage, 'jpeg', padding , curHeight, contWidth, sortedHeight); //이미지 넣기
        curHeight += sortedHeight; // y축 = 기존y축 + 새로들어간 이미지 높이
      }
    }
    doc.save('resultReport.pdf'); //pdf 저장

    curHeight = padding; //y축 초기화
    renderedImg = new Array; //이미지 배열 초기화

    document.getElementById("loading").style.display = "none"; // 로딩 이미지 숨기기
    //$("#loading").hide(); jquery 사용할 경우
    //$("#main_frame", parent.document).parents("body").find("#loading").hide(); 부모 frame에 loading 태그가 있는 경우
    //$(".ad_i, .wrap_chart_btn").css("display", ""); 기존에 숨겼던 태그를 다시 보이게 하기
  });
}

function generateCanvas(i, doc, deferred, curList){ //페이지를 이미지로 만들기
  var pdfWidth = $(curList).outerWidth() * 0.2645, //px -> mm로 변환
      pdfHeight = $(curList).outerHeight() * 0.2645,
      heightCalc = contWidth * pdfHeight / pdfWidth; //비율에 맞게 높이 조절
  html2canvas( curList ).then(
    function (canvas) {
      var img = canvas.toDataURL('image/jpeg', 1.0); //이미지 형식 지정
      renderedImg.push({num:i, image:img, height:heightCalc}); //renderedImg 배열에 이미지 데이터 저장(뒤죽박죽 방지)     
      deferred.resolve(); //결과 보내기
    }
  );
}

위의 소스를 사용하면 잘림 현상은 방지 할 수 있으며 이미지로 나타낼 html 태그들의 영역을 처음부터 잘 나누어야한다.

반응형