"BOKU"のITな日常

還暦越えの文系システムエンジニアの”BOKU”は新しいことが大好きです。

画像ファイルをドラッグ&ドロップで受け取り一覧表示する/Nuxt.js+Buefy

Nuxt.jsで画像ファイルをドラッグ&ドロップで受け取り、Base64に変換してから、IMGタグで表示する・・というのをやってみます。

ついでに、Fileオブジェクトの情報(名前。タイプ・サイズ)を加えて複数ファイルをうけつけた時に一覧表っぽく表示します。

f:id:arakan_no_boku:20190830002137j:plain

 

 

今回の実装でやりたいこと

 

以下のような機能の画面を実装します。

  • 画像ファイル(JPEGPNGのみ)をドラッグ&ドロップで受け取る。
  • 画像ファイルはファイル選択ダイアログでも受け取れるようにする。
  • ドラッグ&ドロップは複数の画像ファイルを受け取れるようにする。
  • ドロップされたFileオブジェクトを受け取る。
  • 受け取った画像ファイルを内部でBase64形式に変換する。
  • Base64に変換した画像ファイルを<img>タグで表示する。
  • 取得したFileの名前・タイプ・サイズの情報を受け取って表示する。

今回はそれぞれのポイントをソースで整理しながら、最後にソース全体をのせる感じでやります。

 

なお。

Nuxt.js(Vue.js)にUIフレームワーク buefy(cssはBulma)を使います。

インストール・環境構築はこちらの記事の通りにやってます。

arakan-pgm-ai.hatenablog.com

今回は個別機能のVueソースのみ作りますので、それを内部リンクで呼び出す部分とかは、以下のチュートリアルのindex.vueを参考に用意してください。

arakan-pgm-ai.hatenablog.com

さて・・。

 

ドラッグ&ドロップ関連機能

 

この3つに対応します。

  • 画像ファイル(JPEGPNGのみ)をドラッグ&ドロップで受け取る。
  • 画像ファイルはファイル選択ダイアログでも受け取れるようにする。
  • ドラッグ&ドロップは複数の画像ファイルを受け取れるようにする。

ドラッグ&ドロップの実装、まずはHTML(<templete>内)です。

<div class="card">
  <div class="card-content" @dragleave.prevent="checkDrag($event, 'new', false)" @dragover.prevent="checkDrag($event, 'new', true)" @drop.prevent="onDrop">
    <div class="drop is-primary">
      <p>
        {{ msg1 }}
      </p>
      <label for="file_selection">
        {{ msg2 }}
        <input id="file_selection" type="file" accept="image/*" style="display:none" @change="onDrop">
      </label>
    </div>
  </div> 
</div> 

ポイントです。

ドラッグ&ドロップを処理するということは、以下のイベントを処理することです。

  • dragleave:ドラッグエリアから離れた
  • dragover:ドラッグエリアの上に来た
  • drop:ドロップされた

その処理を定義した<Div>タグでくくったエリアがドラッグ&ドロップのエリアです。

ただ、普通に処理してしまうと、ドロップした画像とかをブラウザが表示してしまうので、それを抑制するために、「drop.prevent」のように「.prevent」をつけて、その動きを抑制します。

それぞれのイベント対して、以下のスクリプトを実行するように定義してます。

  • @dragleave.prevent="checkDrag($event, 'new', false)"
  • @dragover.prevent="checkDrag($event, 'new', true)"
  • @drop.prevent="onDrop"

 それぞれの処理は<script>~</script>部分で定義します。

 まず、ドラッグの処理です。

    checkDrag (event, key, status) {
      this.isDrag = status ? key : null
      if (status) {
        this.msg1 = 'ドラッグ中'
        this.msg2 = 'ここにドロップしてください'
      } else {
        this.msg1 = 'ここにファイルをドロップ。または'
        this.msg2 = 'ここをクリックして選択'
      }
    },

ドラッグの処理は、ソースをシンプルにして後で見直した時にわかりやすいように、文字で状態を表示するようにしました。 

結局、ドラッグゾーンの上にくれば「status」がTrueなので、「ドラッグ中」と表示。

ドラッグゾーンから離れれば「status」がFalseなので「ここにファイルをドロップ」の案内に戻るというだけのことをやってます。

ドロップした時の「onDrop」の処理は、ちょっと他の要素が沢山あるので後で説明するとして「画像ファイルはファイル選択ダイアログでも受け取れるようにする」について、整理しときます。

それをやっているのは、以下の部分です。

      <label for="file_selection">
        {{ msg2 }}
        <input id="file_selection" type="file" accept="image/*" style="display:none" @change="onDrop">
      </label>

つまり、 <input type="file"・・を使っているだけです。

ただ、それを単純に表示するとイマイチカッコ悪いので、dhisplay:noneにして隠して、かわりに<label>でくくって「ここをクリックして選択」の文字を表示しています。

なので、その文字の上を表示するとファイル選択ダイアログが開きます。

これでとりあえず、以下のようなドラッグエリアが表示されます。

f:id:arakan_no_boku:20190907203355p:plain

そして、ファイルをドラッグしてエリアの上にもってくると。

f:id:arakan_no_boku:20190907203630p:plain

こうなります。

さて、今度は「drop」された後の、諸々の処理です。

 

ドロップされたFileオブジェクトを受け取る。

 

エリアにドロップされたFileオブジェクトを受け取る。

もしくは、ファイル選択ダイアログで選択されたFileオブジェクトを受け取る。

この両方をこの部分でやります。

    const fileList = event.target.files
        ? event.target.files
        : event.dataTransfer.files

ドロップ されたら「event.target.files」にはいってきて、ファイル選択ダイアログで選ばれたら「event.dataTransfer.files」にはいってくるわけです。

それを「event.target.files」がnullでなければ「event.target.files」、でなければ「event.dataTransfer.files」を「fileList」にセットします。

 

受け取った画像ファイルを内部でBase64形式に変換する。

 

fileListにセットされたfileオブジェクトを処理するので、ここには複数のfileがセットされている可能性があります。

なので、ループで処理しているのですが、とりあえず、ひとつのfileを処理する方法を整理しておきます。

const reader = new FileReader()
reader.onload = () => {
 this.images.push(reader.result)
}
reader.readAsDataURL(fileList[i])

ここがちょっと難しいです。 

readAsDataURL(file)が、fileオブジェクトを引数にとって、Base64形式に変換してくれるメソッドです。

developer.mozilla.org

このメソッドは非同期に実行されるので、単純に戻り値で結果を受け取ることができなくて、読込処理が終了すると loadend イベントが生じて、同時に result プロパティにファイルのデータをBase64エンコードしたデータがセットされます。

なので、「reader.onload = () => {」のようにonloadイベントハンドラを定義して、その中でreader.resultにセットされた結果を受け取ってます。

ここが最初わからなくて苦労しました。(笑)

なお。

引数が「fileList[i]」になっていたり、受け取るのが「this.images.push」になっているのは複数ファイルを処理する前提でループの中の一部を切り取っているからです。

 

Base64に変換した画像ファイルを<img>タグで表示する。

 

Base64形式のデータを表示する方法はこうです。

 <img src="data:image/png;base64,xxxxx..." />

このxxxxxの部分はBase64形式にエンコードされたデータです。

つまり。

Based64形式にエンコードされたデータの前に 「data:image/png;base64,」とか「data:image/jpeg;base64,」とか「data:*/*;base64, 」とかをつけて、srcに渡せばよいということです。

でも。

readAsDataURL(file)がresultにセットした結果には、既に「data:*/*;base64, 」がセットされていますので、単純にそれを<imgタグに渡せば画像が表示されます。

今回はこんな感じでやってます。

<figure class="image is-square">
  <img :src="images[index]">
</figure>  

imagesが複数ファイルのdataがセットされたListの想定なので、上記のような渡し方になってますが「iamgees[index]」にはエンコードされたBase64データがはいってると思えば、実にシンプルなので迷うところはないです。

ただ、srcが「v-bind」の短縮形の「:src」になっている点には注意が必要です。

 

取得したFileの名前・タイプ・サイズの情報を受け取って表示する 

 

ポイントの最後です。

fileオブジェクトに持っている情報の参照の仕方です。

<div v-for="(file, index) in files" :key="index">
   <div class="columns">
      <div class="column">
         <figure class="image is-square">
           <img :src="images[index]">
         </figure>
      </div>
      <div class="column">
         <p class="has-text-weight-semibold is-size-4 has-text-centerd">
            {{ file.name }}
         </p>
      </div>
      <div class="column">
         <p class="has-text-weight-semibold is-size-4 has-text-centerd">
            {{ file.type }}
         </p>
      </div>
      <div class="column">
         <p class="has-text-weight-semibold is-size-4 has-text-centerd">
            {{ file.size.toLocaleString() }} Byte
         </p>
      </div>
   </div>
</div>

filesは複数のfileオブジェクトがはいったListです。

なので、v-forでループを回して「file」オブジェクトを取り出します。

あとは。

  • file.name でファイル名
  • file.type で「image/jpeg」のようなタイプ名
  • file.size でファイルサイズ(バイト)

を取得して表示すればよい・・とまあ、こんな感じです。

 

最後にソース全体と動作イメージです

 

動作イメージとしては。

画面にこんな感じでドラッグエリアが表示されて

f:id:arakan_no_boku:20190907203355p:plain

そして、ファイルをドラッグしてエリアの上にもってくると、こうなって。

f:id:arakan_no_boku:20190907203630p:plain

ドロップするとこうなります。

f:id:arakan_no_boku:20190907223515p:plain

これを行うソース全文はこちら。

image_predict.vue

<template>
  <div class="container content has-text-centered">
    <div class="columns">
      <div class="column" />
      <div class="column">
        <NLink to="/">
          <p>ルートのページへ戻ります</p>
        </NLink>
      </div>
      <div class="column" />
    </div>
    <div class="columns">
      <div class="column" />
      <div class="column is-harf">
        <div class="card">
          <div class="card-content" @dragleave.prevent="checkDrag($event, 'new', false)" @dragover.prevent="checkDrag($event, 'new', true)" @drop.prevent="onDrop">
            <div class="drop is-primary">
              <p>
                {{ msg1 }}
              </p>
              <label for="file_selection">
                {{ msg2 }}
                <input id="file_selection" type="file" accept="image/*" style="display:none" @change="onDrop">
              </label>
            </div>
          </div>
        </div>
      </div>
      <div class="column" />
    </div>
    <div v-if="files != null">
      <div v-for="(file, index) in files" :key="index">
        <div class="columns">
          <div class="column">
            <figure class="image is-square">
              <img :src="images[index]">
            </figure>
          </div>
          <div class="column">
            <p class="has-text-weight-semibold is-size-4 has-text-centerd">
              {{ file.name }}
            </p>
          </div>
          <div class="column">
            <p class="has-text-weight-semibold is-size-4 has-text-centerd">
              {{ file.type }}
            </p>
          </div>
          <div class="column">
            <p class="has-text-weight-semibold is-size-4 has-text-centerd">
              {{ file.size.toLocaleString() }} Byte
            </p>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  data () {
    return {
      isDrag: null,
      msg1: 'ここにファイルをドロップ。または',
      msg2: 'ここをクリックして選択',
      files: [],
      images: []
    }
  },
  methods: {
    checkDrag (event, key, status) {
      this.isDrag = status ? key : null
      if (status) {
        this.msg1 = 'ドラッグ中'
        this.msg2 = 'ここにドロップしてください'
      } else {
        this.msg1 = 'ここにファイルをドロップ。または'
        this.msg2 = 'ここをクリックして選択'
      }
    },
    onDrop (event) {
      this.isDrag = null
      const fileList = event.target.files
        ? event.target.files
        : event.dataTransfer.files
      for (let i = 0; i < fileList.length; i++) {
        if (fileList[i].type === 'image/jpeg' || fileList[i].type === 'image/png') {
          const reader = new FileReader()
          reader.onload = () => {
             this.images.push(reader.result)
          }
          reader.readAsDataURL(fileList[i])
        } else {
          this.images.push('/jamap.JPG')
        }
        this.files.push(fileList[i])
      }
      this.msg1 = 'ドロップされました'
      this.msg2 = 'ファイル数は' + fileList.length + 'です。'
    }
  }
}
</script>

   

ちょっと長いのですが、ポイントとして上に説明したものが組み合わさっているだけなので、読めばわかる・・と思います。

とりあえず、これでドラッグ&ドロップで画像ファイルを受け取って画像を表示したりする処理はできました。

今回はこんなところで。

ではでは。