PDF.js란
Mozilla에서 만든 PDF 렌더링 라이브러리다. 브라우저의 Canvas API를 써서 PDF를 그린다.
Firefox의 기본 PDF 뷰어가 이걸로 만들어졌다. 꽤 안정적이고 기능도 많다.
설치
npm install pdfjs-dist
버전 주의. 4.x부터 ES 모듈만 지원한다. 구버전 프로젝트면 3.x 쓰는 게 편할 수 있다.
기본 사용법
import * as pdfjsLib from 'pdfjs-dist'
// 워커 설정 (중요!)
pdfjsLib.GlobalWorkerOptions.workerSrc =
`//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`
// PDF 로드
const loadingTask = pdfjsLib.getDocument(pdfUrl)
const pdf = await loadingTask.promise
// 페이지 가져오기
const page = await pdf.getPage(1)
// 캔버스에 렌더링
const scale = 1.5
const viewport = page.getViewport({ scale })
const canvas = document.getElementById('pdf-canvas')
const context = canvas.getContext('2d')
canvas.height = viewport.height
canvas.width = viewport.width
await page.render({
canvasContext: context,
viewport: viewport
}).promise
간단해 보이지만 함정이 있다.
워커 설정 삽질
PDF.js는 무거운 파싱 작업을 Web Worker에서 처리한다. 워커 설정을 안 하면:
Warning: Setting up fake worker.
가짜 워커로 돌아가긴 하는데, 메인 스레드에서 처리하니까 UI가 멈춘다. 큰 PDF 열면 브라우저가 얼어버림.
방법 1: CDN
pdfjsLib.GlobalWorkerOptions.workerSrc =
`//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`
간단하지만 오프라인에서 안 됨.
방법 2: 로컬 복사
import workerSrc from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc
Vite에서 ?url 붙이면 파일 경로를 가져온다. 빌드할 때 assets 폴더에 복사됨.
방법 3: 번들에 포함 (비추)
import PDFWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker'
pdfjsLib.GlobalWorkerOptions.workerPort = new PDFWorker()
워커를 번들에 포함시키는 방법. 번들 크기가 커지고 캐싱 효율이 떨어진다.
나는 방법 2를 썼다. 배포 환경에서 경로만 잘 맞추면 문제없다.
Vue 컴포넌트로 감싸기
<template>
<div class="pdf-viewer">
<canvas ref="canvasRef"></canvas>
<div class="controls">
<button @click="prevPage" :disabled="currentPage <= 1">이전</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button @click="nextPage" :disabled="currentPage >= totalPages">다음</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
import workerSrc from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc
const props = defineProps({
file: File
})
const canvasRef = ref(null)
const pdfDoc = ref(null)
const currentPage = ref(1)
const totalPages = ref(0)
const scale = ref(1.5)
// PDF 로드
async function loadPdf(file) {
const arrayBuffer = await file.arrayBuffer()
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer })
pdfDoc.value = await loadingTask.promise
totalPages.value = pdfDoc.value.numPages
await renderPage(1)
}
// 페이지 렌더링
async function renderPage(pageNum) {
if (!pdfDoc.value) return
const page = await pdfDoc.value.getPage(pageNum)
const viewport = page.getViewport({ scale: scale.value })
const canvas = canvasRef.value
const context = canvas.getContext('2d')
canvas.height = viewport.height
canvas.width = viewport.width
await page.render({
canvasContext: context,
viewport: viewport
}).promise
currentPage.value = pageNum
}
// 페이지 이동
function prevPage() {
if (currentPage.value > 1) {
renderPage(currentPage.value - 1)
}
}
function nextPage() {
if (currentPage.value < totalPages.value) {
renderPage(currentPage.value + 1)
}
}
// 파일 변경 감지
watch(() => props.file, (newFile) => {
if (newFile) loadPdf(newFile)
})
onMounted(() => {
if (props.file) loadPdf(props.file)
})
</script>
줌 기능
scale 값만 바꾸고 다시 렌더링하면 된다:
function zoomIn() {
scale.value = Math.min(scale.value + 0.25, 3)
renderPage(currentPage.value)
}
function zoomOut() {
scale.value = Math.max(scale.value - 0.25, 0.5)
renderPage(currentPage.value)
}
메모리 누수 주의
페이지 객체는 사용 후 정리해줘야 한다:
let currentPageObj = null
async function renderPage(pageNum) {
// 이전 페이지 정리
if (currentPageObj) {
currentPageObj.cleanup()
}
currentPageObj = await pdfDoc.value.getPage(pageNum)
// ... 렌더링
}
// 컴포넌트 언마운트 시
onUnmounted(() => {
if (currentPageObj) currentPageObj.cleanup()
if (pdfDoc.value) pdfDoc.value.destroy()
})
안 하면 큰 PDF 여러 번 열었다 닫았다 하면 메모리가 계속 쌓인다.
배포 시 경로 문제
Vite로 빌드하면 assets에 해시가 붙은 파일명이 생긴다:
/assets/pdf.worker.min-yatZIOMy.mjs
Hugo 같은 정적 사이트에서 서브 경로로 배포하면 경로가 안 맞을 수 있다:
// 잘못된 경로
"/assets/pdf.worker.min-yatZIOMy.mjs"
// 서브 경로 배포 시 필요한 경로
"/pdf2md/assets/pdf.worker.min-yatZIOMy.mjs"
vite.config.js에서 base 옵션 설정하거나, 빌드 후 경로를 수정해야 한다.
다음 글에서
PDF 위에 영역을 선택하는 기능을 구현한다. 드래그 대신 클릭-투-클릭 방식을 선택한 이유와 구현 방법을 다룬다.