[Vue.js] Html2Canvas로 PDF & Print export 하기

Castle_Junny·2023년 3월 11일
2
post-thumbnail

0. 서론

프로젝트를 진행하다보니 특정 화면의(컴포넌트) 전체 (스크롤 아래까지)를 스냅샷 뜨고 싶다는 고객의 요구 사항이 있었다. 지도 지반의 서비스를 구축하고 있어서 고객은 지도까지 완벽하게 나오길 바랬다.

그래서 canvas를 생성하여 png, pdf, print 까지 할 수 있도록 기능을 구성했다.

npm에 찾아보면 vue-html2canvas가 있는데, 그걸 사용하지 않고 기본 html2canvas 라이브러리를 사용하였다.
vue-html2canvas가 사용하기에는 더 편하긴 했는데, 유연성하게 사용하기에는 기본이 좋겠다고 생각하여 기본 html2canvas를 사용하였다. (주절주절...)

0.1 canvas 생성과 추출 flow

1. html2canvas

html2canvas는 html화면을 캡쳐하여 이미지 파일로 저장할 수 있는 라이브러리이다.

동작되는 원리는 공식 홈페이지에 명시된 것처럼 선택한 DOM을 기반으로 읽어와서 화면을 표현한다. 그래서 이미지나, print를 했을 때 보고있는 화면과 조금 다를 수 있다. (css, cors 문제로 인해 이상하게 나올 수 있다.)

해당 라이브러리는 Promise 형식으로 되어 있기에 .then()~~ 하여 callback을 받을 수 있다. 인터넷에서보이는 option에 onrendered를 사용하는 것을 흔히 볼 수 있는데, 경험 상 동작이 안할 수 있기에 .then() 하는 방식으로 진행했다.

1.1 cdn

<script src="html2pdf.bundle.min.js"></script>

1.2 npm

npm install --save html2pdf.js

1.3 적용 (옵션 (링크))

import html2canvas from 'html2canvas';

// canvas 옵션은 공식 사이트를 보면 좋을 거 같다.
canvasOptions: {
                    allowTaint: true,
                    useCORS: true,
                    height: 0,
                    width: 0,
                    scale: 1.3,
                    dpi: 600,
                    scrollX: 0,
                    fileExtension: "pdf",
                },

html2canvas(DOM 선택(id, class, name...), canvasOption)
.then(canvas => {

// 화면 아래에 캡쳐한 DOM 사진을 붙여넣는다.
			document.body.appendChild(canvas)

// <a> 태그를 생성하고 하이퍼링크 클릭 등 이벤트 적용

			let a = document.createElement('a');
	        a.href = canvas.toDataURL("image/jpeg").replace("image/jpeg", "image/octet-stream");
	        a.download = '파일이름.jpeg';
	        a.click();

});

2. jsPDF

html2canvas에서 캡쳐한 canvas를 pdf파일로 변환해준다. 이 때 pdf 파일의 규격을 설정할 수 있는데, 여기선 a4용지 사이즈를 기준으로 하였다.

그리고 jsPDF 객체 생성 시 용지의 방향unit , 용지 규격(ex. a4, b2..) 을 지정해줘야 하는데, [210, 297] 이렇게 배열로도 입력이 가능하다. (많은 규격을 제공하지 않는다.)

나는 클라이언트 입장에서 다양한 규격의 용지를 사용할 수 있다고 생각하여 배열로 입력하도록 했다.

const pdf = new jsPDF(pageDirection, 'mm', paperStandardSize[letterSize]);

2.1 cdn

<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.2/jspdf.min.js"></script>

2.2 npm

npm i jsPDF

2.3 적용

여기서부터 생각을 할 필요가 있다.

html2canvas를 통해서 사진을 찍었다.
모든 사진은 x, y의 길이와 비율이 있고 천차만별로 사이즈가 다르다.
그리고 용지가 세로인지 가로인지에 따라서 폭과 높이의 길이가 반대로 되니 이것도 로직을 생각해야한다.
이런 경우의 수( 가로 / 세로 방향, 용지 사이즈)를 전제로 두고 클라이언트가 지정한 용지 규격에 사진을 넣기 위해선 간단한 수학 계산을 해야한다.

import html2canvas from 'html2canvas';
import { jsPDF } from "jspdf";

// canvas 옵션은 공식 사이트를 보면 좋을 거 같다.
canvasOptions: {
                    allowTaint: true,
                    useCORS: true,
                    height: 0,
                    width: 0,
                    scale: 1.3,
                    dpi: 600,
                    scrollX: 0,
                    fileExtension: "pdf",
                },

html2canvas(DOM 선택(id, class, name...), canvasOPtion)
.then(canvas => {

// canvas가 추출한 사진을 용지 규격(a4 용지)에 맞게 정의하는 수식
//                 원본 가로(mm)	/  (원본 가로(mm) / a4용지 폭)						
	let imageWidth = canvas.width / (canvas.width / a4용지 폭);

//   [ 가로: 세로 = 가로':세로'] 을 하게되면 높이를 구할 수 있다.
	let imageHeight = (a4용지 폭 * canvas.height) / a4용지 폭;

const pdf = new jsPDF(용지방향, 'mm', 용지 규격);

// 첫페이지 출력 (imgData,    확장자 ,  시작x,   시작y,  폭,     높이)
pdf.addImage(imgData, 'PNG', 0, 0, imageWidth, imageHeight);

// 사진이 한 페이지에 안담길 경우. 다음페이지로 넘어가야하기 때문에 
// 삽입하려는 사진의 높이가 a4용지의 높이보다 큰지 계산
let heightLeft = imageHeight - initOptions.imgHeight;

// 계산 결과가 0보다 크면 다음 장을 추가한다. 
while (heightLeft >= 0) {

// 이때 위치는 원본 사진에서 출력한 첫 번째장의 높이 다음부터 출력을 한다.
// 그렇기에 position을 아래와 같이 한다. 
    let position = heightLeft - imageHeight;
    pdf.addPage();
    pdf.addImage(imgData, 'PNG', 0, position, imageWidth, imageHeight);
    heightLeft -= initOptions.imgHeight;
}
// 파일 저장
pdf.save('pdf export.pdf');

});

3. 전체 코드

print 기능의 경우 라이브러리 설치를 한게 아나라 자바스크립트의 기본 API를 사용하였다.

위에서 html2canvas를 이용해서 canvas를 생성하는 것만 이해한다면 아래 코드를 이해하는 것에 어려움은 없을 것 같다.

  • canvasOption을 class로 생성한 이유
    • 화면 별로 캡처하는 옵션이 다를 수 있기 때문에 호출 받을 때 마다 새로운 객체를 생성하여 다른 화면에서의 옵션과 섞이는 것을 예방 할 수 있다고 생각했다.
    • 그러니까 함수가 실행 될 때마다 새로운 옵션을 생성하기 때문에 변형에 안정적이라고 생각했다.
import _ from 'lodash';
import html2canvas from "html2canvas";
import {jsPDF} from "jspdf";

const getNowDate = () => {
    /* 파일 이름 설정 : (default) 현재 시간 기준 */
    const now = new Date();
    return `${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}${now.getHours()}${now.getMinutes()}`;

}

/* 용지 별 규격(mm) */
const paperStandardSize = {
    a1: [594, 841],
    a2: [420, 594],
    a3: [297, 420],
    a4: [210, 297],
    a5: [148, 210],
    a6: [105, 148],
    b1: [788, 1091],
    b2: [788, 545],
    b3: [545, 394],
    b4: [394, 272],
    b5: [272, 197],
    b6: [197, 136],
};

class initOptions {
    _canvasOptions = {
        fileName: '',
        allowTaint: true,
        useCORS: true,
        logging: true,
        height: 0,
        width: 0,
        scale: 1.3,
        dpi: 600,
        scrollX: 0,
        scrollY: 0,
        fileExtension: "pdf",
    }

    constructor(canvasOptions) {
        this.canvasOptions = _.merge(this._canvasOptions, canvasOptions);
    }

    get canvasOptions() {
        return this._canvasOptions;
    }

    set canvasOptions(value) {
        this._canvasOptions = _.merge(this._canvasOptions, value);
    }
}

export default {
    install(Vue) {

/**********************************************
  pdf export
*******************************************************/
        Vue.prototype.$exportPrint = (element) => {

            /* export 할 element Id 미지정 시 return */
            if (element === null || element === undefined) {
                return console.log("DOM ID를 입력해 주세요.");
            }

            /* export 할 element Id 명 */
            const elementDOM = document.querySelector(element);
            let editedOptions = new initOptions({});
            /* 선택한 element Id의 가로 세로 폭 (캡쳐 이미지 사이즈 지정) */
            editedOptions.canvasOptions.height = elementDOM.height;
            editedOptions.canvasOptions.width = elementDOM.width

            if (paperStandardSize.a4[0] > paperStandardSize.a4[1]) {
                editedOptions.imgWidth = paperStandardSize.a4[0];
                editedOptions.imgHeight = paperStandardSize.a4[1];
            } else {
                editedOptions.imgWidth = paperStandardSize.a4[1];
                editedOptions.imgHeight = paperStandardSize.a4[0];
            }

            html2canvas(elementDOM, editedOptions.canvasOptions).then(canvas => {
                const my_window = window.open("", '', `width=${editedOptions.canvasOptions.width},height=${editedOptions.canvasOptions.height}`);
                my_window.document.body.appendChild(canvas);
                my_window.focus();
                my_window.print();
                my_window.close();
            });

        };

/**********************************************
   pdf, img export
*******************************************************/

        Vue.prototype.$pdfExport = function (element, letterSize, pageDirection, options) {

            /* export 할 element Id 미지정 시 return */
            if (element === null || element === undefined) {
                return console.log("DOM ID를 입력해 주세요.");
            }

            /* export 할 element Id 명 */
            const elementDOM = document.querySelector(element);

            /*  기본 옵션  */
            let editedOption = new initOptions(options);

            /* 선택한 element Id의 가로 세로 폭 (캡쳐 이미지 사이즈 지정) */
            editedOption.canvasOptions.height = elementDOM.scrollHeight;
            editedOption.canvasOptions.width = elementDOM.scrollWidth;

            switch (pageDirection.toLowerCase()) {
                case "l":
                    if (paperStandardSize[letterSize][0] > paperStandardSize[letterSize][1]) {
                        editedOption.imgWidth = paperStandardSize[letterSize][0];
                        editedOption.imgHeight = paperStandardSize[letterSize][1];
                    } else {
                        editedOption.imgWidth = paperStandardSize[letterSize][1];
                        editedOption.imgHeight = paperStandardSize[letterSize][0];
                    }
                    break;
                case "p" :
                    if (paperStandardSize[letterSize][0] < paperStandardSize[letterSize][1]) {
                        editedOption.imgWidth = paperStandardSize[letterSize][0];
                        editedOption.imgHeight = paperStandardSize[letterSize][1];
                    } else {
                        editedOption.imgWidth = paperStandardSize[letterSize][1];
                        editedOption.imgHeight = paperStandardSize[letterSize][0];
                    }
                    break;
                default:
                    return console.log("용지 방향을 가로(l), 세로(p)로 구분해주세요. ")
            }

            const exportSnapshot = ({canvas, fileName, type, replaceValue = ''}) => {
                let a = document.createElement('a');
                a.href = canvas.toDataURL(type).replace(type, replaceValue);
                a.download = fileName;
                a.click();
            }

            /* 클라이언트의 성격에 따라 pdf, png 파일을 구분하여 받는 분기문 (기본은 pdf)  */
            switch (editedOption.canvasOptions.fileExtension) {
                case "jpg" :
                case "jpeg":
                    html2canvas(elementDOM, editedOption.canvasOptions).then(canvas => {

                        const fileName = `${editedOption.canvasOptions.fileName}Jpeg_Export.jpeg`
                        const type = "image/jpeg"
                        const replaceValue = "image/octet-stream"
                        exportSnapshot({canvas, fileName, type, replaceValue})
                    });
                    break;
                case "png":
                    html2canvas(elementDOM, editedOption.canvasOptions).then(canvas => {
                        const fileName = `${editedOption.canvasOptions.fileName}PNG_Export.png`
                        const type = "image/png"
                        exportSnapshot({canvas, fileName, type})
                    });
                    break;
                default:
                    html2canvas(elementDOM, editedOption.canvasOptions).then(canvas => {
                        const imgData = canvas.toDataURL('image/png');
                        let imageWidth = canvas.width / (canvas.width / editedOption.imgWidth);
                        let imageHeight = (editedOption.imgWidth * canvas.height) / canvas.width;

                        const pdf = new jsPDF(pageDirection.toLowerCase(), 'mm', paperStandardSize[letterSize]);

                        // 첫페이지 출력 (imgData,    확장자 ,  시작x,   시작y,  폭,     높이)
                        pdf.addImage(imgData, 'PNG', 0, 0, imageWidth, imageHeight);

                        let heightLeft = imageHeight - editedOption.imgHeight;

                        while (heightLeft >= 0) {
                            let position = heightLeft - imageHeight;
                            pdf.addPage();
                            pdf.addImage(imgData, 'PNG', 0, position, imageWidth, imageHeight);
                            heightLeft -= editedOption.imgHeight;
                        }
                        // 파일 저장
                        pdf.save(`${editedOption.canvasOptions.fileName}Pdf_Export.pdf`);
                    });
                    break;
            }

        };
    }
};

0개의 댓글