Flash内の表示オブジェクトをPNG画像として開く

はじめに

世界樹の迷宮風マップツールII β で実装した、マップの画面をPNG画像として開く、というお話です。

簡易なお絵かきができるようなFlashはよくありますが、描いたものをいちいちPrintScreenしたりするのも面倒ですよね。

セキュリティ的な問題だと思いますが、Flashで生成したデータはそのままファイルとしてそのまま保存はできません。せいぜい、クリップボードにテキストを貼り付けられる程度です(バイナリは無理)

ならばせめて、画像を右クリックして名前をつけて保存、ぐらいのことが出来るようにしたいと思います。

ここまでならサーバーを介して既にやっているところも多いのですが、さらに可能な限りローカルで処理を行い、できるだけサーバーの負担を軽減するようにします。

ちなみに、ActionScript3.0/Flex2 SDKで作っています。AS2まではByteArrayとかがないので下の方法では出来ません(無理やり画像化する方法はあるようです)

Step 0 〜 とりあえず実際の例を見てみる

言葉で書いていてもわかりにくいので、簡単なサンプルと使用したソースを上げておきました。どういう風に動くのかはそちらを見てみてください。IEFirefoxでは微妙に挙動がちがいます。

ちなみに、世界樹の迷宮風マップツールII β 新規マップの上部の左から6番目のアイコンからおんなじことが出来ます。(というか、これをやるために作ったので)

Step 1 〜 表示オブジェクトをPNG画像にする

では実装したコードについて順序だてて説明してきます。

まずは、DisplayObjectをBitmapDataに変換して、BitmapDataをPNGエンコードされたByteArrayに格納します。たとえばこんな感じ

import com.adobe.images.PNGEncoder;

import flash.display.BitmapData;
import flash.display.DisplayObject;
import flash.utils.ByteArray;

public function encodeDisplay(display:DisplayObject):ByteArray
{
  var bitmapData:BitmapData = new BitmapData(display.width,display.height);
  bitmapData.draw(display);
  var png:ByteArray = PNGEncoder.encode(bitmapData);
  return png;
}

BitmapData.draw()でDisplayObjectをBitmapDataに変換します。もちろん、MovieClipとかSpriteとかShapeとかはDisplayObjectを継承しているのでどれも同じ処理が使えます。BitmapData.draw()に渡せるのはIBitmapDrawableインターフェース(DisplayObjectとBitmapDataで実装されている)ですが、BitmapDataならそのままPNGエンコードすればいい話です。

BitmapDataからPNGへの変換はcom.adobe.images.PNGEncoderを使っています。使い方はソースのとおり。簡単ですね。

Step 2 〜 サーバーに送って表示させる

生成したByteArrayをサーバーにPOSTして、画像を表示させます。たとえばこんな感じ

import flash.net.URLRequest;
import flash.net.URLRequestMethod;

private static const OUTPUT_URL:String = 'サーバの送り先';

public function output(image:ByteArray,window:String = null):void
{
  request = new URLRequest();
  request.url = OUTPUT_URL;
  request.contentType = 'application/octet-stream';
  request.method = URLRequestMethod.POST;
  request.data = image;
  navigateToURL(request,window);
}

サーバー側はなんでもいいですが、例えばPHPで簡単に書けば

<?php
$png = file_get_contents('php://input');

if(isset($png)){
  header("Content-type: image/png");
  header("Cache-control: no-cache");
  
  print $png;
}
else{
  print 'no image';
}
?>

とでもしてやればいいです。実際に使う場合は、サーバ側は送られた画像が本当にPNGなのかとかスクリプトが埋め込まれてないかとかチェックする必要があります(サーバーに画像を保存するなら特に)

まあ簡単ですね。

Step 3 〜 dataスキームも使う

サーバーを介せばもうそれでいい話なんですが、あんまり負担が増えても困りものです。せっかくFlashがローカルで画像処理してくれているのだから、できれば全部ローカルで処理したいと思うのが普通ですよね。

そこで、dataスキームを使えば、サーバーを介せずにローカルだけで処理できます。dataスキームは、バイナリのデータをテキストに符号化(普通はBase64で符号化)し、URLの代わりに直接データを埋め込むことができるものです。ブラウザによってはdataスキームが実装されていませんが、FirefoxOperaなどではdataスキームを使うことが出来ます。

例えば、「data:text/plain;base64,aGVyaWV0」という文字列をブラウザのアドレスバーに貼り付けてやれば、dataスキームが使えるブラウザならば「aGVyaWV0」という文字列をBase64でデコードしたテキストが表示されるはずです。エンコードされている文字がtext/plain以外の形式でも、適宜書き換えてやればOKです。

で、要はBase64に符号化してやればいいだけなので実装も簡単です。ライブラリもあるのでそちらを使ってやれば下のようになります。

import com.dynamicflash.utils.Base64;

public function outputDataScheme(image:ByteArray,window:String = null):void
{
  var imageString:String = Base64.encodeByteArray(image);
  var url:String = 'data:image/png;base64,' + imageString;
  var request:URLRequest = new URLRequest(url);
  navigateToURL(request,window);
}

これはcom.dynamicflash.utils.Base64 を使っています。

ちなみに、標準でmx.utils.Base64Encoderというクラスもある(でもFlex2のドキュメントには載ってない)ので、そっちを使うのであれば

import mx.utils.Base64Encoder;

public function outputDataScheme(image:ByteArray,window:String = null):void
{
  var encoder:Base64Encoder = new Base64Encoder();
  encoder.encodeBytes(image);
  var imageString:String = encoder.flush();
  var url:String = 'data:image/png;base64,' + imageString;
  var request:URLRequest = new URLRequest(url);
  navigateToURL(request,window);
}

とも書けます(ちょっと使いにくい気もするけど)で、バイナリをBase64符号化したものに、先頭に「data:image/png;base64,」とくっつけてやってそれをURLとして開いてやれば、dataスキームに対応したブラウザなら画像が表示されるはずです。

でも、dataスキームに対応しているかどうかはブラウザ依存。困りますね。

Step4 〜 dataスキームとサーバーを使い分ける

仕方がないので、dataスキームが実装されているブラウザではdataスキームを使うことでサーバーの負担を減らし、どうしようもない場合はサーバーを介して表示させることにしましょう。閲覧者のブラウザがdataスキームを実装しているかどうか判別できればいいわけです。

実際に適当なデータをつかったdataスキームを読み込んで、結果正しく読み込めていれば判別できるかと思ったのですが

import flash.net.URLLoader;
import flash.net.URLRequest;

private _ableDataScheme:Boolean = false;

public function checkAbleDataScheme():void
{
  var url:String = 'data:text/plain;base64,aGVyaWV0'; 
// aGVyaWV0はherietという文字列をBASE64エンコードしたもの。対応するブラウザで直接開くと読み込める

  var request:URLRequest = new URLRequest();
  request.url = url;
  request.method = URLRequestMethod.GET;

  var loader:URLLoader = new URLLoader();
  loader.addEventListener(Event.COMPLETE,loadComplete);
  loader.load(request);
}
private function loadComplete(e:Event):void
{
  var loader:URLLoader = URLLoader(e.currentTarget);
  if(String(loader.data) == 'heriet')
    _ableDataScheme = true;
}

上のcheckAbleDataSchemeのような処理をおこなっても、URLLoaderではdata スキームに対応していないようで、対応しているブラウザで開こうとしてもIOErrorEventになってうまくいきません。

そこで、代わりにJavaScriptでブラウザのdata スキーム判別を行うコードがあったので、そいつを使います。JavaScriptでのBase64が使用できるかどうかのBoolean値を返す関数をdschk()とすれば、それをFlash側で呼び出してやればいいわけです。たとえばこんなかんじ

private var _ableDataScheme:Boolean;

public function checkAbleDataScheme():void
{
  try{
    _ableDataScheme = Boolean(ExternalInterface.call('dschk'));
  }
  catch(e:Error){
    _ableDataScheme = false;
  }
}

画像を表示させる前に、checkAbleDataScheme関数を呼び出しておいて、画像を表示させるときに_ableDataSchemeがtrueならdataスキーム、falseならサーバーに送信すればよいということになります。ただ、この方法だとHTMLから呼び出していて、かつJavaScriptが有効でないと使えないので状況が限定されるというのがやや難点です(だいたいの閲覧者は有効だとおもうのですが)

Flash側から直接、使用しているブラウザがdata スキームに対応しているかどうかわかれば一番いいんですけどね。とりあえず私の知識ではできませんでした。

Step5 〜 応用することを考える

というわけで、上では表示オブジェクトをPNGの画像として開く、という問題に限定して考えましたが、ここまで読んだ利口なみなさんならばすぐに応用例が思いつくことでしょう。

サーバーやdataスキームとして渡すデータ形式は別にPNGである必要はないし、画像ですらある必要はありません。バイナリだろうがテキストであろうが、同じような方法で渡すことが出来ます。

表示オブジェクトからJPG形式のByteArrayにするライブラリもありますし、音声なり動画なりSWFなりをFlash側で編集とか生成とかしてやれば、わりと簡単にブラウザから読み込んでダウンロードとかさせることができるのです。