이 영역을 누르면 첫 페이지로 이동
천천히 꾸준히 조용히 블로그의 첫 페이지로 이동

천천히 꾸준히 조용히

페이지 맨 위로 올라가기

천천히 꾸준히 조용히

천천히 꾸준히 조용히.. i3months 블로그

[PDF.js] PDF.js 완벽 가이드

  • 2024.11.07 15:39
  • 💡 솔루션
반응형

 

 

 

PDF.js는 웹에서 PDF 를 다룰 때 필요한 거의 모든 기능을 제공하는 라이브러리로 몇십년 전 부터 현재까지 꾸준히 업데이트 되고 있다. 

 

처음 라이브러리를 사용했던건 3년 전 정도인데.. 그때와 현재를 비교해도 정말 많은 기능이 추가됐다.

뭔가 PDF 관련 기능을 만들어야 할 때 개발하기 전 PDF.js 라이브러리가 제공하는 기능인지 먼저 확인하자. 

(https://github.com/mozilla/pdf.js)

 

npm을 통한 설치 / cdn을 사용해 script 태그로 추가 / 깃허브나 공식사이트에서 다운로드 

크게 세 가지 방법으로 초기 세팅을 마치고 필요한 기능이나 디자인을 추가하거나 수정하자. 

 

개발하면서 겪었던 몇 가지 이슈사항을 정리한다. 

 

 

1. 한글 폰트 관련 문제 

PDF.js 는 cmap(character map) 파일들을 브라우저 환경에서 사용할 수 있도록 번들링한 bcmap을 사용한다.

브라우저에서 폰트를 제대로 읽으려면 해당 bcmap 파일을 모두 프로젝트에 추가해 줘야 한다. 

 

(https://github.com/mozilla/pdfjs-dist/blob/master/cmaps/GBK-EUC-H.bcmap) 

위의 깃허브 링크에서 bcmap 파일을 모두 다운받고 프로젝트에 추가해주자.

 

추가한 후에는, 파일을 넣어 준 경로를 PDF.js 라이브러리가 읽을 수 있게 경로를 지정해 줘야 한다. 

라이브러리에 포함된 pdf.worker.mjs 파일을 수정하자. 

 

  async fetchBuiltInCMap(name) {
    const cachedData = this.builtInCMapCache.get(name);
    if (cachedData) {
      return cachedData;
    }
    let data;
    if (this.options.cMapUrl !== null) {
      //const url = `${this.options.cMapUrl}${name}.bcmap`;
      const url = `cmap/${name}.bcmap`;
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`fetchBuiltInCMap: failed to fetch file "${url}" with "${response.statusText}".`);
      }
      data = {
        cMapData: new Uint8Array(await response.arrayBuffer()),
        compressionType: CMapCompressionType.BINARY
      };
    } else {
      data = await this.handler.sendWithPromise("FetchBuiltInCMap", {
        name
      });
    }
    if (data.compressionType !== CMapCompressionType.NONE) {
      this.builtInCMapCache.set(name, data);
    }
    return data;
  }

 

 

사용하는 PDF.js 라이브러리의 버전에 따라 조금씩 다를 수 있겠지만..

fetchBuiltInCMap 함수를 찾아 해당 함수에서 bcmap 파일을 불러오는 부분을 수정해주자. 

 

 

2. 원하는 PDF 파일 읽어오기 

PDF.js는 프론트엔드쪽 라이브러리이고, 상용 서비스에서 프론트엔드 프로젝트 내부에 PDF 파일을 포함시켜놓지는 않으니 시스템을 구성할 때 백엔드 서버를 구축하고 백엔드 서버에게 PDF를 요청하는 방식으로 구성된다.

 

엔드포인트로 접근 시 PDF파일을 HTTP로 전달해주는 api는 만들었다고 하자.

PDF.js에서 해당 PDF 를 불러올 때는 라이브러리의 app.js 또는 viewer.mjs 부분을 조작해야 한다. 

 

해당 js에는 페이지 탐색 / 확대 및 축소 / 북마크 등 다양한 기능이 정의되어있고, async run 메서드에서 PDF 파일을 불러온다. 

 

async run(config) {
    await this.initialize(config);

    const { appConfig, eventBus } = this;
    let file;
    if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
      const queryString = document.location.search.substring(1);
      const params = parseQueryString(queryString);
      file = params.get("file") ?? AppOptions.get("defaultUrl");
      validateFileURL(file);
    } else if (PDFJSDev.test("MOZCENTRAL")) {
      file = window.location.href;
    } else if (PDFJSDev.test("CHROME")) {
      file = AppOptions.get("defaultUrl");
    }

    if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
      const fileInput = (this._openFileInput = document.createElement("input"));
      fileInput.id = "fileInput";
      fileInput.hidden = true;
      fileInput.type = "file";
      fileInput.value = null;
      document.body.append(fileInput);

      fileInput.addEventListener("change", function (evt) {
        const { files } = evt.target;
        if (!files || files.length === 0) {
          return;
        }
        eventBus.dispatch("fileinputchange", {
          source: this,
          fileInput: evt.target,
        });
      });

      // Enable dragging-and-dropping a new PDF file onto the viewerContainer.
      appConfig.mainContainer.addEventListener("dragover", function (evt) {
        for (const item of evt.dataTransfer.items) {
          if (item.type === "application/pdf") {
            evt.dataTransfer.dropEffect =
              evt.dataTransfer.effectAllowed === "copy" ? "copy" : "move";
            evt.preventDefault();
            evt.stopPropagation();
            return;
          }
        }
      });
      appConfig.mainContainer.addEventListener("drop", function (evt) {
        if (evt.dataTransfer.files?.[0].type !== "application/pdf") {
          return;
        }
        evt.preventDefault();
        evt.stopPropagation();
        eventBus.dispatch("fileinputchange", {
          source: this,
          fileInput: evt.dataTransfer,
        });
      });
    }

    if (!AppOptions.get("supportsDocumentFonts")) {
      AppOptions.set("disableFontFace", true);
      this.l10n.get("pdfjs-web-fonts-disabled").then(msg => {
        console.warn(msg);
      });
    }

    if (!this.supportsPrinting) {
      appConfig.toolbar?.print?.classList.add("hidden");
      appConfig.secondaryToolbar?.printButton.classList.add("hidden");
    }

    if (!this.supportsFullscreen) {
      appConfig.secondaryToolbar?.presentationModeButton.classList.add(
        "hidden"
      );
    }

    if (this.supportsIntegratedFind) {
      appConfig.findBar?.toggleButton?.classList.add("hidden");
    }

    if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
      if (file) {
        this.open({ url: file });
      } else {
        this._hideViewBookmark();
      }
    } else if (PDFJSDev.test("MOZCENTRAL || CHROME")) {
      this.setTitleUsingUrl(file, /* downloadUrl = */ file);

      this.externalServices.initPassiveLoading();
    } else {
      throw new Error("Not implemented: run");
    }
  },
  
  ////
  this.open({
      url: `/api/pdf?fldrid=${fldrId}&filepath=${filePath}&filename=${fileName}`
    });

 

 

여기서 url을 지정하는 부분을 쿼리스트링으로 받아온 데이터를 페이로드로, 구축한 api에 요청하면, 응답으로 받아온 PDF 파일을 PDF.js 내장 뷰어에서 보여준다. 

 

 

3. 논리페이지와 물리페이지 다루기 

개발 요청사항 중 PDF 내부적으로 관리하는 페이지인 논리페이지와 1부터 시작하는 물리페이지를 뷰어에서 함께 보여달라는 요청사항이 있었다. 

 

viewer.mjs 또는 pdf_thumbnail_view.js 를 조작해 논리페이지와 물리페이지를 모두 보여주자. 

 

class PDFThumbnailView {
  constructor({
    container,
    eventBus,
    id,
    logicalId,
    defaultViewport,
    optionalContentConfigPromise,
    linkService,
    renderingQueue,
    pageColors
  }) {
    this.id = id;
    this.logicalId = logicalId;
    this.renderingId = "thumbnail" + id;
    this.pageLabel = null;
    this.pdfPage = null;
    this.rotation = 0;
    this.viewport = defaultViewport;
    this.pdfPageRotate = defaultViewport.rotation;
    this._optionalContentConfigPromise = optionalContentConfigPromise || null;
    this.pageColors = pageColors || null;
    this.eventBus = eventBus;
    this.linkService = linkService;
    this.renderingQueue = renderingQueue;
    this.renderTask = null;
    this.renderingState = RenderingStates.INITIAL;
    this.resume = null;
    const anchor = document.createElement("a");
    anchor.href = linkService.getAnchorUrl("#page=" + id);
    anchor.setAttribute("data-l10n-id", "pdfjs-thumb-page-title");
    anchor.setAttribute("data-l10n-args", this.#pageL10nArgs);
    anchor.onclick = function () {
      linkService.goToPage(id);
      return false;
    };
    this.anchor = anchor;
    const div = document.createElement("div");
    div.className = "thumbnail";
    div.setAttribute("data-page-number", this.id);
    this.div = div;
    this.#updateDims();
    const img = document.createElement("div");
    img.className = "thumbnailImage";
    this._placeholderImg = img;
    div.append(img);
    const div2 = document.createElement("div");
    const span3 = document.createElement("span");
    span3.style.fontSize = "13px";
    span3.style.color = "black";
    div2.style.textAlign = "center";  
    span3.textContent = this.logicalId ? this.logicalId : this.id;
    div2.append(span3);
    div.append(div2);
    anchor.append(div);
    container.append(anchor);
  }
}

 

 

먼저 생성자에서 논리페이지를 보여줄 logicalId 변수를 설정해주자.

 

 

setDocument(pdfDocument) {
    if (this.pdfDocument) {
      this.#cancelRendering();
      this.#resetView();
    }
    this.pdfDocument = pdfDocument;
    if (!pdfDocument) {
      return;
    }
    
    const pageLabelsPromise = pdfDocument.getPageLabels();
    const firstPagePromise = pdfDocument.getPage(1);
    const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({
      intent: "display"
    });

    Promise.all([firstPagePromise, pageLabelsPromise]).then(([firstPdfPage, pageLabels]) => {
      const pagesCount = pdfDocument.numPages;
      const viewport = firstPdfPage.getViewport({
        scale: 1
      });
      for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
        let pageLabel = pageLabels ? pageLabels[pageNum - 1] : pageNum;        
        const thumbnail = new PDFThumbnailView({
          container: this.container,
          eventBus: this.eventBus,
          logicalId: pageLabel,
          id: pageNum,
          defaultViewport: viewport.clone(),
          optionalContentConfigPromise,
          linkService: this.linkService,
          renderingQueue: this.renderingQueue,
          pageColors: this.pageColors
        });
        this._thumbnails.push(thumbnail);
      }
      this._thumbnails[0]?.setPdfPage(firstPdfPage);
      const thumbnailView = this._thumbnails[this._currentPageNumber - 1];
      thumbnailView.div.classList.add(THUMBNAIL_SELECTED_CLASS);    
    }).catch(reason => {
      console.error("Unable to initialize thumbnail viewer", reason);
    });
  }

 

 

이후 setDocument 메서드에서 pdfDocument 객체에서 관리되고 있는 논리페이지를 가져와서 설정해준다. 

 

백엔드 단에서 처리될 문제이지, PDF.js 라이브러리에서 처리할 문제는 아니지만.. 

혹시 PDF 를 분할하는 경우 논리페이지와 물리페이지를 잘 구분해서 분할해야 한다. 

 

Map을 설정해 해당 물리페이지에 해당하는 논리페이지를 지정해 두고 (해당하는 논리페이지가 없다면 기본값 사용)

PDF 분할 시 설정해 둔 Map을 사용하자.

 

 

4. 뷰어 페이지 내 드래그 구현 

OCR 기능을 구현하기 위해 Ctrl + 드래그 시 OCR을 수행할 영역을 지정하는 기능을 만들어보자.

 

class DragArea {
    constructor() {
        this.currentDragArea = null;
        this.currentCoords = null;
        this.onDragStart();
        this.onMouseLeave();
    }

    onMouseLeave() {
        document.body.onmouseleave = () => {
            document.body.onmousemove = null;
        };
    }

    onDragStart() {
        document.body.onmousedown = (e) => {
            if (e.ctrlKey) {
                e.preventDefault();

                if (this.currentDragArea) this.currentDragArea.remove();
        
                const dragStartLeft = e.pageX;
                const dragStartTop = e.pageY;
                const dragArea = document.createElement("div");
        
                dragArea.className = "drag-area";
                document.body.appendChild(dragArea);
                this.currentDragArea = dragArea;
        
                this.onDrag(dragArea, dragStartLeft, dragStartTop);
                this.onDragEnd();
            }
        };
    }

    onDrag(box, dragStartLeft, dragStartTop) {
        document.body.onmousemove = (e) => {
            e.preventDefault();
        
            const x = e.pageX;
            const y = e.pageY;
            const left = x < dragStartLeft ? x : dragStartLeft;
            const top = y < dragStartTop ? y : dragStartTop;
            const width = Math.abs(dragStartLeft - x);
            const height = Math.abs(dragStartTop - y);
        
            box.style.transform = `translate(${left}px, ${top}px) scale(${width}, ${height})`;

            this.currentCoords = { left, top, width, height };
        };
    }

    onDragEnd() {
        document.body.onmouseup = (e) => {
            document.body.onmousemove = null;            
        };
    }
}

 

 

스타일은 적당히 설정해주고..

 

 

 

 

 

 

반응형
저작자표시 (새창열림)

'💡 솔루션' 카테고리의 다른 글

[Nginx] 리버스 프록시 서버 구축  (1) 2024.11.13
[Tomcat] 네트워크 드라이브 권한 관련 오류  (0) 2024.11.09
[Spring Batch] 메타데이터 테이블과 시퀀스  (0) 2024.11.05
[Spring Security] 인증 실패 오류 다루기  (0) 2024.06.20
[SQL Server] 지원하지 않는 TLS 버전 설정  (1) 2024.06.05

댓글

이 글 공유하기

  • 구독하기

    구독하기

  • 카카오톡

    카카오톡

  • 라인

    라인

  • 트위터

    트위터

  • Facebook

    Facebook

  • 카카오스토리

    카카오스토리

  • 밴드

    밴드

  • 네이버 블로그

    네이버 블로그

  • Pocket

    Pocket

  • Evernote

    Evernote

다른 글

  • [Nginx] 리버스 프록시 서버 구축

    [Nginx] 리버스 프록시 서버 구축

    2024.11.13
  • [Tomcat] 네트워크 드라이브 권한 관련 오류

    [Tomcat] 네트워크 드라이브 권한 관련 오류

    2024.11.09
  • [Spring Batch] 메타데이터 테이블과 시퀀스

    [Spring Batch] 메타데이터 테이블과 시퀀스

    2024.11.05
  • [Spring Security] 인증 실패 오류 다루기

    [Spring Security] 인증 실패 오류 다루기

    2024.06.20
다른 글 더 둘러보기

정보

천천히 꾸준히 조용히 블로그의 첫 페이지로 이동

천천히 꾸준히 조용히

  • 천천히 꾸준히 조용히의 첫 페이지로 이동

검색

방문자

  • 전체 방문자
  • 오늘
  • 어제

카테고리

  • 분류 전체보기 (675) N
    • Algorithm (205)
      • Data Structure (5)
      • Theory && Tip (33)
      • Baekjoon (166)
      • ALGOSPOT (1)
    • Spring (123)
      • Spring (28)
      • Spring Web MVC (20)
      • Spring Database (14)
      • Spring Boot (6)
      • Spring 3.1 (11)
      • Spring Batch (6)
      • Spring Security (16)
      • JPA (12)
      • Spring Data JPA (5)
      • QueryDSL (4)
      • eGovFramework (1)
    • Programming Language (74)
      • C (25)
      • C++ (12)
      • Java (19)
      • JavaScript (15)
      • Python (1)
      • PHP (2)
    • Computer Science (142)
      • Machine Learning (38)
      • Operating System (18)
      • Computer Network (28)
      • System Programming (22)
      • Universial Programming Lang.. (8)
      • Computer Architecture (4)
      • Compiler Design (11)
      • Computer Security (13)
    • Database (21)
      • Database (7)
      • MySQL (3)
      • Oracle (3)
      • Redis (5)
      • Elasticsearch (3)
    • DevOps (20)
      • Docker && Kubernetes (8)
      • Jenkins (4)
      • Amazon Web Service (8)
    • Mobile (28)
      • Android (21)
      • Flutter (7)
    • 💡 솔루션 (17)
    • 👥 모각코 (8)
    • 💬 기록 (7) N
    • 📚 공부 (5)
    • -------------- (25)

최근 글

나의 외부 링크

메뉴

  • 홈
반응형

정보

i3months의 천천히 꾸준히 조용히

천천히 꾸준히 조용히

i3months

블로그 구독하기

  • 구독하기
  • RSS 피드

티스토리

  • 티스토리 홈
  • 이 블로그 관리하기
  • 글쓰기
Powered by Tistory / Kakao. Copyright © i3months.

티스토리툴바