[備忘録]アプリ上でpdfMakeにて日本語PDF生成(swift編)


・目的
haruを使わずに日本語PDFを出力したい。(日本語をテキストとして出力したい)
できれば、android側もkotolinで書いて移植しやすいよーにしたい。

・前準備
※Mac上で実行したが、多分、Windowsでも問題ない(と思いたい)

1.
bower,gruntをインストール
(nodeはインストールされている事前提)

sudo npm install -g grunt-cli
sudo npm install -g bower

bowerいらんかも。。。orz

2.
githubからソースダウンロードし解凍
https://github.com/bpampuch/pdfmake

3.解凍したフォルダにもぐり、ライブラリインストール

npm install grunt-text-replace grunt-browserify grunt-contrib-uglify grunt-dump-dir grunt-contrib-concat
npm install runt-mocha-cov grunt-jsdoc runt-contrib-jshint

4.
フリーのTrueTypeのフォントを落としてくる
とりあえず、ここから。
http://ipafont.ipa.go.jp/
で、解凍。

5.
「2.」で解凍したフォルダの examples/fontsフォルダ下の、Roboto*.ttfファイルをすべて消し、
「4.」で解凍したttfファイルを置く
(明朝を使うので、とりあえず明朝の方)

6.
「2.」で解凍したフォルダ下で、コマンド実行

grunt dump_dir

build下の「vfs_fonts.js」が、解凍したフォント用に置きかわる。

前準備終わり。

・iOS側実装

1.
xcodeでシングルビューのswiftのプロジェクト作成

2.
pdfをJSで生成するhtmlを準備

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <button id="btn_gen_pdf" type="button">日本語PDF出力</button><br/>
    <div style="display:none;">

        <!--
         nativeactionとかいう、テキトーなプロトコルをつけ、
         UIWebviewにてhandleする。
         メソッドはPDFがエンコードされてダラダラ長いのでPostとする。。。
         -->
        <form id="frm_generate_pdf" method="post" action="nativeaction://generated_pdf">
            <input type="hidden" name="enc_pdf" id="enc_pdf" value=""/>

            <!-- 以下パラメータはいらんけど、とりあえず、
             postのパラメータテストの為に付加した。。。 -->
            <input type="hidden" name="fuga"  value="tttt"/>
            <input type="hidden" name="ggg"  value=""/>
        </form>
    </div>
    <script src="./jquery.min.js"></script>
    <script src="./pdfmake.js"></script>
    <script src="./vfs_fonts.js"></script>
    <script>


        var docDefinition;

        // 初期化
        $(function(){

          pdfMake.fonts = {
            tekito_font: {
            normal: 'ipaexm.ttf'
            }
          };
          $("#btn_gen_pdf").on("click",function(){
                               genPdf();
                               return false;});

          docDefinition = {
            content: [
                      {text: 'This is an sample PDF printed with pdfMake. 日本語のテスト',
                       style: 'tekito_style'
            }],
            styles: {
                tekito_style: {
                fontSize: 25
                }
            },
            defaultStyle: {font: 'tekito_font'}
          };
        });

        // PDF生成
        function genPdf() {
            try {

                var _hoge = pdfMake.createPdf(docDefinition);

                // pdfMake.createPdf().open()は
                // mobileSafariでは実行されない
                // なので、Base64でエンコードされた文字列を
                // フォームでpostする(多分、Getはダメ。。。)
                //
                // getDataUrl(function)はopenの中でやってるメソッド。
                // pdfをエンコードして文字列にしてる
                _hoge.getDataUrl(function(result) {

                    // 「data:application/pdf;base64,」は
                    // ios上で正規表現使って置換するのは面倒くさいので、ここで除去。。。
                    $("#enc_pdf").val(result.replace(/^data:application\/pdf;base64,/, ''));
                    $('#frm_generate_pdf').submit();
                });
            } catch(e) {
                alert(e);
            }
        }

        // PDF表示
        function handleCreatePdfFromiOS(pdfPath) {
            // めんどくさいので、webviewからjavascriptキック
            location.href = pdfPath;
        }
    </script>

  </body>
</html>



プロジェクトにてきとーにフォルダを掘り、コピー
(jsファイルもコピー(「vfs_fonts.js」は、前準備で作ったフォントファイル))

3.
ストリーボード上のVCのビューにWebView貼り付けて、
constraint全画面にし、vcのソースに紐付け

4.
VCのソースをこんな感じで。。。

import UIKit

// UIWebViewDelegeteでリクエストをキャプチャする
class ViewController: UIViewController,UIWebViewDelegate {

    @IBOutlet weak var wb: UIWebView!

    // リクエストハンドラ
    let nativeReqHandler = NativeRequestHandler()


    // 起動画面
    var targetURL = NSBundle.mainBundle().pathForResource("hoge", ofType: "html");

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)

        // リクエストをキャプチャするのでWebViewのデリゲート設定
        wb.delegate = self

        // 起動画面をリソースに。。。
        let requestURL = NSURL(string: targetURL!)
        let req = NSURLRequest(URL: requestURL!)
        wb.loadRequest(req)

    }


    // まあ、どーでもいい
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // まあ、どーでもいい
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    // WebViewのデリゲートメソッド
    func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {

         // 何かリクエスト来たら 
        // リクエストハンドラに処理を委譲
        // プロトコルで「nativeaction」が来たら、
        // 処理をswift側でやる。
        return self.nativeReqHandler.handleRequest(webView, request: request, naviTp: navigationType)
    }

    func webViewDidFinishLoad(webView: UIWebView) {
        // shouldStartLoadWithRequestでfalseの場合はこのイベントは起きない
//        webView.request!.URL
//        print("*** load completed to:\(webView.request!.URL!)")
    }


}


5.
WebViewのリクストキャプチャするクラスをこんな感じで。。。

import UIKit

class NativeRequestHandler {

    func handleRequest(webView: UIWebView, request: NSURLRequest, naviTp: UIWebViewNavigationType) -> Bool {

        let strUrl = request.mainDocumentURL!.absoluteString
        print("start handle request:\(strUrl)")

        if isNativeRequest(request) {
            // Native処理の場合
            handleNativeRequest(webView, request: request, naviTp: naviTp)
            // 画面遷移なし
            return false
        }

        // その他はコンテンツを画面遷移するようにtrueを返す
        return true
    }

    // nativeaction判定
    func isNativeRequest(request: NSURLRequest) -> Bool {
        // 適当。。。
        if request.URL!.scheme.caseInsensitiveCompare("nativeaction") == NSComparisonResult.OrderedSame ||
            request.URL!.scheme.caseInsensitiveCompare("data") == NSComparisonResult.OrderedSame{
            return true
        }
        return false
    }


    // Getパラメータパース
    func parseQueryString(request: NSURLRequest) -> [String:String] {
        let q = request.URL!.query
        if q == nil {
            let empty: [String:String] = [:]
            return empty
        }

        let prms = request.URL!.query!.componentsSeparatedByString("&")
        var params: [String:String] = [:]

        for sprm: String in prms {
            let prms = sprm.componentsSeparatedByString("=")
            params[prms[0]] = prms[1]
        }
        return params
    }



    // Getパラメータパース
    func parseGetParams(request: NSURLRequest) -> NSDictionary {

        let strUrl = request.mainDocumentURL!.absoluteString
        let urlAttr = strUrl.characters.split{$0 == "?"}.map(String.init)
        let urlParamsStr: String = urlAttr[1]

        let urlParamStrs = urlParamsStr.characters.split{$0 == "&"}.map(String.init)

        let results = NSMutableDictionary()

        for urlParamStr: String in urlParamStrs {

            let keyVal = urlParamStr.characters.split{$0 == "="}.map(String.init)
            results[keyVal[0]] = keyVal[1]
        }

        return NSDictionary(dictionary: results)
    }

    // JSからのデバッグ出力
    func handleDebugLogFromJS(webView: UIWebView, request: NSURLRequest) {

        let strUrl = request.mainDocumentURL!.absoluteString
        let urlAttr = strUrl.characters.split{$0 == "?"}.map(String.init)
        let urlParamsStr: String = urlAttr[1]
        let encMsg = urlParamsStr.stringByReplacingOccurrencesOfString("message=", withString: "")

        let data = NSData(base64EncodedString: encMsg, options: NSDataBase64DecodingOptions.IgnoreUnknownCharacters)
        let logMsg = NSString(data: data!, encoding: NSUTF8StringEncoding) as! String

        print(logMsg)
    }


    // Post判定
    func isPostMethod(request: NSURLRequest) -> Bool {
        let m = request.HTTPMethod
        if (m != nil) {
            if m! == "POST" {
                return true;
            }
        }
        return false
    }




    // アプリ処理呼び出しハンドラ
    func handleNativeRequest(webView: UIWebView, request: NSURLRequest, naviTp: UIWebViewNavigationType) {

        let strUrl = request.mainDocumentURL!.absoluteString

        var pMap = Dictionary<String,String>()
        let method = request.HTTPMethod
        print("method:[\(method)]")

        // BodyからPostパラメータを取り出す
        let data = request.HTTPBody;
        if (data != nil) {
            let bodyStr = NSString(data: data!, encoding: NSUTF8StringEncoding)! as String!

            print("==============")
            let params = bodyStr.characters.split{$0 == "&"}.map(String.init)
            for param in params {
                let pKv  = param.characters.split{$0 == "="}.map(String.init)


                if pKv.count == 1 {
                    pMap[pKv[0]] = ""

                } else {
                    // Postパラメータはエンコードされてるので、デコード
                    var v = pKv[1]
                    let decodeS = (v as NSString!).stringByRemovingPercentEncoding
                    if decodeS != nil {
                        v = decodeS as String!
                    }
                    pMap[pKv[0]] = v
                }
            }
            for (k,v) in pMap {
                print("[\(k)]=[\(v)]")
            }
        }

        // PDF出力指示の場合
        // ここでやる処理じゃないけど、忘備録なので、とりあえず、ここで書く。。。
        if strUrl.hasPrefix("nativeaction://generated_pdf") {
            if pMap["enc_pdf"] != nil {
                let decodedData = NSData(base64EncodedString: pMap["enc_pdf"]!, options: NSDataBase64DecodingOptions())
                if decodedData != nil {

                    // Documentディレクトリを取得
                    let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
                    // ファイル名
                    let fileName = "/ggg.pdf"
                    // 保存する場所
                    let filePath = documentsPath + fileName

//                    print(filePath)

                    decodedData!.writeToFile(filePath, atomically: true)
//                    print("decode")

                    // 画面上のjavascriptをキックし、ローカルに出力したPDFを表示。 
                    // location.hrefしてるだけだが。。。
                    webView.stringByEvaluatingJavaScriptFromString("handleCreatePdfFromiOS('file://\(filePath)');")

                }
            }
        } else {
        // どうしようか。。。
        }
    }

}

いらない処理がたくさん入ってるけど、とりあえずこんな感じで。。。

6.
実行してみる。。。

ボタンがかぶってるけど、気にしない。。
で、ボタンを押してみる。

日本語で、PDFが出力された。

・TODO、その他。
1.
PDFをJS作る時、ちょっと考えるので、お待ちくださいが必要。
2.
作った結果をBase64エンコードするので、大きいサイズだとどうなるか。
3.
画像を使う場合、画像データはパスで指定できない。Base64でエンコードしたものを
jsonに設定する。「2.」に関連して、サイズが、どしても、大きくなるのでどーしたものか。。。
でも、テキストベースの軽いPDFならこれで、haruを導入しなくてよいし、
kotolinとあるてーど共通化出来るのでとても助かる。。。