こんにちは。西日本テクノロジー&イノベーション室の藤田です。

日々(技術的に)強くなりたいと言っている新卒3年目です。趣味はプロレス観戦、座右の銘は「己こそ己の寄る辺」です。

先日まで関わっていたサービス開発案件で使用した、RubyでData URI形式で送られてきた画像をURLとして扱う方法を書いていきます。

背景

React + Ant Design Mobile + Railsでサービス開発をしています。

開発中のサービスでは、利用を希望するユーザーにFacebookログイン(OAuth 2.0)をしてもらった後にユーザー登録をしてもらいます。ユーザー登録時に本サービスで使うアバターを設定してもらうのですが、アバターの初期値にはFacebookから取得したアバターのURLを設定しています。ユーザーは必要があればアバターを変更できます。

ユーザー登録画面でアバターを変更するのに、Ant Design MobileのImage Pickerを使っています。ユーザーがFacebookに設定された初期アバターを変更しなかった場合、その画像はURLとして送られますが、ユーザーがImage Pickerを使用してアバターを変更した場合、その画像はData URIとして送られます。

Data URI化された画像データをそのままDBに登録することは避けたいですし、ユーザー登録以降は通常のURLかData URIかを意識したくありません。そのためData URIをデコードしてファイルとして保存して、そのファイルのURLをDBに登録することにしました。

Data URIとは

Data URIとは、data:スキームが先頭についたURIのことで、ファイルをインラインで文書に埋め込むことができるものです。

スキーム(data:)、MIMEタイプ、BASE64トークン、データ自体の4つの部品で構成されます。

data:[<mediatype>][;base64],<data>

扱うデータがURIで使える文字のみで構成されていればエンコードすることなく埋め込むことができますが、URIで使えない文字列に対してはエスケープが必要です。また、文字以外のデータはBASE64エンコードする必要があります。

例えば、PNGの画像をBASE64エンコードしたData URIは以下のようになります。

data:image/png;base64,<data>

<data>の部分は、実際にBASE64エンコードされたデータそのものが入ります。

Data URIをDBに登録することの問題点

  • サイズが大きい

画像を表すData URIはDBに登録するにはサイズが大きく、ストレージを圧迫してパフォーマンス低下を引き起こしかねません。

  • ページサイズが大きくなる

Data URIはページに埋め込まれます。ページそのものにData URIのサイズが乗るので、読み込むページのサイズが画像の分だけ増えます。

Data URI形式の画像をURLで扱うためにやったこと

  • 送られてきた画像がData URIかどうかを判断する
  • Data URIであればBASE64でデコードする
  • S3に画像を保存する
  • S3に保存した画像のURLを取得する
  • URLをDBに登録する

実装

以下で、コードを見ていきます。

送られてきた画像がData URIかどうかを判断する

URIクラスを使ってパースします。スキームがdataであればData URIであると判断します。

  def create
    avatar = params[:avatar]
    uri = URI.parse(avatar)
    if uri.scheme == "data" then
      # Data URIと判断した場合の処理
    end

Data URIであればBASE64でデコードする

Data URIであればデコードして拡張子を取得します。

opaqueでスキーム以外の文字列を取得できるので、区切り文字を利用してmime_typedataを取得します。

dataはBASE64でデコードし、mime_typeは拡張子の判断に使用します。拡張子はS3に画像を保存する時に使用します。

  data = decode(uri)
  extension = extension(uri)
 def decode(uri)
    opaque = uri.opaque
    data = opaque[opaque.index(",") + 1, opaque.size]
    Base64.decode64(data)
  end

  def extension(uri)
    opaque = uri.opaque
    mime_type = opaque[0, opaque.index(";")]
    case mime_type
    when "image/png" then
      ".png"
    when "image/jpeg" then
      ".jpg"
    else
      raise "Unsupport Content-Type"
    end
  end

S3に画像を保存する

AWS SDK for Rubyで画像をS3に保存します。ここで画像を公開し、URLを返します。

  def put_s3(data, extension)
    file_name = Digest::SHA1.hexdigest(data) + extension
    s3 = Aws::S3::Resource.new
    bucket = s3.bucket("example-bucket")
    obj = bucket.object("avatars/#{file_name}")
    obj.put(acl: "public-read", body: data)
    obj.public_url
  end

S3に保存した画像のURLを取得する

URLを取得して、avatarに設定します。

  user = User.create(email: params[:email], avatar: avatar)

URLをDBに登録する

ユーザー情報をDBに登録します。

  user = User.create(email: params[:email], avatar: avatar)

コード全体

Controllerのソースコードは次のようになります。

class UsersController < ApplicationController

  def create
    avatar = params[:avatar]
    uri = URI.parse(avatar)
    if uri.scheme == "data" then
      data = decode(uri)
      extension = extension(uri)
      avatar = put_s3 data, extension
    end
    user = User.create(email: params[:email], avatar: avatar)
    render json: user
  end

  private

  def decode(uri)
    opaque = uri.opaque
    data = opaque[opaque.index(",") + 1, opaque.size]
    Base64.decode64(data)
  end

  def extension(uri)
    opaque = uri.opaque
    mime_type = opaque[0, opaque.index(";")]
    case mime_type
    when "image/png" then
      ".png"
    when "image/jpeg" then
      ".jpg"
    else
      raise "Unsupport Content-Type"
    end
  end

  def put_s3(data, extension)
    file_name = Digest::SHA1.hexdigest(data) + extension
    s3 = Aws::S3::Resource.new
    bucket = s3.bucket("example-bucket")
    obj = bucket.object("avatars/#{file_name}")
    obj.put(acl: "public-read", body: data)
    obj.public_url
  end

end

avatarがData URIでなければ処理はせず、Data URIであった場合にデコードしてS3に保存し、URLをData URIの代わりにavatarに格納しています。

avatarをDBに登録すれば、以降同じようにURLで画像が扱えます。

参考

データ URL – HTTP | MDN

RFC 2397 – The “data” URL scheme


本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。