LoginSignup
3

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>


参考

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
What you can do with signing up
3