8
6

More than 1 year has passed since last update.

Nuxt.jsでvue-pdfを使ってPDFスライドを実装

Last updated at Posted at 2022-12-06

こちらの記事の個人開発サービスで、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

使うライブラリ

設定関連

  • README.md通りに実装してもうまくいかないところもあるが、基本はREADME通り。
  • Nuxt.jsにvue-pdfを導入するときに少しREADMEの設定を修正する必要がある

使用するAPI (コンポーネント)

exampleにあるように以下のような感じで、 用意されたコンポーネントにパラメーターやイベントを渡してあげて使用すると、コンポーネントがPDFスライドを実現してくれる

image.png

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がバージョン違いで入ってしまうので、揃ってなければ手動で調整が必要。

package.json
"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プレビューの表示方法

image.png

image.png

Nuxt.js用にプラグインを作って読み込ませる

Vue.jsにvue-pdfを導入する場合は特にこちらの対応不要だが、Nuxt.jsに導入する場合はプラグインを作らないと動かなかった。

plugins/vue-pdf.js
import Vue from 'vue'
import vuePdf from 'vue-pdf'

Vue.use(vuePdf)
Vue.component('vue-pdf', vuePdf)

  • nuxt.config.jsにも追記
nuxt.config.js
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [{ src: '~/plugins/vue-pdf.js', mode: 'client' }],

pdfのアップローダーまわり

完成形イメージ

image.png

https://mebee.info/2020/12/02/post-18031/
最低限度の実装です。元実装から必要な部分だけ抽出しているので、動くかはわからないですが、大体こんな感じです。

component/ImageUploader.vue
<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データをエンコードする
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をセットするとスライドできるコンポーネントを作成

完成形イメージ
image.png

最低限度の実装です。元実装から必要な部分だけ抽出しているので、動くかはわからないですが、大体こんな感じです。

components/DisplayPdf.vue
<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>


参考

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6