Webページに表現したツリー図を、以前、HTML2CANVAS で変換して jsPDF でPDF作成をしたのですが、
oboe2uran.hatenablog.com
大きいツリー図になると1ページで入らない場合破綻します。 html2canvas のキャプチャ実行を複数ページに
分割するように何回も実行したのでは、遅すぎて話になりません。
テンプレートとして定義できない「ツリー図」の作成は、PDF帳票ツールでも使えそうなものが見当たりません。
ツリー図、線の描画が無理だと思ってました。。。
落ち着いて考えると手段はありました。そもそもツリー図を書くのは、1つのノードに対して、
ノード要素のネスト(階層)レベルとノードを並べた時に、ツリー図の枝の集合で、枝ツリーの最後にあるのか否かが
把握できれば、ツリー図を書くことができます。
そこで、jsTreeが書いたツリー図から、JSONを抽出してこの階層レベルと枝ツリーの最後にあるのか否かを
Java google GSON で解析抽出すれば、後の面倒くさい、PDFの作成と描画を Python にやらせます。
jsTree の JSON は以下のように取得します
var v = $('#tree').jstree(true).get_json('#', {flat:false}) var jtext = JSON.stringify(v);
これを Java GSON解析に渡します。
jsTree→JSON抽出→Java GSONで解析は、以前、参考になるもので以下を作ってました
jsTree JSON データの変換 - Oboe吹きプログラマの黙示録
解析結果の JstreeNode に、
public int nest = 0; /** icontail = Treeアイコン表現末尾、JstreeNodeTool の parse(String) を実行した結果のみセットされるフラグ */ public boolean icontail = false;
と要素を追加して以下のように gson による解析をします。
public List<JstreeNode> parse(String jsonstr){ List<JstreeNode> list = new ArrayList<>(); StreamSupport.stream(new JsonParser().parse(jsonstr).getAsJsonArray().spliterator(), false) .forEach(e->{ JsonObject jo = e.getAsJsonObject(); parseNode(list, jo, "#", 0); }); // ツリー図出力する Python の為に、icontail boolean セットする list.stream().map(e->{ List<JstreeNode> plist = list.stream() .filter(n->n.parent.equals(e.parent)).collect(Collectors.toList()); if (plist.size() > 0){ JstreeNode j = plist.get(plist.size()-1); if (j.id.equals(e.id)){ e.setIcontail(true); } } return e; }).collect(Collectors.toList()); return list; } private void parseNode(List<JstreeNode> list, JsonObject jo, String parent, int nest){ GenericBuilder<JstreeNode> builder = GenericBuilder.of(JstreeNode::new) .with(JstreeNode::setId, jo.get("id").getAsString()) .with(JstreeNode::setParent, parent) .with(JstreeNode::setIcon, jo.get("icon").getAsString()) .with(JstreeNode::setText, jo.get("text").getAsString()) .with(JstreeNode::setNest, nest); Optional.ofNullable(jo.getAsJsonObject("state")).ifPresent(o->{ builder.with(JstreeNode::setOpened, Optional.ofNullable(o.get("opened")) .map(e->e.getAsBoolean()).orElse(true)); builder.with(JstreeNode::setSelected, Optional.ofNullable(o.get("selected")) .map(e->e.getAsBoolean()).orElse(false)); builder.with(JstreeNode::setDisabled, Optional.ofNullable(o.get("disabled")) .map(e->e.getAsBoolean()).orElse(false)); }); list.add(builder.build()); JsonArray jary = jo.getAsJsonArray("children"); if (jary != null){ final String id = jo.get("id").getAsString(); StreamSupport.stream(jary.spliterator(), false) .forEach(e->parseNode(list, (JsonObject)e, id, nest+1)); } }
解析した結果の JstreeNode リストを、
改善する→「JavaからProcess起動で Python 実行して PDF を作らせる。」 - Oboe吹きプログラマの黙示録
に書いたように、Python 実行としてリストを Python が標準入力で受信するように実行します。
その際、ツリーノードの要素、階層レベル、icontail boolean を1行ずつ、任意の区切り文字で繋げて
改行コードを付けて送信します。最後を 改行2個にしてPython が入力完了できるようにします。
// 上の 解析メソッドです List<JstreeNode> list = tool.parse(treejson); // "%"文字を区切りします。 List<String> jline = list.stream() .map(e->Integer.toString(e.nest)+"%"+e.icontail+"%"+UnicodeTool.convertToUnicode(e.text)+"\n") .collect(Collectors.toList()); // 最後に 改行を追加します。 jline.add("\n"); // Python を実行します。 int sts = ScriptExecutor.run(()->"python /var/tool/treemake_pdf.py /var/template/blank.pdf /var/out.pdf" , ()->jline , e->{ // Python 実行正常終了 },(e, x)->{ logger.warn(x.getMessage(), x); });
# -*- coding: UTF-8 -*- import sys import io import codecs from pdfrw import PdfReader from pdfrw.buildxobj import pagexobj from pdfrw.toreportlab import makerl from reportlab.pdfgen import canvas from reportlab.pdfbase.cidfonts import UnicodeCIDFont from reportlab.pdfbase import pdfmetrics # ネスト線を繋ぐためのディクショナリ key = x , value = y nest_dict = {} # PDF ツリー作成 def create(template, resultfile, list): # テンプレート読込 page = PdfReader(template, decompress=False).pages pp = pagexobj(page[0]) # フォント指定 fontname_g = "HeiseiKakuGo-W5" pdfmetrics.registerFont(UnicodeCIDFont(fontname_g)) # 出力先指定 cc = canvas.Canvas(resultfile) # フォント→ページセット cc.setFont(fontname_g, 12) cc.doForm(makerl(cc, pp)) # font-size:12 → 1page 56 行 row_count = 0 y_pos = 800 prev_nest = 0 cc.drawString(500, 820, "Page %d" % cc.getPageNumber()) for e in list: d = e.split("%") nest = int(d[0]) x_pos = 40 + (12 * nest) if int(d[0]) > 0: x_pos += (14 * nest) # 名称書込み cc.drawString(x_pos, y_pos, d[2].encode().decode('unicode-escape') ) # セル borser書込み cell_border(cc, y_pos, nest, d[1], prev_nest) # 前の行の nest prev_nest = nest #-------------------------------------------- y_pos -= 16 row_count += 1 if y_pos < 40: #===== 改ページ処理 ==================== # ブランチ線の引き継ぎ xlist = [] for key, value in nest_dict.items(): cc.line(key, value, key, 40) xlist.append(key) nest_dict.clear() for nx in xlist: nest_dict[nx] = 820 # PDF Canvas 生成 cc.showPage() cc.doForm(makerl(cc, pp)) cc.setFont(fontname_g, 12) y_pos = 800 row_count = 0 cc.drawString(500, 820, "Page %d" % cc.getPageNumber() ) cc.showPage() cc.save() return # 行 ブランチ線の書込み def cell_border(c, y, nest, tail, prev_nest): x1 = nest * 24 + 28 x2 = x1 + 10 # 横 c.line(x1, y+4, x2, y+4) # 縦 y1 = y + 4 if tail=='true' else y - 2 c.line(x1, y1, x1, y+14) # up or down? if prev_nest > nest: # up if x1 in nest_dict: c.line(x1, nest_dict.get(x1), x1, y + 14) del nest_dict[x1] elif prev_nest==nest: if x1 in nest_dict: del nest_dict[x1] if tail == 'false': nest_dict[x1] = y1 ######################### if __name__ == '__main__': argv = sys.argv argvlen = len(argv) if argvlen != 3: sys.stderr.write('Error: argument [1]=template file [2]=out file required!') exit(1) else: inlist = [] try: while True: inp = input('') if inp == '': break inlist.append(inp) except EOFError: pass print("=======================") print("template = %s" % argv[1]) print("out = %s" % argv[2]) # PDF作成実行 create(argv[1], argv[2], inlist) print("writed !") exit(0)
↑のようなめんどくさい処理を書けないと、最初は思ってました。
落ち着いて階層ネストの処理と、ループ内で実行する線の書き込みを
考えれば案外、簡単にかけるものです。
Python ではなくて、Java で書いたらたいへんだろうなと。。。
Wicket での結果ダウンロードは書込み終わったPDFを投げるだけなので簡単です。
final AJAXDownload download = AJAXDownload.of(out->{ File outpdf = new File("/var/out.pdf"); try(InputStream in = new FileInputStream(outpdf)){ byte[] b = new byte[1024]; int len; while((len=in.read(b, 0, b.length)) >= 0){ out.write(b, 0, len); } out.flush(); out.close(); }catch(Exception ex){ logger.error(ex.getMessage(), ex); }finally{ outpdf.delete(); } }, ()->"application/pdf", ()->"ツリー図.pdf");
これを、AJAXのイベントビヘビアで、コールバックするだけです。