Canvas-渲染 PDF 的方法

講白話一點就是把 PDF 變成 Cavas。

環境配置

由於我當初在研究時踩到了一些地雷,所以還是說明一下環境配置的方式。這邊要安裝的套件是 pdf.js。不過因為是用 React 來實作,所以會用 npm 來安裝 pdfjs-dist 這個套件。

裝好套件後,必須再透過 GlobalWorkerOptions 來做配置:

1
2
3
4
5
import { GlobalWorkerOptions } from 'pdfjs-dist'
import pdf from './assets/sample.pdf'

const src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@<your_pack_version>/build/pdf.worker.js'
GlobalWorkerOptions.workerSrc = src
  • 請務必把 <your_pack_version> 填入和 pdfjs-dist 相同的版號
  • 請務必把 <your_pack_version> 填入和 pdfjs-dist 相同的版號
  • 請務必把 <your_pack_version> 填入和 pdfjs-dist 相同的版號

如果沒有做這段配置或是你填錯版號,等一下在使用的時候就會出錯,所以請務必先做好這段配置。

讀取本機檔案

這段主要是介紹怎麼用套件提供的 methods 來讀取和渲染,要特別注意的是這段期間是「非同步」執行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 引入專案中的 pdf 檔
import pdf from './assets/sample.pdf'

function App() {
GlobalWorkerOptions.workerSrc = workerSrc
const canvas = useRef<HTMLCanvasElement>(null)

useEffect(() => {
renderPDF(pdf, 1).catch((error) => {
console.log('error', error)
})
}, [])

// 讀取檔案的 function
async function renderPDF(src: string, page: number) {
const ctx = canvas.current!.getContext('2d')!
// 利用 getDocument 來讀取檔案
const doc = await getDocument(src).promise
// 拿到檔案後,利用 getPage 來取得頁面資訊
doc.getPage(page).then((page) => {
// 設定縮放比例
const viewport = page.getViewport({ scale: 1 })
// 設定 cavas 的寬高
canvas.current!.width = viewport.width
canvas.current!.height = viewport.height
// 渲染到 canvas 中
page.render({
canvasContext: ctx,
viewport
})
})
}

return (
<div className='App'>
<div className='canvas-wrapper'>
<canvas ref={canvas} id='canvas'></canvas>
</div>

<div className='btns'>
<button
onClick={() => {
// 切到第一頁
renderPDF(pdf, 1).catch((error) => {
console.log('error', error)
})
}}
>
Prev
</button>
<button
onClick={() => {
// 切到第二頁
renderPDF(pdf, 2).catch((error) => {
console.log('error', error)
})
}}
>
Next
</button>
</div>
</div>
)
}

example1-read-pdf-from-local

實際範例可以到 codesandbox 上參考,不過我實在不知道為什麼 codesandbox 上沒辦法讀取到 pdfjs 的包…,所以在範例中是透過 cdn 的方式來引入的,這一點還請見諒。

讀取使用者上傳的檔案

在多數情況下,我們想做的通常是讓使用者自己上傳檔案並顯示到畫面上。其實主要的邏輯都跟剛剛差不多,就是把檔案內容丟給 pdfjs 來處理並且渲染到 canvas 上,但現在要多做的一件事情是:

先把使用者上傳的檔案做格式轉換後,再丟給 pdfjs 來處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function App() {
const canvas = useRef<HTMLCanvasElement>(null)

// 讀取檔案的 function
async function renderPDF(src: ArrayBuffer, pageIndex: number) {
const ctx = canvas.current!.getContext('2d')!
// 利用 getDocument 來讀取檔案
const doc = await getDocument(src).promise
// 拿到檔案後,利用 getPage 來取得頁面資訊
doc.getPage(pageIndex).then((page) => {
// 設定縮放比例
const viewport = page.getViewport({ scale: 1 })
// 設定 cavas 的寬高
canvas.current!.width = viewport.width
canvas.current!.height = viewport.height
// 渲染到 canvas 中
const renderTask = page.render({
canvasContext: ctx,
viewport
})

renderTask.promise.catch(() => alert('render error'))
})
}

function onFileSelect(event: React.ChangeEvent<HTMLInputElement>) {
const files = event.target.files!
// 確認有選擇檔案才做事
if (files.length > 0) {
// new 一個 FileReader
const fileReader = new FileReader()
// 用 FileReader 來讀取 blob
fileReader.readAsArrayBuffer(files[0])
fileReader.onload = () => {
// 下面這兩段會在文中解釋,這邊先看個概念就好了
// new 一個 Uint8Array 的 視圖(view)出來
const typedArray = new Uint8Array(fileReader.result as ArrayBuffer)
// 把 Uint8Array 丟給 pdfjs 來處理,並渲染到 canvas 上
renderPDF(typedArray, 1).catch((error) => console.log(error.message))
}
}
}

return (
<div className='App'>
<div className='btns'>
<input type='file' onChange={onFileSelect} />
</div>
<div className='canvas-wrapper'>
<canvas ref={canvas} id='canvas'></canvas>
</div>
</div>
)
}

example2-read-pdf-from-upload

附註:codesandbox

關於讀檔案的部分其實會牽扯到蠻多東西的,所以我不打算解釋太深,你只要知道每一步大概在做什麼就好了:

  1. 使用者上傳檔案後,用 event.target.files 拿到的會是「blob」格式的物件,我們不能直接把這個丟給 pdfjs 來解析,所以要先做一些處理。
  2. 利用 FileReader.readAsArrayBuffer 來讀取 blob 後,會拿到一個「ArrayBuffer」的資料。ArrayBuffer 是一種比較特別的物件,他的內容會指向某個記憶體位址(類似 C 的 Pointer),但重點是只能讀不能寫,所以必須在透過一個叫做「View」的一種資料型態來操作 ArrayBuffer(這一段真的比較抽象,所以可能要多在自己參考其他文章來想想看,像 這篇 我覺得解釋得還蠻好理解的)。
  3. 為了透過 View 來操作 ArrayBuffer,必須用 new Uint8Array(fileReader.result as ArrayBuffer) 來產生一個 View,而 View 在 JavaScript 中是屬於一種叫做「TypedArray」的資料型態,所以才會以此來當作變數名稱。
  4. 最後把這個「可操作的資料」丟給 pdfjs 後,就可以順利的讀取並渲染到 canvas 上。

雖然以這邊的例子來說,就算你直接把 ArrayBuffer 丟給 pdfjs 也可以正常渲染,但官方建議透過這種方式來減少「memory leak」 的問題。

Canvas-利用 fabric 打造更好的 Canvas Canvas-基本繪製與輸出圖片
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×