2011年12月17日土曜日

FlashでPDF・続き

■続き、っていうか本番■

前回はFetchしてデータを一覧表示するところまででした。

FlashでPDF

「そもそもなんでFlashなのか?」という話が抜けていました。
  1. クライアントサイドの有り余るリソース(メモリ, CPU)を使える
  2. きめ細かく罫線などを制御できる
  3. Force.comでは実現不可能なグラフも作れる
  4. Rich GUIで余計な苦労を負わないで済む
  5. PDF添付メールやファイル出力の制約が少ない
PDF生成そのものに関しては「セルその他の描画を細かく制御できるので、基本的にどんな帳票でもおk」ってのがFlash / purePDFでPDFの長所の一つです。クライアントサイドでは数GBのメモリも当たり前なので、ガバナーの制約を気にしないで好きなことできるのもメリットです。「数万件のデータをドーンと読み込んできてループまわしながら集計し、役員会に出せるような美しい罫線の入ったPDFを作る」ようなケースで使えます。

あとグラフ。Flashのグラフは制約が少ないので複雑なグラフを作りたい場合にはForce.comよりもラクできます。

また最近はWebアプリケーションであってもrichなGUIが求められる案件が増えてきました。Force.comでも売掛入金の消し込み画面で設定した条件が間違っていたらボタンをdisableしたり、jQueryで関連するデータをアニメーション表示したりお客様の多彩なご要望に苦労する日々でございます。

でもFlashはrich GUIの本家です。

先日もあるコンサルタント会社から「100近い検索条件から案件を抽出する検索画面」という依頼を受けましたが、設定した条件が埋もれてしまわないようわかりやすく表示するのもFlashなら自由自在です(簡単です、と書かないところに私の誠実さを感じてください)

 そうそう、それにFlashなら開発の途中でオブジェクトの項目を変更する必要が生じても「それAPEX/VFで使ってるから変更できねぇよw」とForce.comから怒られることもありません(笑)。

■purePDFで■

FlashでPDFを生成するためのライブラリでメジャーなのはAlivePDFとPurePDF。今回はPurePDFを使います。PurePDFはJavaのPDFライブラリiTextをFlashに移植したもので、私はiTextには散々痛い目にあったお世話になったことがあるので、まぁ学習コストがかからないだろう、と。

使ってみたら日本語が文字化けたりしてまた散々痛い目にあったのですが、そういう経緯は全部スキップします。ここからライブラリをダウンロードします。

Player10.0 でも purePDF で日本語PDFを作る

FlashBuilder上のプロジェクトのlibsフォルダには前回force-air.swcを追加しましたが、今回ダウンロードしたpurePDF.swcとpurePDFont.swcも追加します。

APIなどに関してはFlashとJavaとで実装上の違いはありますが、iTextの方がサンプルも多く用意されていて便利です。


■解説■

ソースは最後に貼ります。基本的な処理の流れとしては

  1. サイズと出力先(バッファ先)を指定してDocumentを生成する
  2. ベースフォントを作る
  3. ベースフォントにサイズなどを指定してフォントを生成する
  4. テキスト、イメージ、別のテーブルなどをセットしたセルを作る
  5. セルをテーブルにセットする
  6. テーブルをドキュメントにセットする
  7. 必要ならページ送り(document.newPage)

では、実際の処理を見ていきます。

>queryHandler
前回見たようにarrayDataにfetchしたデータを溜めていきます。result.doneになったらfetch完了ですので、dataGridにarrayDataをセットし、pdfの生成を始めます。

PdfWriterでバイトストリームをセットしつつ用紙サイズを設定します。ここではPageSize.A4でA4縦を指定していますが、PageSize.A4.rotate(90)と書くとA4横になります。

>initPdfFont
ベースフォントを生成します。いろいろなフォントを試してみたのですが、結局これ平成明朝しか使えませんでした。ゴシックも指定できるのですが、日本語フォント部分は同じで英数字部分が変わるのみでした。

>createPdf
実際に使用するサイズのフォントを生成してからPDFを生成していきます。vecという配列を用意していますが、これはTableのrowをどういう比率で生成するかを指定するためのものです。値が合計いくつになってもかまわないので、私はお客さんからもらってきたサンプル帳票に物差しあててその数字を使ったりしていますw

あとは文字列を元にしてセルを作ってテーブルにせっせと詰め込んでいくだけです。終わったらnewPageでページを閉じて、最後に document.close(); でドキュメントを閉じます。

>cellFromStringFontAndAlignment
セルを生成しています。関数名がやたらと長いのは私が長年MacOS系プログラマだったからです。生成したcellオブジェクトにはデフォルトで罫線がついてます。top, border, right, leftごとに太さ、色などを制御することができます。逆にいえば、セルを作る時点で罫線のことを考えないと意図した表になりません。まぁこれがメリットになるかデメリットになるかは状況次第っすね。

>savePdf
保存ダイアログを表示して、ファイル名や出力先を指定し、ファイルを保存します。

>formatDateMeasure
ここがFlashの困ったちゃんで、DataGridにDateやNumberを表示する際の書式指定でいちいちコールバック関数などを書かないといけないのです。VisualBASICが懐かしいぜ。

>convertToLocaltime
RESTで帰ってくる日付はGMTなのでこれを日本時間に変換してやります。60000はtimezoneOffsetで取ってくる時差が分単位なのでこれをミリセカンド単位に換算するためです。

>formatDecimal
3桁ごとのカンマ区切り、もっと簡単な方法あったら教えてください^^;


■注意■

今回サンプルとして掲載したソースにはID / Passwordが文字列定数として埋め込まれていますが、実際にFlashで作る場合この方法は避けてください。ダンプしてソースをリバースエンジニアリングすることが可能ですので。

FlashとしてForce.com環境上で使うのであればセッションを引き継いで認証できるのでID / Passwordは不要ですし、AIRでデスクトップアプリとして配布する場合はGUIからID / Passwordを入力してもらいpreferenceとしてPC上に保存すればOKです。


■Flash業界のみなさんへ■

データベースにお悩みではありませんか?

force.com / database.comの世界にどうぞ。database.comなら最高にセキュアでスケーラブルなRESTデータベースが3アカウント, 10万レコード, 月5万トランザクションまで無料で使えます。サーバのセットアップもバックアップも不要です。

お願い:ソースでお気づきの通り私はFlash初心者です。いろいろツッコミどころ満載だと思いますので、ご指導ご鞭撻のほどよろしくお願いいたしますm(_ _)m。


■おまけ■

個人事業としてiPhoneアプリを書いてます。

現職でForce.comに携わる前はWebObjectsというフレームワークで仕事をしていましたが、WO屋から見るとApexはイマイチ美しくありません(もちろんマルチテナントとしての安全性安定性などを確保するための現実的な実装という面もあると思いますが)

そんな私にとってForce/Database/Chatter.comをRESTでアクセスするiOSアプリはほぼ理想郷です。


■最後に■
このブログはForce.com Advent Calendarに参加しています。


■ソース■

というわけでソースです。

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:s="library://ns.adobe.com/flex/spark" 
        xmlns:mx="library://ns.adobe.com/flex/mx"
        xmlns:salesforce="http://www.salesforce.com/"
        applicationComplete="init()" height="560" width="800">
 <fx:Declarations>
  <salesforce:AIRConnection id="force"/>
 </fx:Declarations>

 <fx:Script>
  <![CDATA[
   import com.salesforce.AsyncResponder;
   import com.salesforce.objects.LoginRequest;
   import com.salesforce.results.Fault;
   import com.salesforce.results.LoginResult;
   import com.salesforce.results.QueryResult;
   
   import mx.collections.ArrayCollection;
   import mx.formatters.DateFormatter;
   
   import org.purepdf.Font;
   import org.purepdf.colors.RGBColor;
   import org.purepdf.elements.Element;
   import org.purepdf.elements.Phrase;
   import org.purepdf.elements.RectangleElement;
   import org.purepdf.pdf.PageSize;
   import org.purepdf.pdf.PdfDocument;
   import org.purepdf.pdf.PdfPCell;
   import org.purepdf.pdf.PdfPTable;
   import org.purepdf.pdf.PdfWriter;
   import org.purepdf.pdf.fonts.BaseFont;
   import org.purepdf.pdf.fonts.cmaps.CJKFontResourceFactory;
   import org.purepdf.pdf.fonts.cmaps.CMapResourceFactory;
   import org.purepdf.resources.BuiltinCJKFonts;
   import org.purepdf.resources.CMap;
   import org.purepdf.resources.ICMap;
   import org.purepdf.utils.IProperties;
   import org.purepdf.utils.Properties;

   
   
   [Bindable]
   private var arrayData:ArrayCollection = null;
   private var dateTimeFormatter:DateFormatter = null;
   
   private const MaginLeft:int    = 20;
   private const MarginRight:int  = 20;
   private const MarginTop:int    = 30;
   private const MarginBottom:int = 30;
   
   private function init():void 
   {
    dateTimeFormatter = new DateFormatter();
    dateTimeFormatter.formatString = "YYYY-MM-DD HH:NN:SS";

    var lr:LoginRequest = new LoginRequest();
    status = "login by id/password";
    
    lr.username = "youraccount@your.domain";
    lr.password = "password"+"securitytoken";
    
    lr.callback = new AsyncResponder(loginHandler, faultHandler);
    force.login(lr);
   }  
   
   private function loginHandler(result:LoginResult):void 
   {
    if (result.userInfo != null) {
     status = "login success, query";
     
     force.autoSyncEnabled = false;
     force.doCache = false;
     
     var strQuery:String = "Select DateMeasure__c, High__c, Low__c, Beat__c From BloodPressure__c order by DateMeasure__c desc";
     
     force.query(strQuery, new AsyncResponder(queryHandler, faultHandler));
    }
   }
   
   
   
   private function queryHandler(result:QueryResult):void
   {
    if (result != null && result.records != null && result.records.length > 0) 
    {
     if (arrayData == null)
      arrayData = new ArrayCollection();
      arrayData.addAll(result.records);
    }
    
    if (!result.done)
    {
     trace("query again");
     force.queryMore(result.queryLocator, new AsyncResponder(queryHandler));
    }
    else
    {
     status = "query result";
     force.syncConnection
     
     dataGrid.dataProvider = arrayData;
     
     var byteArray:ByteArray = new ByteArray();
     
     var pdfWriter:PdfWriter =  PdfWriter.create(byteArray, PageSize.A4);
     var document:PdfDocument  =  createPdf(pdfWriter, initPdfFont());
     savePdf(byteArray, "血圧報告.PDF");
     
     document  = null;
     pdfWriter = null;
     byteArray = null;
    }
   }
   
   private function faultHandler(result:Fault):void
   {
    trace(typeof(result));
    trace(result.faultcode);
    trace(result.faultstring);
   }
   
   
   private function initPdfFont() : BaseFont
   {
    var map: ICMap = new CMap( new CMap.UniJIS_UCS2_H() );
    CMapResourceFactory.getInstance().registerCMap( BaseFont.UniJIS_UCS2_H, 
                                                    map );
    
    var prop: IProperties = new Properties();
    prop.load( new BuiltinCJKFonts.HeiseiMin_W3() );
    
    CJKFontResourceFactory.getInstance()
                          .registerProperty(
        BuiltinCJKFonts.getFontName( BuiltinCJKFonts.HeiseiMin_W3 ), prop);
    
    var bf: BaseFont = BaseFont.createFont( 
        BuiltinCJKFonts.getFontName( BuiltinCJKFonts.HeiseiMin_W3 ), 
        BaseFont.UniJIS_UCS2_H, BaseFont.NOT_EMBEDDED, true );
    
    return bf;
   }
   
   private function createPdf(inWriter:PdfWriter, inBf:BaseFont) : PdfDocument 
   {
    var fontHeader:org.purepdf.Font 
        = new org.purepdf.Font( org.purepdf.Font.BOLD, 12, -1, null, inBf );
    var font:org.purepdf.Font       
        = new org.purepdf.Font( -1.   -1,              10, -1, null, inBf );

    var document:PdfDocument = inWriter.pdfDocument;
    document.open();
    document.setMargins(MaginLeft, MarginRight, MarginTop, MarginBottom);
    
    var table:PdfPTable = null;
    var cell:PdfPCell = null;
    
    var lc:int = 99;
    var lpp:int = 40;
    var i:int = 0;
    var n:int = arrayData.length;
    var monthBreak:int = 0;
    
    var vec:Vector.<Number> = new Vector.<Number>;
    vec.push(50);
    vec.push(25);
    vec.push(25);
    vec.push(25);
    
    while (i < n) {
     // 行を取り出し
     var line:Object = arrayData[i++];
     var dateMeasure:Date = convertToLocaltime(line.DateMeasure__c);
     
     // 改ページ
     if (lc >= lpp) {
      if (table != null) {
       document.add(table);
       document.newPage();
       table = null;
      }
      lc = 0;
     }
     
     //ヘッダ
     if (table == null) { 
      table = new PdfPTable(vec);
      table.addCell(cellFromStringFontAndAlignment("測定日時", fontHeader));
      table.addCell(cellFromStringFontAndAlignment("最高血圧", fontHeader));
      table.addCell(cellFromStringFontAndAlignment("最低血圧", fontHeader));
      table.addCell(cellFromStringFontAndAlignment("心拍",     fontHeader));
     }
     
     table.addCell(cellFromStringFontAndAlignment(
                       dateTimeFormatter.format(dateMeasure), font));
     table.addCell(cellFromStringFontAndAlignment(
                       formatDecimal(line.High__c), font, Element.ALIGN_RIGHT));
     table.addCell(cellFromStringFontAndAlignment(
                       formatDecimal(line.Low__c),  font, Element.ALIGN_RIGHT));
     table.addCell(cellFromStringFontAndAlignment(
                       formatDecimal(line.Beat__c), font, Element.ALIGN_RIGHT));
     lc++;
    }
    
    if (table != null) {
     document.add(table);
     table = null;
     
     document.newPage();
    }
    
    document.close();
    
    return document;
   }
   
   private function cellFromStringFontAndAlignment(
                                        inStr:String, 
                                        inFont:org.purepdf.Font, 
                                        inAlignment:int = Element.ALIGN_CENTER)
                                      : PdfPCell {
    var str:String = (inStr == null) ? "" : inStr;
    
    var cell:PdfPCell = PdfPCell.fromPhrase(new Phrase(str, inFont));
    cell.horizontalAlignment = inAlignment; 
    
    return cell;
   }
   
   private function savePdf(inByteArray:ByteArray, inFileName:String) : void
   {
    var fr:FileReference = new FileReference();
    fr.save(inByteArray, inFileName);
   }

   private function formatDateMeasure(inObj:Object, 
                                      inDataGridColumn:DataGridColumn)
                                    : String
   {
    return (inObj != null && inObj.DateMeasure__c != null) 
           ? dateTimeFormatter.format(convertToLocaltime(inObj.DateMeasure__c))
           : "";
   }
   
   private function convertToLocaltime(inStr:String):Date {
    var date:Date = DateFormatter.parseDateString(inStr);
    var time:Number = date.getTime() - date.timezoneOffset * 60000;
    date.setTime(time);
    
    return date;
   }
   
   private function formatDecimal(inDecimal:Number):String
   {
    if (isNaN(inDecimal))
     return "";
    else {
     var pattern:RegExp = /(\d)(?=(\d{3})+(?!\d))/g;
     return String(inDecimal).replace(pattern, "$1,");
    }
   }

  ]]>
 </fx:Script>

 <mx:DataGrid id="dataGrid" top="10" left="23" right="23" bottom="10" 
              editable="false" enabled="false">
  <mx:columns>
   <mx:DataGridColumn headerText="測定日時" dataField="DateMeasure__c" 
                      labelFunction="formatDateMeasure" textAlign="center"/>
   <mx:DataGridColumn headerText="最高血圧" dataField="High__c" 
                      textAlign="center"/>
   <mx:DataGridColumn headerText="最低"  dataField="Low__c"  textAlign="center"/>
   <mx:DataGridColumn headerText="心拍"  dataField="Beat__c" textAlign="center"/>
  </mx:columns>
 </mx:DataGrid>



</s:WindowedApplication>

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。