1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【make it easy】「pdf-libとpdfmakeをefwに使ってみる」の問題点と解決方法

Posted at

本記事は以下の記事の続きです。

efw環境に、pdf-libとpdfmakeの試験を2週間続き、いろいろダーティテストをやって、問題点を2箇所に集中しました。

問題1:Promiseはマルチスレッドに向いていない

説明ですが、すべてのPromiseにこのような問題があるかどうかわかりません。自分がテストしたのは、promisejs.orgのpromise-7.0.4.js とそれを参考して作ったnashorn-ext-for-es6のPromise実装です。

問題説明

Promiseには、非同期実行のためsetTimeoutを利用します。
image.png
nashorn-ext-for-es6のsetTimeout実装は、java.util.Timerを利用します。
image.png
その中にはjava.util.TaskQueueを利用されています。taskQueueのqueueは固定サイズ128のtimerTaskの配列です。
image.png
想像してください。マルチスレッドのWEBサーバにとって、128は如何に小さい数字でしょう。
ブラウザーの場合、この制限はいかがでしょう?以下のプログラムでテストしてみました。chrome、firefox、edgeは全部クリアできて問題なさそうです。現在のブラウザーはすごいですね。

<html>
<script>
for(var i=0;i<100000;i++){
	setTimeout(function(){console.log("ssss")},10);;
}
</script>
</html>

また、今回の調査により、setTimeoutの裏側にはqueueがあって、同じ実行時刻順の場合、queueに入る順番は、実行順番になるとわかりました。この特徴はProimseの非同期実行にはとても重要です。つまり、setTimeoutのQueueの制限は厳しいですが、Promise実装にとって論理的に大丈夫です。もしかして、java.util.Timerとjava.util.TaskQueueを代替するクラスを作れば制限問題を解決になるかと思います。この方向は別途試します。

解決方法1:同期方式

PDF作成のイベントjsを同時1つリクエストしか対応しないようにすれば、マルチスレッド未対応の問題を回避できます。ユーザ数が少ない場合、この方法は簡単で分かり易いし全然よいのではないかと思います。nashornのJava.synchronizedを利用して、関数を同期関数を宣言するやり方を利用できます。

解決方法2:Pool利用

ユーザ数が多い場合、同時接続のユーザは全部隊列待ちになって、最後の人は長く待たないといけないです。改善しようとする場合、Poolの仕組みを利用するloadWithGlobalPool関数をつくりました。プールに複数スクリプトコンテキストを用意します。リクエストが来るとき、暇になっているスクリプトコンテキストを使って対応します。暇のスクリプトコンテキストがなければ、しばらくまってまたリトライします。

ソース

test.jsp

ボタンを押す場合、一括で10回送信するようにしています。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="efw" uri="efw" %> 
<!DOCTYPE HTML>
<HTML>
<HEAD>
	<META HTTP-EQUIV="CONTENT-TYPE"CONTENT="TEXT/HTML;CHARSET=UTF-8">       
	<TITLE>efw Output Test</TITLE>
	<!-- Efwクライアントの取り込み-->
	<efw:Client lang="jp"/>
	<script>
	function test1(){
		$("#pdf0")[0].src="";
		$("#pdf1")[0].src="";
		$("#pdf2")[0].src="";
		$("#pdf3")[0].src="";
		$("#pdf4")[0].src="";
		$("#pdf5")[0].src="";
		$("#pdf6")[0].src="";
		$("#pdf7")[0].src="";
		$("#pdf8")[0].src="";
		$("#pdf9")[0].src="";
		Efw('test1',{pdf:'#pdf0'});
		Efw('test1',{pdf:'#pdf1'});
		Efw('test1',{pdf:'#pdf2'});
		Efw('test1',{pdf:'#pdf3'});
		Efw('test1',{pdf:'#pdf4'});
		Efw('test1',{pdf:'#pdf5'});
		Efw('test1',{pdf:'#pdf6'});
		Efw('test1',{pdf:'#pdf7'});
		Efw('test1',{pdf:'#pdf8'});
		Efw('test1',{pdf:'#pdf9'});
	}
	function test2(){
		$("#pdf0")[0].src="";
		$("#pdf1")[0].src="";
		$("#pdf2")[0].src="";
		$("#pdf3")[0].src="";
		$("#pdf4")[0].src="";
		$("#pdf5")[0].src="";
		$("#pdf6")[0].src="";
		$("#pdf7")[0].src="";
		$("#pdf8")[0].src="";
		$("#pdf9")[0].src="";
		Efw('test2',{pdf:'#pdf0'});
		Efw('test2',{pdf:'#pdf1'});
		Efw('test2',{pdf:'#pdf2'});
		Efw('test2',{pdf:'#pdf3'});
		Efw('test2',{pdf:'#pdf4'});
		Efw('test2',{pdf:'#pdf5'});
		Efw('test2',{pdf:'#pdf6'});
		Efw('test2',{pdf:'#pdf7'});
		Efw('test2',{pdf:'#pdf8'});
		Efw('test2',{pdf:'#pdf9'});
	}
	</script>
</HEAD>
<BODY>
<button onclick="test1();">テスト1</button>
<button onclick="test2();">テスト2</button><br>
<iframe id="pdf0" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf1" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf2" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf3" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf4" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf5" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf6" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf7" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf8" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf9" src="" style="width:400px;height:300px"></iframe>
</BODY>
</HTML>

global.js

フォントのembedderを作成しておいて、同期のtest1.jsにそれを利用します。test2.jsにはpoolを利用するから、別スクリプトコンテキストのオブジェクトを利用できません。それで、フォントのバイト配列を予め用意して、poolのそれぞれの

var global={};
var bytesIPAexgothic=null;
var embedderIPAexgothic=null;
global.fire=function(){
	load(_eventfolder+"/pdf-lib.min.js");
	load(_eventfolder+"/fontkit.umd.min.js");
	embedderIPAexgothic=await(PDFLib.CustomFontSubsetEmbedder.for(
		fontkit, 
		byteToUint8Array(bytesIPAexgothic=file.readAllBytes("font/ipaexg00401.ttf")), 
		"IPAexゴシック", 
		{}
		)
	);
	efw.register("PDFLib");
	efw.register("fontkit");
	efw.register("bytesIPAexgothic");
	efw.register("embedderIPAexgothic");
	function byteToUint8Array(byteArray) {
		var uint8Array = new Uint8Array(byteArray.length);
		uint8Array.set(Java.from(byteArray));
		return uint8Array;
	}
}

test1.js

Java.synchronizedのロックする対象はtest1に設定しています。任意のグローバル領域のオブジェクトで大丈夫です。例えば、ObjectなどでもOKです。

var test1={};
test1.paramsFormat={
	pdf:null
};
test1.fire=Java.synchronized(function(params){
	var pdfDataUri;
	var pdfDoc=await(PDFLib.PDFDocument.create());
	pdfDoc.registerFontkit(fontkit);
	var myfont=PDFLib.PDFFont.of(pdfDoc.context.nextRef(), pdfDoc, embedderIPAexgothic);
	pdfDoc.fonts.push(myfont);
	var page = pdfDoc.addPage([500, 400]);
	page.setFont(myfont);
	page.moveTo(10, 200);
	page.drawText('Hello World!こんにちは東京');
	pdfDataUri=await(pdfDoc.saveAsBase64({ dataUri: true }));
	return new Result().eval("$('"+params.pdf+"')[0].src='" +pdfDataUri +"'");
},test1);

test2.js

ES2015のテンプレートの書き方を利用しました。前述のようにpool外のものをpool内のスクリプトコンテキストに渡す場合制限があります。単純なものは大丈夫です。プロトタイプありのものはだめです。渡したいオブジェクトのプロトタイプはpool外で定義されて、pool内のスクリプトコンテキストに同名のプロトタイプがあるが、別物ですから、認識されないです。test2.jsにパラメータとして、渡すbytesIPAexgothicはバイト配列です。

var test2={};
test2.paramsFormat={
	pdf:null
};
test2.fire=function(params){
	var initscript=`
		load(_eventfolder+"/pdf-lib.min.js");
		load(_eventfolder+"/fontkit.umd.min.js");
		var embedderIPAexgothic=await(PDFLib.CustomFontSubsetEmbedder.for(
			fontkit, 
			byteToUint8Array(bytesIPAexgothic), 
			"IPAexゴシック", 
			{}
			)
		);
		function byteToUint8Array(byteArray) {
			var uint8Array = new Uint8Array(byteArray.length);
			uint8Array.set(Java.from(byteArray));
			return uint8Array;
		}
	`;
	var runscript=`
		var pdfDataUri;
		var pdfDoc=await(PDFLib.PDFDocument.create());
		var myfont=PDFLib.PDFFont.of(pdfDoc.context.nextRef(), pdfDoc, embedderIPAexgothic);
		pdfDoc.fonts.push(myfont);
		var page = pdfDoc.addPage([500, 400]);
		page.setFont(myfont);
		page.moveTo(10, 200);
		page.drawText('Hello World!こんにちは東京');
		pdfDataUri=await(pdfDoc.saveAsBase64({ dataUri: true }));
		pdfDataUri;
	`;
	var pdfDataUri=loadWithGlobalPool({
		name:"pdf-lib",
		max:10,
		initializer:initscript,
		script:runscript,
		context:{bytesIPAexgothic:bytesIPAexgothic}
	});
	return new Result().eval("$('"+params.pdf+"')[0].src='" +pdfDataUri +"'");
	
}

効果

image.png

test1.jsは、順番ずつでそれぞれの枠が表示されます。全部表示されるまで7秒です。test2.jsの場合、初回16秒です。次回は4秒です。プールの効果はよく見えます。

想定する利用場面

pdf-libは座標定義方式で面倒くさいですが、既存PDFファイルに穴埋め方式で利用すれば便利だと思います。例えば、領収書印刷など、予めワードとか綺麗なレイアウトを作成してPDFに出力して、それをテンプレートとして利用します。穴の座標を測量してpdf-libのプログラムすれば簡単に実用可能のものを作れます。

ブラウザーのままでpdf-libを利用する場合、数MBのフォントクライアントにダウンロードしなければなりません。また、クライアントjavascriptだったら基本的にソースが見えます。証券番号など重要な書類の場合、作成ソースを公開されると偽装・詐欺とか出てきて危ないですね。

注意点

pdf-libのカスタマイズフォント利用の標準方法は以下です。PDFDocument.embedFontにバイト配列からembedderを作成しています。だが、このembedderは自動的にゴミ回収されないみたいで、大抵100回実行したら、メモリリークが発生します。原因はまだ特定できていません。類似するように、画像の取り扱いも要注意でしょう。
前述今回のサンプルの場合、test1とtest2ともに、embedderを一回作成してずっと利用するからメモリリークを回避しています。

var pdfDoc=await(PDFLib.PDFDocument.create());
pdfDoc.registerFontkit(fontkit);
var myfont = await(pdfDoc.embedFont(fontBytes["IPAexゴシック"],{ subset: true }));
var page = pdfDoc.addPage([500, 400]);
page.setFont(myfont);
page.moveTo(110, 200);
page.drawText('Hello World!こんにちは東京');
var pdfDataUri=await(pdfDoc.saveAsBase64({ dataUri: true }));

問題2:pdfmakeのメモリ食い過ぎ現象

問題説明

上記pdf-libのサンプルのようにpdfmakeに対して類似サンプルを作りました。だが自分のマシンでpoolサイズを3以上設定すると、初期化失敗が発生します。pdf-lib.min.jsは513KBです。pdfmake.min.js+vfs_fonts.min.jsは2.05MBです。pool方式の場合、スクリプトコンテキストごとにロードするjsライブラリは同じJVMの中に共存するから、静的領域不足になる恐れでしょうかと思います。

解決方法

なし。

と言いたいですが、実はあります。JVMの外のものを利用すればいけるでしょう。例えば、v8とQuickjsです。chromeはv8を利用するから、chromeを10枚のタブで同じpdfmakeのサンプルを開いてpdfを作成することはできるでしょう。

後日また試験再開します。

今回のサンプルは、以下のフォルダにおいています。
hello-pdf-lib2とhello-pdfmake2

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?