🌵

Vue.jsで画像を作成しRailsのActive Storageで保存する

2021/10/15に公開

Railsアプリケーション内でVue.jsを使って画像を作成するアプリを制作していて色々と詰まった部分があったので記録に残しておこうと思います。
ざっと流れを説明しておくと以下のようになります。

  1. Vue.js + Konva.jsを使ってcanvasタグで図形を作成
  2. Base64形式のData URLに変換
  3. RailsのActive Storageで保存

開発環境

環境構築などの部分は省略します

VueとKonvaで画像を作成しData URLに変換、画像表示させる

今回はChartモデルの_form.html.erb内にVueを描画します

views/charts/_form.html.erb
  <div id="js-chart"></div>
javascript/chart.js
import { createApp } from 'vue'
import App from './chart.vue'

document.addEventListener('DOMContentLoaded', () => {
  const selector = '#js-chart';
  if(document.querySelector(selector)){
    createApp(App).mount(selector);
  }
})
chart.vue
<template>
  <div id="chart"></div>
~ 中略 ~
  <img id="img">
</template>

公式にサンプルコード付きで様々なパターンの図形の描画方法が載っているので割愛します。

chart.vue
// data関数でstageとimageUrlを定義しておく
data() {
  return {
    stage: '',
    imageUrl: ''
  }
}
chart.vue
this.stage = new Konva.Stage({
  width: 500,
  height: 800,
  container: 'chart' // template内の<div id="chart"></div>のid名を指定して紐付ける
});

土台になるstageインスタンスを生成しこれにレイヤーや図形のインスタンスを追加していくことで1つのcanvasさまざまな図形を組み合わせて画像を作ることができます。

例)

  • stage
    • background_layer
      • base_line
    • main_layer
      • circle1
      • circle2
chart.vue
// toDataURLで 'data:image/png;base64' で始まるData URLを返す
this.imageUrl = this.stage.toDataURL 

// template内の<img id="img">に作成した画像を表示させる
const img = document.getElementById("img")
img.src = this.imageUrl

作成した画像のData URLをRailsで受け取れるようにする

Vue側のhidden inputにData URLの値をバインドしRailsのform_withで生成したフォームと紐付ける

views/charts/_form.html.erb
+ <%= form_with model: chart, multipart: true, id: 'form' do %>
    <div id="js-chart"></div>
+ <% end %>
chart.vue
+ <input type="hidden" name="chart[image]" id="chart_image" :value="imageUrl">

+ <a @click="save">Save</a>

~中略~

methods: {
+   save() {
+    const promise = new Promise(function(resolve) {
      this.imageUrl = this.stage.toDataURL 
+      resolve()
+    })
+    function onFulfilled() {
+      const form = document.getElementById('form')
+      form.submit()
+    }
+    promise.then(onFulfilled)
+  }
}

これでSaveボタンを押下することで画像のData URLをparamsで受け取れるようになりました

受け取ったData URLからActive Storageへ保存する

受け取ったData URLをゴニョゴニョやる必要があるので見通しがよくなるようクラスに切り分けました

image_blob.rb
class ImageBlob
  attr_reader :image_data_url

  def initialize(image_data_url)
    @image_data_url = image_data_url
  end

  def mime_type
    image_data_url[%r/(image\/[a-z]{3,4})/]
  end

  def to_io
    StringIO.new(decoded_content)
  end

  private

  def decoded_content
    Base64.decode64(content)
  end

  def content
    image_data_url.sub(%r/data:image\/.{3,},/, '')
  end
end
chart.rb
class Chart < ApplicationRecord
  has_one_attached :image

  def attach_blob(image_data_url)
    image_blob = ImageBlob.new(image_data_url)
    image.attach(
      io: image_blob.to_io,
      filename: Time.zone.now,
      content_type: image_blob.mime_type #content_typeは無くてもOK
    )
  end
end
charts_controller.rb
class ChartsController < ApplicationController

  def create
    @chart = Chart.new(chart_params)
    @chart.attach_blob(image_data_url)

    if @chart.save
      redirect_to @chart, notice: 'Saved!'
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def image_data_url
    params.require(:chart).permit(:image)[:image]
  end
end

以上になります。

色々調べてみたんですが、<input type="file">でアップロードする方法の記事がほとんどで参考にできるものが少なく苦労しましたがなんとか実装できました。
おそらく作成した画像を直接Active Storageで保存するユースケースは少ないと思いますが誰かの役に立てば幸いです。

リポジトリ:
obregonia1/visible_scratch_skillz

参考:
Active Storage の概要 - Railsガイド
base64でエンコードされた画像をActive Storageで保存する - Qiita

Discussion