こちらの記事の個人開発サービスで、PDFスライド機能を実装しました。
個人開発第一弾の記事を書いていましたが、続編で技術記事を書くと言っていたので、書きました。
作りたい機能の要件
- PDFファイルをセットしてプレビュー表示できる
- プレビューページをキーボードの矢印キー等でスライド形式でswipeできる
- 画面に表示している←→ボタンをクリックしてもページをswipeできる
- PDFファイルはストレージに保存せず、セットするだけでswipe可能にしたい(ストレージに保存されないことでセキュリティーリスクをなくす&工数削減)
- PDFプレビューは全画面表示もできる(この記事では触れない)
バージョン
- yarn v1.22.19
- nuxt v2.15.8
- Composition API v1.6.3
- Vue v2.6.14
- vuetify v2.6.1
使うライブラリ
- 今回はvue-pdfというライブラリを使用
- "vue-pdf": "4.2.0"を使用
- https://github.com/FranckFreiburger/vue-pdf
設定関連
- README.md通りに実装してもうまくいかないところもあるが、基本はREADME通り。
- Nuxt.jsにvue-pdfを導入するときに少しREADMEの設定を修正する必要がある
使用するAPI (コンポーネント)
exampleにあるように以下のような感じで、 用意されたコンポーネントにパラメーターやイベントを渡してあげて使用すると、コンポーネントがPDFスライドを実現してくれる
vue-pdfが用意しているコンポーネントに渡すprops
https://github.com/FranckFreiburger/vue-pdf#props
今回使ったものは以下。
- :src
- PDFデータのURL以外にもオブジェクトも渡すことができる(今回はオブジェクトを渡すことでPDFをストレージに保存したりしなくて済むようにした)-
- page
- 表示しているページ番号
ライブラリを使用することで使えるevent
https://github.com/FranckFreiburger/vue-pdf#events
上記を参考に。
イベントの受け取りはREADMEの通り、$event
変数を使って受け取る。
https://github.com/FranckFreiburger/vue-pdf#examples
- 今回使ったイベント
- @num-pages: 指定された pdf のすべてのページの合計。PDFをセットした時に総枚数が何枚なのか表示するのに使った
その他はREADMEをご参考に
vue-pdfをinstall
$ yarn add vue-pdf
依存関係としては以下だが、注意点として、Nuxt.jsで使う場合は、バージョンを以下で揃えておかないと動かなかった。普通にyarnでinstallすると、pdfjs-dist
がバージョン違いで入ってしまうので、揃ってなければ手動で調整が必要。
"dependencies": {
~~省略~~,
"pdfjs-dist": "2.5.207",
"vue-pdf": "4.2.0",
~~省略~~,
}
srcオプションにpdfのデータをセット
上述で紹介したvue-pdfが用意しているコンポーネントのsrc引数に対して、PDFデータを渡していく
- srcに渡せるもの(検証したもの)
- PDFファイルを静的なデータとしてディレクトリ配下に置いたものを呼び出す
<pdf src="/slide.pdf"></pdf>
- 外部のストレージなどに保存しているデータを通信して表示する
<pdf src="https://cdn.mozilla.net/pdfjs/tracemonkey.pdf"></pdf>
- base64などでencodeしたPDFデータのオブジェクトを指定(今回はこれを採用)
<pdf src='data:application/pdf;base64,JVBERi0xLjUKJbXtrvsKMyAwIG9iago8PCAvTGVuZ3RoIDQgMCBSCiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCj4+CnN0cmVhbQp4nE2NuwoCQQxF+/mK+wMbk5lkHl+wIFislmIhPhYEi10Lf9/MVgZCAufmZAkMppJ6+ZLUuFWsM3ZXxvzpFNaMYjEriqpCtbZSBOsDzw0zjqPHZYtTrEmz4eto7/0K54t7GfegOGCBbBdDH3+y2zsMsVERc9SoRkXORqKGJupS6/9OmMIUfgypJL4KZW5kc3RyZWFtCmVuZG9iago0IDAgb2JqCiAgIDEzOAplbmRvYmoKMiAwIG9iago8PAogICAvRXh0R1N0YXRlIDw8CiAgICAgIC9hMCA8PCAvQ0EgMC42MTE5ODcgL2NhIDAuNjExOTg3ID4+CiAgICAgIC9hMSA8PCAvQ0EgMSAvY2EgMSA+PgogICA+Pgo+PgplbmRvYmoKNSAwIG9iago8PCAvVHlwZSAvUGFnZQogICAvUGFyZW50IDEgMCBSCiAgIC9NZWRpYUJveCBbIDAgMCA1OTUuMjc1NTc0IDg0MS44ODk3NzEgXQogICAvQ29udGVudHMgMyAwIFIKICAgL0dyb3VwIDw8CiAgICAgIC9UeXBlIC9Hcm91cAogICAgICAvUyAvVHJhbnNwYXJlbmN5CiAgICAgIC9DUyAvRGV2aWNlUkdCCiAgID4+CiAgIC9SZXNvdXJjZXMgMiAwIFIKPj4KZW5kb2JqCjEgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzCiAgIC9LaWRzIFsgNSAwIFIgXQogICAvQ291bnQgMQo+PgplbmRvYmoKNiAwIG9iago8PCAvQ3JlYXRvciAoY2Fpcm8gMS4xMS4yIChodHRwOi8vY2Fpcm9ncmFwaGljcy5vcmcpKQogICAvUHJvZHVjZXIgKGNhaXJvIDEuMTEuMiAoaHR0cDovL2NhaXJvZ3JhcGhpY3Mub3JnKSkKPj4KZW5kb2JqCjcgMCBvYmoKPDwgL1R5cGUgL0NhdGFsb2cKICAgL1BhZ2VzIDEgMCBSCj4+CmVuZG9iagp4cmVmCjAgOAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDA1ODAgMDAwMDAgbiAKMDAwMDAwMDI1MiAwMDAwMCBuIAowMDAwMDAwMDE1IDAwMDAwIG4gCjAwMDAwMDAyMzAgMDAwMDAgbiAKMDAwMDAwMDM2NiAwMDAwMCBuIAowMDAwMDAwNjQ1IDAwMDAwIG4gCjAwMDAwMDA3NzIgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA4CiAgIC9Sb290IDcgMCBSCiAgIC9JbmZvIDYgMCBSCj4+CnN0YXJ0eHJlZgo4MjQKJSVFT0YK'></pdf>
PDFプレビューの表示方法
- 一枚ずつ見ていくタイプ
今回はこれで実装。
このexampleと同じ形式
https://github.com/FranckFreiburger/vue-pdf#example---current-page--page-count
- 分割してPDFプレビューを見たいタイプ
v-forを使ってコンポーネントをページ分だけ作るようにする方法
https://github.com/FranckFreiburger/vue-pdf#example---display-multiple-pages-of-the-same-pdf-document
Nuxt.js用にプラグインを作って読み込ませる
Vue.jsにvue-pdfを導入する場合は特にこちらの対応不要だが、Nuxt.jsに導入する場合はプラグインを作らないと動かなかった。
-
プラグイン作り方の参考: https://develop365.gitlab.io/nuxtjs-2.8.X-doc/ja/guide/plugins/
-
以下のファイルを作成
import Vue from 'vue'
import vuePdf from 'vue-pdf'
Vue.use(vuePdf)
Vue.component('vue-pdf', vuePdf)
- nuxt.config.jsにも追記
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [{ src: '~/plugins/vue-pdf.js', mode: 'client' }],
pdfのアップローダーまわり
完成形イメージ
https://mebee.info/2020/12/02/post-18031/
最低限度の実装です。元実装から必要な部分だけ抽出しているので、動くかはわからないですが、大体こんな感じです。
<template>
<div>
<!-- ドラッグ&ドロップ領域start -->
<v-row
@click="openImageSelectDialog"
@drop.prevent="handleDropImage"
@dragover.prevent="handleDragEvent($event, true)"
@dragleave.prevent="handleDragEvent($event, false)"
>
<v-col style="margin-top: 165px">
<div class="text-center mb-12">
<img src="/svg/uploader_icon.svg" alt="SwipeLeft" />
</div>
<div class="text-center">ここにスライドをドロップしてください</div>
</v-col>
</v-row>
<!-- ドラッグ&ドロップ領域end -->
<!-- スライドアップロード(ボタンをクリックした時のイベントでpdfのアップローダが開くようにしている)start -->
<v-row justify="center" align="center" no-gutters>
<v-btn
color="primary"
class="textWhite--text text-h4 elevation-0"
width="420px"
height="61px"
@click="openImageSelectDialog"
>
スライドをアップロードする
</v-btn>
</v-row>
<input
ref="uploadedImage"
class="d-none"
type="file"
accept=".pdf"
@change="handleChangeImage"
/>
<!-- スライドアップロードend -->
</div>
</template>
<script>
import { defineComponent, ref, useStore } from '@nuxtjs/composition-api'
export default defineComponent({
setup() {
const store = useStore()
const isDragging = ref(false)
const uploadedImage = ref(null)
// アップローダーを開く
const openImageSelectDialog = () => {
uploadedImage.value.click() // inputタグ要素をクリックする
}
// データをドラッグ&ドロップしたときにイベントを拾いfileデータを整形
const handleDropImage = (event) => {
isDragging.value = false
const file = extractFile(event)
if (!file) {
return
}
changeImage(file)
}
// ドラッグ時に領域の色を変えるためにドラッグ時のステータスをリアクティブな形で更新
const handleDragEvent = (event, dragging) => {
isDragging.value = dragging && event.dataTransfer?.types[0] === 'Files'
}
// ファイルデータを整形
const handleChangeImage = (event) => {
const file = extractFile(event)
if (!file) {
return
}
changeImage(file)
}
const extractFile = (event) => {
const fileList = event.target?.files || event.dataTransfer?.files
if (!fileList) {
return null
}
return fileList[0] || null
}
// pdfデータをbase64でエンコード
const changeImage = (file) => {
const reader = new FileReader()
reader.onload = (event) => {
const result = event.target?.result
if (typeof result !== 'string') {
return
}
store.dispatch('presentation/readyState', result)
}
reader.readAsDataURL(file)
}
return {
openImageSelectDialog,
handleDropImage,
handleDragEvent,
handleChangeImage,
isDragging,
uploadedImage,
}
},
})
</script>
コンポーネントに渡すpdfオブジェクトをbase64でencodeして作成
上記の部分の中で、pdfのbase64encodeなどの箇所を抜粋
- inputタグでセットされたpdfデータを受け取る
<input
ref="uploadedImage"
class="d-none"
type="file"
accept=".pdf"
@change="handleChangeImage"
/>
- onChangeイベントをトリガーに関数を実行してfileデータをエンコードする
- FileReader.readAsDataURL()メソッドを使う
const handleChangeImage = (event) => {
const file = extractFile(event)
if (!file) {
return
}
changeImage(file)
}
const extractFile = (event) => {
// ドラッグ&ドロップイベントからきたデータはdataTransferで取り出す
const fileList = event.target?.files || event.dataTransfer?.files
if (!fileList) {
return null
}
return fileList[0] || null
}
const changeImage = (file) => {
const reader = new FileReader()
// ↓pdfデータの読み込みが完了したら作動してオブジェクトを取り出す
reader.onload = (event) => {
const result = event.target?.result
if (typeof result !== 'string') {
return
}
store.dispatch('presentation/readyState', result) // 返却されたオブジェクトをstoreに格納しvue-pdfコンポーネントのsrcに渡す
}
reader.readAsDataURL(file) // inputタグでの読み込みpdfデータをセットしたら作動
}
pdfをセットするとスライドできるコンポーネントを作成
最低限度の実装です。元実装から必要な部分だけ抽出しているので、動くかはわからないですが、大体こんな感じです。
<template>
<client-only>
<v-row>
<!-- vue-pdfのコンポーネントstart -->
<vue-pdf
:src="pdfUrl"
:page="currentPageNum"
@num-pages="totalPageNum = $event"
/>
<!-- vue-pdfのコンポーネントend -->
</v-row>
<!-- 矢印キーでのページスワイプ等start -->
<v-row justify="center" align="center" class="mb-8 mt-12">
<div class="mr-8">
<v-btn icon @click="jumpPage(1)">
<img src="/svg/swipe_ahead.svg" alt="swipe_ahead" />
</v-btn>
</div>
<div class="mr-7">
<v-btn icon @click="jumpPage(currentPageNum - 1)">
<img src="/svg/swipe_left.svg" alt="swipe_left" />
</v-btn>
</div>
<div class="mr-7">{{ currentPageNum }} / {{ totalPageNum }}</div>
<div class="mr-8">
<v-btn icon @click="jumpPage(currentPageNum + 1)">
<img src="/svg/swipe_right.svg" alt="swipe_right" />
</v-btn>
</div>
<div>
<v-btn icon @click="jumpPage(totalPageNum)">
<img src="/svg/swipe_end.svg" alt="swipe_end" />
</v-btn>
</div>
</v-row>
<!-- 矢印キーでのページスワイプ等end -->
</client-only>
</template>
<script>
import {
defineComponent,
ref,
onBeforeUnmount,
onBeforeMount,
} from '@nuxtjs/composition-api'
export default defineComponent({
props: {
// propsでpdfのオブジェクトを受け取る
pdfUrl: {
type: String,
default: '',
},
},
setup(_) {
// リアクティブなデータをセット
const currentPageNum = ref(1)
const totalPageNum = ref(0)
// ページのswipe
const jumpPage = (toPage) => {
if (toPage < 1 || toPage > totalPageNum.value) {
return
}
currentPageNum.value = toPage
}
// 左右矢印キーを押下時のアクション
const keyAction = (e) => {
if (e.keyCode === 37) {
jumpPage(currentPageNum.value - 1)
} else if (e.keyCode === 39) {
jumpPage(currentPageNum.value + 1)
}
}
// マウント時にイベントを仕込んでおく
onBeforeMount(() => {
// keydownのイベント
window.addEventListener('keydown', keyAction)
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', keyAction)
})
return {
jumpPage,
currentPageNum,
totalPageNum,
}
},
})
</script>