画面上のDOM要素を画像に変換してサーバーに保存する 【Rails6】
この記事はフィヨルドブートキャンプ Part 1 Advent Calendar 2021の記事です。
フィヨルドブートキャンプ Part 1 Advent Calendar 2021 - Adventar
フィヨルドブートキャンプ Part 2 Advent Calendar 2021 - Adventar
昨日の19日目は
- obregonia1 さんのghqとpecoでリポジトリの管理を便利にする
- sanfrecce_osaka さんの レビューするときにやっていること・気を付けていること
でした。
はじめに
わたしは本日フィヨルドブートキャンプの課題として個人開発した TwiCode というサービスをリリースしました。
サービスの概要や作った背景、苦労したことや工夫したことはリリースブログに書いたので、今回はサービスの肝となる実装について技術的な話をしたいと思います。
TwiCodeはテキストで入力したソースコードをシンタックスハイライトを適用したものを画像に変換し、その画像をサーバー側で保存しています。その画像はURLをツイートすることでTwitterカードに画像を表示することができます。
画像にして画面上に表示するだけであればJavaScriptのみで実装できますが、OGPに表示するには画像にするだけでなくサーバーに保存する必要があります。
DOM要素を画像化してサーバーに保存する処理は他に参考にできる記事がなく苦労したので、簡単ではありますが紹介していきます。
処理のおおまかな流れです
-
- パラメーターで受け取ったデータURLをActiveStorageに保存する
開発環境・使用ライブラリ
今回はhtml2canvas というライブラリを使ってDOMの要素を画像(canvas)変換していきます。 html2canvasを使うと、下記のサンプルのボタンを押すとHello world!の要素が画像になって表示されるようなことができます。便利!
See the Pen Untitled by yana-gi (@yana-gi) on CodePen.
事前準備
今回はscaffoldで自動で作成されたformのtextarea部分を画像にして、サーバーに保存する処理を作ってみます。
rails new して scaffoldする
サンプル用のアプリを作ります。
rails new
して text型のbody
を持つモデルをscaffoldします。モデル名はなんでもいいですが、今回はcodeで作成していきます。
$ rails new $ rails g scaffold Code body:text
html2canvasをインストール
$ yarn add html2canvas
Active Storage をインストール
$ rails active_storage:install $ rails db:migrate
app/models/code.rb
class Code < ApplicationRecord has_one_attached :image # 追加 end
Railsでhtml2canvasを動かしてみる
いきなり保存する処理を作るのは難しいので、試しに要素の画像を画面上に表示するボタンを作ってみます。
app/views/codes/_form.html.erb
formテンプレートにボタンを追加します。
<input type="button" value="button" class="my-button">
app/javascript/packs/application.js
/app/javascript/src/code.js を読み込めるようにします
require("../src/code");
/app/javascript/src/code.js
ボタンが押されたらhtml2canvasが#code_body
の要素をcanvasに変換し、子ノードの最後に表示するようにします。
import html2canvas from "html2canvas"; document.addEventListener('DOMContentLoaded', () => { const myButton = document.querySelector('input.my-button'); myButton.addEventListener('click', function () { html2canvas(document.querySelector("#code_body")).then(canvas => { document.body.appendChild(canvas) }); }); })
これでボタンを押すとformのtextareaが画像として表示する処理できました✌️
画面上のDOM要素をサーバーに保存する
事前準備が終わりhtml2canvasの動かし方がわかったところで、実装に入っていきます。
JavaScript側の処理
- 画面上のDOM要素を取得する
- submitボタンが押される
- submit後の処理をキャンセルする
- 要素をcanvas形式に変換する
- canvasデータをデータURLに変換する
- データURLを隠しフィールドにsetする
- submit処理を実行する
Rails側の処理
- データURLを受け取る
- データURLからActiveStorageにattachする
JavaScript
app/javascript/src/code.js
import html2canvas from "html2canvas"; document.addEventListener('DOMContentLoaded', () => { // [1] 画面上のDOM要素を取得する const form = document.querySelector("form"); const code_body = document.querySelector("#code_body") const hiddenImageDataUrl = document.querySelector("#code_image_data_url"); // [2] submitボタンが押される form.addEventListener("submit", function (event) { // [3] submit後の処理をキャンセルする event.preventDefault(); // 要素をcanvas形式に変換する getImageDataUrl() .then((imageDataUrl) => { // [6] データURLを隠しフィールドにsetする hiddenImageDataUrl.setAttribute("value", imageDataUrl); }) .then(() => { // [7] submit処理を実行する form.submit(); }); async function getImageDataUrl() { // [4] 要素をcanvas形式に変換する const canvas = await html2canvas(code_body); // [5] canvasデータをデータURLに変換する return canvas.toDataURL("image/jpeg", 1.0); } }) })
ビュー
app/views/codes/_form.html.erb
<div class="field"> <%= form.hidden_field :image_data_url, value: "" %> </div>
app/views/codes/show.html.erb
<p> <strong>Image:</strong> </p> <p> <%= image_tag(@code.image) %> </p>
コントローラー
image_data_url をStrong Parameters で受け取り、画像をattachします。
dataURLをActiveStorageに保存する処理はこちらを参考にさせていただきました。
処理の流れやデータURLなどについて書くと長くなってしまうのですが、以前フィヨルドブートキャンプの日報で雑に書いたことをScrapboxに雑に転記したので載せておきます。
app/controllers/code_controller.rb
# frozen_string_literal: true class CodeController < ApplicationController before_action :set_code, only: %i[show destroy] def create @code = Code.new(code_params) @code.attach_blob(image_data_url) if @code.save redirect_to @code, notice: 'Code was successfully created.' else render :new, status: :unprocessable_entity end end private def set_code @code = Code.find(params[:id]) end def code_params params.require(:code).permit(:body) end def image_data_url params.require(:code).permit(:image_data_url)[:image_data_url] end end
モデル
app/models/code.rb
# frozen_string_literal: true class Code < 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 ) end end
app/models/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
これで画像をサーバーに保存する処理ができました🎉
注意事項
turbolinksがオンになっているとJS側の処理がうまく動かないことがあります。その場合はturbolinksをオフにするか、addEventListener
で指定するイベントをDOMContentLoaded
からturbolinks:load
に変えてみてください。
本番環境の画像の保存先をローカルにすると、herokuでデプロイした場合一定時間で画像データが消えてしまうので、S3などの画像サーバーに保管する必要があります。*1
画像毎にOGPの画像を設定するときに、相対パスurl_for
指定だとTwitterカードの反映ができません。絶対パスpolymorphic_url
で指定する必要があるようです。*2
さいごに
今回はformのtextareaを画像化しましたが、自分が作成したアプリではtextareaに入力したコードをいい感じにした要素を画像化して使っていました。
アイデア次第では色々面白いことができそうなので、参考にしてもらえると嬉しいです!