AGRISMILE DEV BLOG

株式会社AGRI SMILEの開発ブログです。

rubyXLでエクセル上の画像を差し替える

プロダクト開発部新田です。
エンジニアはみなxlsxよりもcsvが好きですが、現実にはシステムでエクセルファイルを出力するという実装をする必要に迫られることがあります。
またそのエクセルファイルに画像を貼る必要があることもあります。

rubyでエクセルに画像を貼りたくて苦心したので、今後同じように困る誰かの一助になることを願って、rubyXLでエクセルファイルの画像を差し替える方法について書きます。

今回やりたいこと

この画像のようなエクセルファイルをシステムから出力する必要があります。

なお、名前と電子印影はユーザーごとに変える必要があります。

また、実際に生成するエクセルファイルは複雑なレイアウトであり、
プログラムでスクラッチからレイアウトを再現するのはとてもつらく、人の手によって作られたエクセルファイルをテンプレートとしてその中の一部を書き換える方法で実現するのがよさそうな状況です。

参考情報・先行研究

qiita.com

エクセルをRubyで扱う場合に、要件に合わせてどのライブラリを選べばよいかについてまとめられています。
画像を持ったxlsxを作るには caxlsx が良さそうですが、caxlsxはエクセルファイルを読み込んで編集することはできず、新規に作る場合に画像を載せることが得意なライブラリであって、今回の要件に合わず。
一方でrubyXLはエクセルファイルを読み込んで内容を変更することができますが、画像に関するサポートはありません

qiita.com

rubyXLで一般に使われるものより低レベルなAPIを用いて画像を貼り付ける方法について書いてあります。
コピペすると確かにワークしますが、これを保守し続けられるかというと疑問符がつき、採用を見送りました。(たとえば、ランタイムの都合でrubyXLのメジャーバージョンを上げなくてはならなくなり、一部のAPIが提供されなくなる等起きた場合を想定して; rubyXLはバージョンごとにAPIがごりっと変わることがあります)

どう解決したか

.xlsx ファイルは、xmlとアセットを持ったzip形式のファイルです。
(このことは、 unzipコマンドで.xlsxを解凍することで確かめられます; mac標準の解凍では何故か失敗するのでunzipを使います)

unzipして見てみると、エクセル上に貼り付けた画像がそのままのファイル形式で保存されている事がわかります。
では、この画像ファイルを差し替えて再度zipしてみると画像の差し替えができるのでは?と思いやってみたらできました。

これをRubyでどうやるかというと、プレースホルダーとして適当なpngファイルを貼りつけたエクセルファイルを用意して、

  require 'rubyXL'
  require 'zip'

  workbook = RubyXL::Parser.parse(Rails.root.join('path', 'to', 'the_file.xlsx').to_s)
  tempfile = Tempfile.new(['filename', '.xlsx'])
  # テキストを書き換える等の処理をここで行う
  workbook.write(tempfile.path)

  Zip::File.open(tempfile.path) do |zipfile|
    # 画像は xl/media/* に格納されている。名前は image1 等エクセルによってリネームされるので(決め打ちでも構わないが)globで探す。
    zipfile.glob('xl/media/*.png').each do |entry|
      zipfile.get_output_stream(entry.name) do |f|
        f.write user_info.stamp_image.file.read
      end
    end
  end
  record.file_column = tempfile # carrierwaveでmount_uploaderしているカラム
  record.save!
  tempfile.close!

こうです。 方法はややハッキーですがコードとしてはシンプルで(直接的に外部コマンドを呼んだりしていないので)ギリギリrubyの世界から逸脱せずにすみました。

おわりに

書いてみてよく考えたら「rubyXLで」というより「rubyzipで」という内容でした。
rubyXLを使っていても画像を扱う方法はありますよ、ということでご容赦ください。

今回紹介した方法はユースケースがかなり限定的なのでより柔軟に(画像サイズを変更したいとか、画像の枚数を増やしたり減らしたりだとか)操作したい場合は、 Pythonに便利なライブラリがあるので、この手の操作はRuby固執せずにPythonで書いてLambda等にそれ用のファンクションを用意してしまうのが良いかもしれません。