IEやExcel向けにUTF8⇒SJIS変換を行う


ダウンロードファイルに日本語を使用する場合、IE だと文字化けする現象に悩まされることが多いと思います。
これは、IE がダウンロードファイル名に SJIS(WIndows-31J)を基本的に要求するためです。
(正確には、OS の言語設定のデフォルトコードページ、だったかも。日本語だと CP932 ですが、他の言語だとまた違うと思います)
なので多くの方は IE 向けに SJIS に文字エンコーディング変換を行うか、URL エンコードしていると思います。

MS の KB にも下記のように言及されています。
http://support.microsoft.com/kb/436616
IE9 からはダウンロードファイル名を UTF-8 として扱う(SJIS でも正常に扱える)ので、この問題に頭を悩ませる機会は今後減りそうですね。

一方、表題の Excel は基本的に SJIS オンリーで、UTF-8 を簡単には扱えません。
CSV ダウンロード ⇒ ダブルクリックで開く ⇒ CSV に関連づけられたエクセルが起動して開く ⇒ 文字化け ってコンボは当分続きそうです。
一応 UTF-8 を BOM 付きにすれば Windows 版エクセルでは正常に閲覧できるようになりますが、Mac 版では文字化けしたままとか手焼かせやがってコノヤロ的な感じです。

IE8 もエクセルもまだまだ相手にする必要があるので、このようなコードを組みました。

application_controller.rb
  # IEのダウンロードファイル名の文字化け対応
  def filename_for_download(filename)
    if request.env['HTTP_USER_AGENT'] =~ %r{MSIE} && request.env['HTTP_ACCEPT_LANGUAGE'] =~ /^ja/
      filename = Utf8ToWin31jCodeConverter.convert_to_windows31j(filename)
    end
    filename
  end
utf8_to_win31j_code_converter.rb
class Utf8ToWin31jCodeConverter

  class << self
    def convert_table
      [
        %w|301C FF5E|, # WAVE DASH            => FULLWIDTH TILDE
        %w|2212 FF0D|, # MINUS SIGN           => FULLWIDTH HYPHEN-MINUS
        %w|00A2 FFE0|, # CENT SIGN            => FULLWIDTH CENT SIGN
        %w|00A3 FFE1|, # POUND SIGN           => FULLWIDTH POUND SIGN
        %w|00AC FFE2|, # NOT SIGN             => FULLWIDTH NOT SIGN
        %w|2014 2015|, # EM DASH              => HORIZONTAL BAR
        %w|2016 2225|, # DOUBLE VERTICAL LINE => PARALLEL TO
      ]
    end

    def convert_to_windows31j(utf_8_str)
      utf_8_str.encode!("UTF-8-MAC", "UTF-8", :invalid => :replace, :undef => :replace, :replace => '')
      utf_8_str.encode!("UTF-8")

      utf_to_win31j_safe_str = Utf8ToWin31jCodeConverter.convert(utf_8_str)
      utf_to_win31j_safe_str.encode 'WINDOWS-31J'
    end

    def convert(str)
      result = str.dup
      convert_table.each do |arr|
        from   = code_point_to_char(arr[0])
        to     = code_point_to_char(arr[1])
        result = result.gsub(from, to)
      end
      result
    end

    def code_point_to_char(code_point)
      code_point.to_i(16).chr("UTF-8")
    end

    def char_to_code_point(char)
      char[0].unpack("U*").first.to_s(16).upcase
    end
  end
end

肝になるのは二点。
convert_table メソッドの変換テーブルがないと、SJIS 変換に失敗します。
詳細は以下のサイトを参照していただくと理解しやすいと思います(コード書くときにも参考にさせていただいています)。
http://esoz.blog.fc2.com/blog-entry-59.html
http://space.geocities.jp/nequomame/java/mojibake/mojibake_01.html

もうひとつは convert_to_windows31j メソッドの最初の二行で、これがないと SJIS 変換に失敗する場合があります。
再現する環境は限定的なので、なくても正常なことが多いと思いますが。
原因は UTF-8 の NFC と NFD の違いによるものです。
こちらのサイトをご覧いただくと分かりやすいと思います。
http://d.hatena.ne.jp/zariganitosh/20131124/utf8_nfd_nfc_bom
http://sho.tdiary.net/20110204.html
※ 後者のirbでの文字エンコーディング確認操作はとても分かりやすいです。時間があればぜひ試してみてください。

例えば「あさがお」という日本語ファイル名の場合、内部的には「あさか゛お」と、濁点が一文字として記録されているんですね。
なんか半角カナみたいですけれど。
これを SJIS 変換しようとした場合、濁点に対応した文字が存在しないため、例外が発生する訳です。
通常であればこの仕様を意識する必要はあまりない(ブラウザがなんとかしてくれる)と思うのですが、FireFox でアップロードされたファイルのファイル名を SJIS 変換する場合、この UTF-8-MAC の問題が顕在化します。

Job-Hub ではユーザがアップロードしたファイルを別のユーザがダウンロードして閲覧する機能があるのですが、上記が原因で例外が発生していました。
Mac 環境の Safari や Chrome でアップロードしたファイルであれば問題ないのですが、FireFox でアップロードした日本語ファイル名のファイルの場合は顕在化します。
前者二つは NFC に変換してアップロードする仕組みで、FireFoxはNFDのままアップロードするのでしょうか。
アップロード側が Mac 版 FireFox、ダウンロード側が IE って組み合わせでしか起きないので、発生するのは結構レアかもしれません。
実際のところこの UTF-8 ⇒ SJIS 変換処理自体はもう1年近く前に実装していて、NFD の問題が発生したのはつい最近のことです。
文字コードっていろいろ難しいですね。
※ 追試が十分ではないので再現条件は間違っていたり不十分だったりするかもしれません。お気づきの方は教えてください。