
ホームページでPDFを設置しているサイトがありますが、ほとんどの場合、
ダウンロードして勝手に見てくれ的な設置方法になっています。
最近のインターネットブラウザであれば、PDFをブラウザの別タブで表示できたりもしますが、ホームページのモードやテイストなども切り替わってしまい、なんだかもどかしく感じます。
ていうか、そんなサイト使いづれ〜〜〜!と思うのは自分だけでしょうか?
以前からPDFjsの存在は知っていたんですが、nodejsじゃないと使えないな〜と勝手に思っていたんですが、全然簡単に使えるライブラリだったので、コレを利用して便利にPDFファイルをコントロールしてみようと思います。
注意:javascriptのpdfjsの機能説明やリファレンス解説をする内容ではないので、スニペット備忘録程度にソースコードを参照してください。
ホームページにおけるPDFファイルの扱い方について
今回PDFデータを便利に扱いたいと考えたのは、色々なサイトでPDFダウンロードするのが、本当にめんどくさいと思ったのと、先日から作っている
自炊用ブックリーダー(自炊にちょうどいいブックリーダーアプリを開発する話)でPDFをページごとに画像として扱う処理を便利にしたかったからです。
PDFファイルは、ページをレンダリングして使うだけじゃなく、中にテキストデータも持っていたり、ベクターデータを持っていたり、非常に高機能なファイルなのですが、今回は1ページ毎を画像として取り出してみたいと思います。
ソースコード
構成
root-directory/
├  index.htmnl
├  style.css
└  js/
    ├ main.js
    ├ build/
    └ cmaps/
PDFjsの取得
次のサイトから、ソースコードをダウンロードしてください。
https://mozilla.github.io/pdf.js/getting_started/#download
 Prebuilt (modern browsers)
Prebuilt (modern browsers)をダウンロードしておけば間違いないでしょう(恐らく)
ダウンロードしたファイルを解凍すると、buildというフォルダがあるので、それをjsフォルダにコピーしてください。
次に、webフォルダの中にあるcmapsというフォルダも同じ場所にコピーしておいてください。
index.html
<link rel="stylesheet" href="style.css"/>
<script type="module" src="js/main.js"></script>
<input type="file" name="pdf_upload"/>
<div class="image-area"></div>
style.css
.image-area{
  display:flex;
  gap:20px;
  flex-wrap:wrap;
  margin:20px;
  padding:10px;
  border:1px dotted black;
}
.image-area:empty{
  display:none;
}
.image-area > *{
  width:150px;
  height:150px;
  background-color:#eee;
}
.image-area img{
  width:100%;
  height:100%;
  object-fit:contain;
  box-shadow:4px 4px 10px rgba(0,0,0,0.5);
}
.image-area img:hover{
  width:300px;
  height:300px;
  box-shadow:10px 10px 20px rgba(0,0,0,0.5);
  position:relative;
  z-index:10;
  border:2px solid black;
  background-color:#eee;
}
main.js
import * as PdfJs  from "./build/pdf.js"
class Main{
  constructor(){
    PdfJs.GlobalWorkerOptions.workerSrc = `js/build/pdf.worker.js`;
    this.init()
  }
  get pdf_upload(){
    return document.querySelector(`input[type="file"][name="pdf_upload"]`)
  }
  get img_area(){
    return document.querySelector(`.image-area`)
  }
  // 初期設定
  init(){
    this.pdf_upload.addEventListener("change" , this.load.bind(this))
  }
  // 選択されたPDFの読み込み
  load(e){
    const pdf_file = e.target.files[0]
    const reader = new FileReader();
    reader.onload = this.loaded.bind(this)
    reader.readAsArrayBuffer(pdf_file)
  }
  // 読み込んだPDFをライブラリで読み取る処理
  loaded(e){
    const pdf_data = e.target.result
    PdfJs.getDocument({
      data       : pdf_data,
      cMapUrl    : `js/cmaps/`,
      cMapPacked : true,
    }).promise.then(this.view.bind(this))
  }
  // レンダリング表示
  async view(pdf){
    for(let i=1; i<=pdf.numPages; i++){
      const pdf_page = await pdf.getPage(i)
      const canvas   = await this.get_canvas(pdf_page)
      const imgData  = this.canvas2img(canvas)
      const img      = await this.create_image(imgData)
      const page     = document.createElement("div")
      page.className = "page"
      page.setAttribute("data-num" , i)
      this.img_area.appendChild(page)
      page.appendChild(img)
    }
  }
  // canvasエレメントの作成
  async get_canvas(page){
    const canvas   = document.createElement("canvas")
    const viewport = page.getViewport({ scale: 1 })
    canvas.width   = viewport.width
    canvas.height  = viewport.height
    const ctx = canvas.getContext('2d')
    const task = page.render({
      canvasContext: ctx,
      viewport: viewport,
    })
    await task.promise // レンダリング設定完了待ち
    return canvas
  }
  canvas2img(canvas){
    const base64 = canvas.toDataURL(Main.mime)
    const tmp    = base64.split(",")
    const bin    = atob(tmp[1])
    const mime   = tmp[0].split(':')[1].split(';')[0]
    const buf    = new Uint8Array(bin.length)
    for (let i=0; i<bin.length; i++) {
      buf[i] = bin.charCodeAt(i)
    }
    const blob   = new Blob([buf], {type: mime})
    return URL.createObjectURL(blob);
  }
  async create_image(data){
    return new Promise((resolve, reject) => {
      const img = new Image()
      img.src = data
      img.onload = (e => resolve(e.target))
      img.onerror = reject
    })
  }
}
switch(document.readyState){
  case "complete":
  case "interactive":
    new Main()
  break
  default:
    window.addEventListener("DOMContentLoaded" , (()=>new Main()))
}
CDNを使うパターン
上記フォルダを設置しなくても、下記のようにmain.jsに記述することで簡単に構築することもできます。
import * as PdfJs from 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/build/pdf.min.mjs'
class Main{
  constructor(){
    PdfJs.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/build/pdf.worker.mjs`;
    this.init()
  }
...
// 読み込んだPDFをライブラリで読み取る処理
loaded(e){
  const pdf_data = e.target.result
  PdfJs.getDocument({
    data       : pdf_data,
    cMapUrl    : `https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/cmaps/`,
    cMapPacked : true,
  }).promise.then(this.view.bind(this))
}
実行

httpプロトコルで立ち上げないとjsのモジュール読み込みエラーがでるので、dockerやsimple-webや、VScodeのGoLiveなどで実行してください。
テキトウなPDFを選択すると、画像の用に中のページを画像に変換して一覧表示するようにしています。
もちろん、中のページを1ページずつ画像として取り出すこともできるし、今回はやっていませんが、テキストを取得することもできます。
デモ
あとがき
HTML5でかなり便利になったFILE apiを便利に使いこなすことで、Webサイトの利便性も格段に向上する事が分かりますね。
今回は、ローカルにあるPDFをFileReaderを使ってデータ取得しましたが、サーバーに置いてあるPDFファイルも便利にajaxで読み込んで同じ用に分解して使うことも可能です。
公系のページや、家電メーカーなどの説明書などPDFをダウンロードさせるパターンがほとんどなようなので、できればこうしてWebコンテンツとして、便利にユーザーインターフェイスも整えた形で提供してもらいたいモノですね。
もし可能なら、PDF便利読み取りツールなどを自分で作るというのもアリかもしれませんね。
もし不便と感じているメーカーの方や、総務省関連の方などあれば、是非お問い合わせください。
 
0 件のコメント:
コメントを投稿