Front-end

HTML to PDF 변환기 (2)

elysia365 2023. 5. 21.

이번 포스팅에서는 지난 포스팅에 이어서 html2pdf 라이브러리를 사용하여 html array 를 pdf 로 변환하는 기능을 개발해보려고 한다.

Vue.js 2버전의 기본 프로젝트 환경이 구성되어있음을 전제로 글을 작성하였다.

html2pdf.js 라이브러리 설치

우선 html2pdf.js 라이브러리를 프로젝트에 설치하자

npm install --save html2pdf.js

설치가 완료되니 package.json 파일의 dependencyies 에 아래와 같이 "html2pdf.js": "^0.10.1" 가 추가된다.

"dependencies": {
  "core-js": "^3.8.3",
  "element-ui": "^2.15.13",
  "html2pdf.js": "^0.10.1",
  "vue": "^2.6.11"
},

기능 개발

나는 HtmlToPdfConverter 라는 이름의 클래스를 사용자에게 제공하여, html to pdf 변환 기능을 사용할 수 있게 하려고 한다.

htmlToPdfConverter.js 라는 이름으로 빈 파일을 하나 생성하고 차례로 작성해보겠다.

html2pdf.js 임포트

우선, html2pdf.js 를 임포트 한다.

import html2pdf from 'html2pdf.js';

클래스 생성

그리고, 클래스를 생성한다.

import html2pdf from 'html2pdf.js';
​
export class HtmlToPdfConverter {
​
    defaultOption = ''; // pdf 생성을 위한 option
​
    /**
     * HTML To PDF 를 위한 생성자
     * @param pdfFormat pdf 포맷 (e.g. 'a3', 'a4', ...)
     * @param pdfOrientation pdf 방향 (e.g. 'portrait' : 세로, 'landscape' : 가로)
     */
    constructor(pdfFormat, pdfOrientation) {
        this.defaultOption = {
            margin: 0,
            // filename: out.pdf,
            image: {type: 'jpg', quality: 0.98},
            html2canvas: {
                useCORS: true,
                allowTaint: true,
                scrollX: 0,
                scrollY: 0,
                scale: 1,
                dpi: 300,
                letterRendering: true,
                logging: false,
                ignoreElements: function (element) {
                    if (element.id === 'pdf-ignore-area') {
                        return true;
                    }
                },
            },
            jsPDF: {orientation: pdfOrientation, unit: 'mm', format: pdfFormat, compressPDF: true},
        };
    }
​
    /**
     * html element 단건을 pdf 로 생성하여 다운로드
     * @param element html element
     * @param filename pdf 파일명
     * @returns {Promise<*>}
     */
    async htmlToPdf(element, filename) {
        // 개발 예정
    }
​
    /**
     * html element 배열을 여러 페이지의 pdf 로 생성하여 다운로드
     * @param elements html element array
     * @param filename pdf 파일명
     * @returns {Promise<*>}
     */
    async htmlsToPdf(elements, filename, callback) {
        // 개발 예정
    }
}

HtmlToPdfConverter 인스턴스를 생성할 때, 변환할 pdf 의 용지 크기와 방향을 설정하도록 생성자를 구현할 것이다.

그리고, 사용자는 htmlToPdf(element, filename) 함수와 htmlsToPdf(elements, filename, callback) 함수를 호출하여 html element 단건을 pdf 로 변환하거나 html element 다건을 pdf 로 변환할 수 있도록 기능을 개발할 것이다.

생성자 코드 작성

우선, 생성자 내용 부터 채우자.

/**
 * HTML To PDF 를 위한 생성자
 * @param pdfFormat pdf 포맷 (e.g. 'a3', 'a4', ...)
 * @param pdfOrientation pdf 방향 (e.g. 'portrait' : 세로, 'landscape' : 가로)
 */
constructor(pdfFormat, pdfOrientation) {
    this.defaultOption = {
        margin: 0,
        // filename: out.pdf,
        image: {type: 'jpg', quality: 0.98},
        html2canvas: {
            useCORS: true,
            allowTaint: true,
            scrollX: 0,
            scrollY: 0,
            scale: 1,
            dpi: 300,
            letterRendering: true,
            logging: false,
            ignoreElements: function (element) {
                if (element.id === 'pdf-ignore-area') {
                    return true;
                }
            },
        },
        jsPDF: {orientation: pdfOrientation, unit: 'mm', format: pdfFormat, compressPDF: true},
    };
}

사용자가 pdf 용지 크기와 방향을 파라미터로 전달하면, 해당 속성과 함께 나머지 기본 속성들을 채워서 html2pdf.js 라이브러리의 사용법에 맞게 option 객체를 생성한다.

html2pdf.js 라이브러리 options 관련 설명은 html2pdf.js npm readme 를 참고하면 된다.

htmlToPdf 함수 코드 작성

내용은 간단하다. 생성자에서 설정한 defaultOptions 에 함수 파라미터로 전달 받은 filename 만 추가로 정의한 옵션을 사용하여 pdf 변환하는 html2pdf().set().from().save(); 함수를 호출하면 된다.

/**
 * html element 단건을 pdf 로 생성하여 다운로드
 * @param element html element
 * @param filename pdf 파일명
 * @returns {Promise<*>}
 */
async htmlToPdf(element, filename) {
    const opt = {...this.defaultOption, filename : `${filename}.pdf`};
    return html2pdf().set(opt).from(element).save();
}

htmlsToPdf 함수 코드 작성

html element array 의 첫번째 요소로 pdf 첫 페이지를 생성하고, 두번째 요소부터는 기존 pdf 에 addPage() 로 이어붙이는 방식으로 pdf 를 생성해주도록 하였다.

그리고, page 가 추가될때마다 callback 함수를 호출하도록 하여, 해당 함수의 caller 가 pdf page 추가될때마다 원하는 기능을 수행하도록 하였다.

나는 해당 callback 함수로 진행율 업데이트 하는 함수를 파라미터로 전달하여 사용하였다.

그리고, 함수 초반부에 element를 clone 한 이유는 caller 쪽에서 html 을 숨긴채로 pdf 변환하고 싶어서 hidden 처리를 해두었기 때문에 pdf 변환하기 전 hidden 처리된 요소들의 style.display 속성을 변경하여 pdf 에는 빈 페이지가 나오지 않게 하기 위함이다.

필요하면, clone element 에 transform scale 값을 변경해서 이미지를 원하는 비율로 확대 or 축소 후 pdf 변환할 수도 있다.

아무래도 브라우져에서 렌더링 되는 html view 와 pdf 변환된 view 의 사이즈가 정확히 일치하지는 않을 것이기 때문에 필요할 것이다.

/**
 * html element 배열을 여러 페이지의 pdf 로 생성하여 다운로드
 * @param elements html element array
 * @param filename pdf 파일명
 * @returns {Promise<*>}
 */
async htmlsToPdf(elements, filename, callback) {
    const opt = {...this.defaultOption, filename: `${filename}.pdf`};
    let pdf = null;
    for (let i = 0; i < elements.length; i++) {
        // html 이 화면에 보이지 않는 상태로 pdf 생성하면, 빈 페이지로 생성되기 때문에 html 복사 후 style 만 변경 한 뒤 pdf 생성
        const clone = elements[i].cloneNode(true);
        clone.style.display = "block";
        if (pdf == null) {
            // pdf 첫 페이지
            pdf = html2pdf().set(opt).from(clone).toPdf();
            callback();
        } else {
            // pdf 두번째 페이지부터는 기존 pdf 에 이어붙임
            pdf = pdf.get('pdf')
                .then((pdf) => {
                    pdf.addPage();
                    callback();
                })
                .from(clone)
                .toContainer()
                .toCanvas()
                .toPdf();
        }
        // 메모리 누수를 방지하기 위해
        clone.remove();
    }
    return pdf.save().then(() => {
        pdf = null;
        console.log(`${filename} 생성 완료`);
    });
}

전체 완성 코드

아래는 위의 과정을 거쳐 완성된 전체 코드이다.

import html2pdf from 'html2pdf.js';
​
export class HtmlToPdfConverter {
​
    defaultOption = ''; // pdf 생성을 위한 option
​
    /**
     * HTML To PDF 를 위한 생성자
     * @param pdfFormat pdf 포맷 (e.g. 'a3', 'a4', ...)
     * @param pdfOrientation pdf 방향 (e.g. 'portrait' : 세로, 'landscape' : 가로)
     */
    constructor(pdfFormat, pdfOrientation) {
        this.defaultOption = {
            margin: 0,
            // filename: out.pdf,
            image: {type: 'jpg', quality: 0.98},
            html2canvas: {
                useCORS: true,
                allowTaint: true,
                scrollX: 0,
                scrollY: 0,
                scale: 1,
                dpi: 300,
                letterRendering: true,
                logging: false,
                ignoreElements: function (element) {
                    if (element.id === 'pdf-ignore-area') {
                        return true;
                    }
                },
            },
            jsPDF: {orientation: pdfOrientation, unit: 'mm', format: pdfFormat, compressPDF: true},
        };
    }
​
    /**
     * html element 단건을 pdf 로 생성하여 다운로드
     * @param element html element
     * @param filename pdf 파일명
     * @returns {Promise<*>}
     */
    async htmlToPdf(element, filename) {
        const opt = {...this.defaultOption, filename : `${filename}.pdf`};
        return html2pdf().set(opt).from(element).save();
    }
​
    /**
     * html element 배열을 여러 페이지의 pdf 로 생성하여 다운로드
     * @param elements html element array
     * @param filename pdf 파일명
     * @returns {Promise<*>}
     */
    async htmlsToPdf(elements, filename, callback) {
        const opt = {...this.defaultOption, filename: `${filename}.pdf`};
        let pdf = null;
        for (let i = 0; i < elements.length; i++) {
            // html 이 화면에 보이지 않는 상태로 pdf 생성하면, 빈 페이지로 생성되기 때문에 html 복사 후 style 만 변경 한 뒤 pdf 생성
            const clone = elements[i].cloneNode(true);
            clone.style.display = "block";
            if (pdf == null) {
                // pdf 첫 페이지
                pdf = html2pdf().set(opt).from(clone).toPdf();
                callback();
            } else {
                // pdf 두번째 페이지부터는 기존 pdf 에 이어붙임
                pdf = pdf.get('pdf')
                    .then((pdf) => {
                        pdf.addPage();
                        callback();
                    })
                    .from(clone)
                    .toContainer()
                    .toCanvas()
                    .toPdf();
            }
            // 메모리 누수를 방지하기 위해
            elements[i].remove();
            clone.remove();
        }
        return pdf.save().then(() => {
            pdf = null;
            console.log(`${filename} 생성 완료`);
        });
    }
}

회고

HtmlToPdfConverter 를 이용해 html element array 를 pdf 로 변환할 때, 안정적으로 브라우져의 메모리 관리를 하기 위해서는 caller 쪽 개발도 중요하다.

다음 포스팅에서는 해당 클래스를 사용하는 쪽의 개발 내용에 대해 작성해보겠다.

댓글