구현하게 된 배경

회사에서 웹뷰이지만 모바일에서 PDF가 이미지로 보여야 한다는 요구사항을 받았습니다.

여기서 제약사항은 HTML, CSS, JavaScript만 사용해야 한다는 것입니다.

결과물은 페이지가 한 개만 있는 pdf 파일을 웹뷰로 보여주는 게 끝이었습니다(정적인 페이지).

 

모바일에서 웹뷰를 띄우고 거기다가 PDF를 이미지로 변환하는 것이기 때문에 너무 무거우면 안 된다고 생각했습니다.

그래서 바닐라 자바스크립트를 사용해서만 구현하게 되었습니다.

 

 

PDF.js란?

웹 표준 호환 HTML5 Canvas를 사용해 PDF 파일은 렌더링 하는 자바스크립트의 라이브러리입니다.

모질라 제단의 Andreas Gal가 2011년에 런칭 했습니다.

웹 브라우저에서 기본적으로 PDF 문서를 볼 수 있는 방법을 제공하기 위해 만들어졌으며 문서를 표시하기 위한 코드가

브라우저에서 샌드박스 처리되기 때문에 브라우저 외부에서 PDF 문서를 열 때 보안 위험을 방지합니다.

HTML5의 Canvas를 사용하기 때문에 빠른 렌더링 속도를 보여줍니다.

* 문서를 이미지화하기 때문에 PDF 내 텍스트를 복사하는 것을 방지할 수 있습니다.

 

공식 사이트에 보면 promise 를 사용하는데 자바스크립트 promise에 대한 개념을 모른다면 해당 내용을 이해하고

라이브러리를 적용 해보는것을 권장하고 있습니다.

(하지만 해당 라이브러리를 활용해 방대한 프로젝트를 만들고 있는게 아니라 우선 사용해 봤습니다.)

 

 

개발에 참고한 링크들

1. PDF.JS 공식 사이트

https://mozilla.github.io/pdf.js/

 

 

2. PDF.JS 공식 예제 코드

https://mozilla.github.io/pdf.js/examples/

 

 

3. PDF.JS 라이브러리 docs

https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html

 

 

구현한 코드

 

더보기
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" type="text/css" href="style.css">
    <link rel="stylesheet" href="https://fonts.sandbox.google.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
    <script src="//mozilla.github.io/pdf.js/build/pdf.js"></script>
    <script defer src="main.js"></script>
    <title>Title</title>
</head>
<body>
    <main class="main-notice">
        <div class="notice">
            <div class="notice__title">
                <h3>sample 1-1</h3>
            </div>
            <canvas id="jsSampleOneCanvas" class="notice__canvas"></canvas>
            <div class="notice__btn" >
                <span class="material-symbols-outlined">
                    expand_more
                </span>
            </div>
        </div>
        <div class="notice">
            <div class="notice__title">
                <h3>sample 1-2</h3>
            </div>
            <canvas id="jsSampleTwoCanvas" class="notice__canvas"></canvas>
            <div class="notice__btn">
                <span class="material-symbols-outlined">
                    expand_more
                </span>
            </div>
        </div>
        <div class="notice">
            <div class="notice__title">
                <h3>sample 1-3</h3>
            </div>
            <canvas id="jsSampleThreeCanvas" class="notice__canvas"></canvas>
            <div class="notice__btn">
                <span class="material-symbols-outlined">
                    expand_more
                </span>
            </div>
        </div>
        <div class="notice">
            <div class="notice__title">
                <h3>sample 1-4</h3>
            </div>
            <canvas id="jsSampleFourCanvas" class="notice__canvas"></canvas>
            <div class="notice__btn">
                <span class="material-symbols-outlined">
                    expand_more
                </span>
            </div>
        </div>
    </main>
</body>
</html>

 

더보기
let noticeBtnAll = document.querySelectorAll(".notice__btn");

const SAMPLE_ONE_CANVAS = document.getElementById("jsSampleOneCanvas");
const SAMPLE_TWO_CANVAS = document.getElementById("jsSampleTwoCanvas");
const SAMPLE_THREE_CANVAS = document.getElementById("jsSampleThreeCanvas")
const SAMPLE_FOUR_CANVAS = document.getElementById("jsSampleFourCanvas")

const CMAP_URL = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.5.207/cmaps/';

const CANVAS_ARRAY = [SAMPLE_ONE_CANVAS,
                      SAMPLE_TWO_CANVAS,
                      SAMPLE_THREE_CANVAS,
                      SAMPLE_FOUR_CANVAS];

// PDF.JS (pdf to image convertor)
let pdfJsLib = window['pdfjs-dist/build/pdf'];

pdfJsLib.GlobalWorkerOptions.workerSrc = '//mozilla.github.io/pdf.js/build/pdf.worker.js';

function imgOpenClose(event) {
    let btn = event.target;
    let btnDiv = btn.parentNode;
    let canvas = btnDiv.parentNode.querySelector(".notice__canvas");

    if(!canvas.style.display || canvas.style.display === "none") {
        canvas.style.display = "block";
        btn.innerHTML = "expand_less";
        btnDiv.style.borderBottom = "1px solid rgba(128, 128, 128, 0.3)";
    } else {
        canvas.style.display = "none";
        btn.innerHTML = "expand_more";
        btnDiv.style.borderBottom = "none";
    }
}

function pdfToImg(urlArr) {

    let pdfUrl = ["/static/pdf/1-1.pdf",
                  "/static/pdf/1-2.pdf",
                  "/static/pdf/1-3.pdf",
                  "/static/pdf/1-4.pdf"];

    for (let i = 0; i < pdfUrl.length; i++) {
        pdfRender(pdfUrl[i], CANVAS_ARRAY[i]);
    }
}

function pdfRender(url, canvas) {
    let loadingTask = pdfJsLib.getDocument({
        cMapPacked: true,
        disableFontFace: true,
        cMapUrl: CMAP_URL,
        url: url
    });

    loadingTask.promise.then(function (pdf) {
        // Fetch the first page
        let pageNumber = 1;
        pdf.getPage(pageNumber).then(function (page) {

            let scale = 1.5;
            let viewport = page.getViewport({scale: scale});

            // Prepare canvas using PDF page dimensions
            let context = canvas.getContext('2d');
            canvas.height = viewport.height;
            canvas.width = viewport.width;

            // Render PDF page into canvas context
            let renderContext = {
                canvasContext: context,
                viewport: viewport
            };
            let renderTask = page.render(renderContext);
            renderTask.promise.then(function () {
            });
        });
    }, function (reason) {
        console.error(reason);
    });
}

function init() {
    for(const btn of noticeBtnAll) {
        btn.childNodes[1].addEventListener("click", imgOpenClose);
    }
    pdfToImg();
}

init();

 

더보기
body {
    display: flex;
    align-items: center;
    flex-direction: column;
    margin: 0;
}

.notice {
    width: 100vw;
    margin-bottom: 10px;
}

.notice__title {
    display: flex;
    align-items: center;
    background-color: rgba(128, 128, 128, 0.1);
    border-bottom: 1px solid rgba(0, 0, 0, 0.3);
    border-top: 1px solid rgba(0, 0, 0, 0.3);
    border-top-left-radius: 15px;
    border-top-right-radius: 15px;
}

.notice__title h3 {
    margin-left: 13px;
}

.notice__btn {
    width: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
}


.material-symbols-outlined {
    font-variation-settings:
            'FILL' 0,
            'wght' 800,
            'GRAD' 0,
            'opsz' 48;
    opacity: 0.5;
}

.notice-question {
    margin-top: 9vh;
    background-color: rgba(128, 128, 128, 0.05);
    border: 2px solid rgba(0, 0, 0, 0.2);
    border-radius: 12px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.notice-question__btn {
    border: none;
    background-color: rgba(128, 128, 128, 0.01);
    cursor: pointer;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 5px;
}

.notice-question__arrow span {
    background-color: rgba(128, 128, 128, 0.05);
}

.notice-question__title {
    display: flex;
    align-items: center;
    margin-left: 15px;
}

.notice-question__title h3 {
    display: flex;
    align-items: center;
    margin-left: 7px;
}

.notice-question__title img {
    width: 25px;
}

.notice__canvas {
    display: none;
    width: 100vw;
}

.modal-wrap {
    position:fixed;
    top:0;
    left:0;
    right:0;
    bottom:0;
    background:rgba(0,0,0,.5);
    font-size:0;
    text-align:center;
}

.modal-wrap:after {
    display:inline-block;
    height:100%;
    vertical-align:middle;
    content:'';
}

.modal-wrap .modal-inner  {
    display:inline-block;
    padding: 8px 8px;
    background:#fff;
    width:95vw;
    vertical-align:middle;
    font-size:15px;
}

.modal-inner img {
    width: 90vw;
}
  • HTML, CSS는 제외하고 자바스크립트 소스만 보셔도 됩니다.
  • 예제코드와 거의 똑같아서 주석이나 설명은 달아놓지 않았습니다.

 

트러블 슈팅) 한국어가 글자가 렌더링 되지 않는 문제

 

구현을 하면서 한국어를 입력한 부분의 글자가 깨지는 문제가 발생했습니다.

해당 내용을 해결하기 위해 찾아보다가 중국 개발자들이 해당 문제를 해결한 것을 봤습니다.

 

 

문제를 해결하는데 큰 도움이 된 링크 => https://github.com/mozilla/pdf.js/issues/12629

 

Part of the Chinese character cannot be rendered · Issue #12629 · mozilla/pdf.js

Attach (recommended) or Link to PDF file here: pdf link Configuration: Web browser and its version: chrome 86.0.4240.193 Operating system and its version: macOS 10.13.6 PDF.js version: 2.5.207 Is a...

github.com

 

확인해보니 CDN이 형식이 잘못된 데이터를 반환하는 것이라고 합니다.

bCmap 형식을 리턴해야 하는데 잘못된 형식이 반환되는 것입니다.

그래서 cMapUrl을 변경하니까 문제가 바로 해결되었습니다.

 

 

bCmap == binary cmap

* bCmap이란? => 링크

 

cMap(Character Maps)는 PostScript 나 기타 Adobe 제품에서 문자 코드를 CID 글꼴의 문자를 매핑하는데 사용되는 텍스트 파일

(CID 폰트에 대한 설명은 오른쪽 링크 참조(영문 사이트) => 링크)

주로 동아이사 문자 체계를 다룰 때 사용되고 이 기술은 레거시 기술이므로 최신 도구로 만든 pdf에서는 사용이 안된다고 합니다.

 

pdf.js는 이런 CID 폰트를 화면상에 나타내기 위해서는 CMap 파일이 필요하다고 합니다.

그래서 같은 아시아권인 중국 개발자가 해결한 방법으로 해결이 가능했던 것 같습니다.

 

 

구현하면서 의문점

  • pdf가 이미지로 변환된 파일을 다운로드 못하게 해야 하지 않는지?
    • 이 부분은 해당 PDF 파일을 따로 암호화를 진행 해야 한다는 것을 알게 되었습니다.
      그래서 민감한 정보가 들어있는 PDF파일은 암호화 토큰 값을 주입해서 위조 및 변형이 힘들도록 하는 방식을 적용합니다.
      * 제가 구현한 방식은 단순히 텍스트를 복사 붙여넣기만 못하도록 막는 방식입니다.
  • html <head> 태그 안에 PDF.js 에 필요한 스크립트 태그 하나 추가했는데 라이브러리가 쉽게 적용되는 이유는 무엇인지?
    • CDN에 대한 개념 이해 필요

 

 

반응형

+ Recent posts