jsTree→JSON抽出→Java GSONで解析→PythonでPDF作成→Wicket でPDFダウンロード

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);
});

ツリー図を書くPython スクリプト

# -*- 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のイベントビヘビアで、コールバックするだけです。