Chienomi

超簡単にWebP圧縮のWebUIを作る

開発::noddy

宇宙庭園は画像アップロードのサイズ制限が80kBである。 なかなか厳しい制限で、普通は圧縮なしにアップロードはできない。

別におのおのが任意の圧縮ツールを用意して使うこと自体は何も難しいことではないだろうと思うが1、個人的にはスマートフォンでは割と困る。

というのも、私はConvert Imageというアプリを使っているのだが、音声付きで動画広告が流れる上に、リサイズとか色々処理はできない。

PCではmogrifyとcwebpを使って処理しているので全く問題ないのだが、前々から「スマホで画像上げにくいなー」とは思っていた。

そこで、web上でサクっと圧縮するツールを作ることにした。

重要なのは「サクッと」の部分。 可能な限り省エネで、つまり手抜きで作ることだ。

まずは普段の流れ

別に難しいことはなく、

mogrify -resize 800x800 -strip image.jpg
cwebp image.jpg -o image.webp

って感じで済むことが多い。 quality factorなどを指定しなくても、あらかじめリサイズしてあれば80kB未満に抑えるのは簡単だ。

cwebpには-sizeというオプションがあり、これで希望のサイズの指定をすることができる。 ただし、デフォルトでは10回まで圧縮するが、10回でそのサイズまで圧縮しきれないこともある。

コード

#!/usr/bin/ruby
require 'cgi'

cgi = CGI.new

value = cgi.params["source"][0]
name = nil
IO.popen(["perl", "-mHTML::Entities", "-e",'use utf8;', "-e", 'use Encode', "-e", 'print Encode::encode("UTF-8", HTML::Entities::decode_entities($ARGV[0]));', File.basename(value.original_filename, ".*")], "r") do |io|
  name = io.read.strip
end

mogrify = ["magick", "mogrify", "-strip"]

case cgi.params["resize"][0]
when "2000px"
  mogrify += ["-resize", "2000x2000>"]
when "1500px"
  mogrify += ["-resize", "1500x1500>"]
when "1000px"
  mogrify += ["-resize", "1000x1000>"]
when "500px"
  mogrify += ["-resize", "500x500>"]
when "50%"
  mogrify += ["-resize", "50%"]
when "25%"
  mogrify += ["-resize", "25%"]
end

puts "Content-Type: application/octet-stream"
puts "Content-Disposition: attachment; filename*= UTF-8''#{CGI.escape name}.webp; filename=image.webp"
puts "Content-Transfar-Encoding: binary"
puts

system(*mogrify, value.local_path)
system("cwebp", "-size", "70000", "-o", "-", value.local_path)

ポイント解説

CGI

こういうちょっとしたツールを作るときに便利なのがCGI。

だが、これは私はLighttpdでCGIを動かす環境がある、ということを前提にしてCGIを選択している。 環境によってはWebrick/CGIを使うとか、Sinatraを使うとか、選択肢は色々ある。 簡単にやれるものを選べば良い。

この「簡単」はファイルの受け取りと取り扱いが簡単ということを前提にしている。 cgiライブラリは受け取ったファイルをTempFileにするので、ファイルを生成しなければ後始末を考えなくていい。

余談だが、現状Nginx-Lighttpdの構成になっているのだが、他にHiawathaも使っている。 しかし、HiawathaはリクエストボディをCGIスクリプトに渡した最後でEOFを発行しないので、多くのライブラリがまともに動作しない。 自力でContent-Lengthを見て読むことはできるが、ちっとも簡単でなくなるため、やらないほうがいい。

ファイル名の受け取り

魔術じみたPerlのワンライナーを呼び出しているが、これはRuby 3.0のCGI.escapeHTMLが数値による実体参照をデコードしてくれないからだ。 いや、マニュアルにはしてくれるように書いてあるのだが、うまく動作しない。おそらく;の有無が問題。 そこで、Perlにやらせることにしたわけだ。

ここはうまく動作しない理由を探るのにちょっと時間がかかったところ。

Content-Disposition

Content-Type: application/octet-streamは結構有名で知っている人も多いと思うのだけど、少し踏み込んだ内容になるのがContent-Disposition

これは、コンテンツをどう扱うべきかの提示なのだけど、octet-streamで返している時点で大抵のブラウザは保存ダイアログを出す。 ここで重要なのは、ファイル名を与えるということだ。

filename="filename"形式は使える文字に結構な制限がある。 元のファイル名を維持しようとすると、filename*= UTF-8''filename形式を使うことになる。 このファイル名はURIエンコードされている必要があるのだけど、ファイル名次第ではそれでも通らないことがある。

両方指定していると、filename*=で指定したファイル名を使えないとブラウザが判断すると、filename=のものを使うため、フォールバックに使える。

ちなみに、先のファイル名のデコードの問題でfilename*=のものが反映されず、ここでかなり時間を使ってしまった。 省エネ優先であれば、さっさと諦めてフォールバックにゆだねても良かった。

コマンド呼び

cgiライブラリはTempFileに保存する以上、そこに「ファイルがある」状態になっている。かつ、#local_pathメソッドでそのファイルの所在を特定できる。

普通はFile#readを使って受け取ったデータを読むところだけれど、別にRubyのスクリプト上で処理しなければいけない理由は全くないので、そのままmagickcwebpを呼んでいる。

さらに言うと、cwebpは標準出力にそのまま吐かせている。 CGIの動作的にはレスポンスデータを標準出力に吐けば良いわけで、もうヘッダーは出力した後だから残りの出力はcwebpの出力だけである。 なので、cwebpにそのまま標準出力を使わせればお任せでいける。

公開は……無理。

見ての通りこのスクリプトは入力データの健全性チェックなどをしていない。 そのため、単純にこのスクリプトを公開するわけにはいかない。

このスクリプトの安全性の担保はサーバー側で認証を導入することで行っている。 webアプリケーションの雑さを許容する前提であるため、このスクリプトをそのまま公開のアプリケーションとして動作させることはできない。

公開するためには、入力データとファイル名の検証を行えば良い。 このスクリプトは画像を保存しないため、入力データが正当な画像であることが担保されれば画像コンテンツの中身まで責任を持つ必要はない。

だが、ImageMagickとcwebpを呼ぶということは、それなりに計算リソースを必要とする重い処理なわけで、無制限に利用させるのは難しい。 そのため、何らかのアカウントで信頼できるとみなしたユーザーに使わせる形にする必要がある。 そうなると、アカウントの信頼性を確認できるプラットフォームのOAuthを使うような方法になるだろう。だいぶ面倒だ。

そして、そのような方法をとったからといって、こんなザルなアプリケーションをそのまま使わせられるようになるわけではない。

といったことを考えると、割に合わない労力になるため、トータルで考えれば公開できるようにはならないだろう。