はじめに
iOS 10の標準MUAにTwitterやFacebookのアプリからコピーしたテキストを貼り付けて、古いブログツールにメール投稿すると、投稿の前後や途中に?
が混じることがあります。
調べてみたところ、その原因はなんとBIDIでした。
メールの受信先がUnicode以外で文字を処理している場合には同等のトラブルが発生し得ます。本質的には文字列をすべての処理の過程でUTF-8で扱えばよいだけですが、とりあえずの応急処置もあります。
【追記】:この問題、先日、「WKWebViewのアドレスバーからWebページのURLをコピーし、それをFacebookの投稿に貼り付けただけで発生」しました。通常の投稿ですとLREなどはそのまま解釈されるのでふつうに表示されるだけですが、URL抽出でLREを拾って、そのままurlencodeされてしまったようです。 →リンク先にLREが紛れたFacebookの例
文字化けの例
私は旅行好きで、自分用オンラインアルバムとして旅ブログを開設しています。2004年の開始時はライブドアブログを使っていましたが、ほどなく運用を自宅サーバーに移し、はじめはBlogn、次にP_BLOGを導入して、今も後者での運用を続けています。いろいろプラグインも自作してまして、その中にはいわゆるモブログ機能、メールからの投稿機能もあります。
で、ガラケー、AndroidときてiPhoneに至るまで、ずっと順調に動作していたのですが…
iOSが10になってから、ときどき、文字化けというか、投稿テキストの前後や途中に、疑問符がつくようになったのでした。
ご覧のスクリーンショットの記事では、タイトルの前後に思いっきり、意図しない?
がついてしまっています。(なお、タイトルの中の日本語も変ですが、これはコピペからの編集ミスです(^^;))
メール本文をダンプしてみた
どう考えても原因は何らかの文字コードトラブル。かつ、P_BLOGはなぜか「表示内容はUTF-8で出力するのにMySQLの文字コードはeucjp」という妙な実装になっており、この?
は何か「未知の文字」を表しているに違いない、と見立て、iPhoneからのメールの本文を生ダンプしてみました(まるで旅と無関係の本文ですが、現象は同一です)。
結果は上のとおり。冒頭にe2 80 aa
、末尾にe2 80 ac
という、UTF-8にしては見慣れないバイト列があるわけです。
原因はBIDI
このバイト列、UnicodeにするとそれぞれU+202A
とU+202C
。まさに、BIDI_control charactersのLeft-to-right embedding (LRE)
とPop directional formatting (PDF)
でした。
これをPHPのmb_convert_encoding
でeucjpに変換しようとしても、そんな文字はないので、「変換不能」扱いで?
に変換されていた、というわけでした。
これらは、iOS標準のMUAに、ふつうに日本語キーボードから文字列入力をした場合にはまぎれこみません。というかまぎれこませようがありません。
しかし、TwitterやFacebookのアプリから文字列をコピーして標準MUAにペーストすると、まさにこれらがまぎれこむことがあります。
私は旅行中にTwitter、Facebook、Instagram、そして旅ブログへと画像をリアルタイム投稿していますが、それぞれ文字数や画像添付数、ハッシュタグづけなどの違いがあるため、上記の順番でそれぞれ個別に送信します。そして制限がきついTwitter投稿をコピーして、それをFacebookに貼り付けて投稿します。場合によってはTwitterの複数の投稿をFacebookの1投稿にまとめることもあります。そして最後の順番になる旅ブログへの投稿は、TwitterかFacebookからのコピペとなることがほとんどなのです。
確かに、それらのアプリはBIDIに対応していて当然のもの。そしてiOS 10で突然、標準MUAなのかクリップボードなのかわかりませんが、文字列レベルでBIDI対応が登場したため、こういう現象が起きてしまったのでした。
応急処置
今回の「文字化け」の原因は、BIDI_control charactersがeucjpに変換不能、というものです。
「そもそもイマドキeucjpなんか使ってるの?」っていう話でしかないんですが、個人的には「1つの投稿に複数のカテゴリーを設定できる」というP_BLOGの機能の代替が他にあまりなく、「既に動いているもの」を捨てるまでではない、というところから、ごまかせるところはごまかしてなんとか使い続けたい、というずぼらです。
なので、今回も応急処置で逃げました。モブログ投稿処理の冒頭で、
mb_substitute_character('none');
としただけです。