画像切取り、crop のプレビュー

Webページ上で画像切取り crop を行う時のプレビュー

https://github.com/fengyuanchen/cropperjs
を元に jQuery 用の Cropper
https://github.com/fengyuanchen/jquery-cropper
これを使用した時の プレビューについて、
crop 実行のオプションで任意の場所に、プレビューで表示するのだが。。
f:id:posturan:20190111194133j:plain
HTML

<div class="target><img id="image" src="flower2.jpg" id="trimed_image" ></div>
<div class="img-preview"></div>

CSS

.target img{
  max-width: 100%;
}
.img-preview{
  overflow: hidden;
  height: 100px;
  width: 100px;
  border: 1px solid #000000;
}
.img-preview > img{
  max-width: 100%;
}

cropper.css
cropper.min.js
jquery-cropper.min.js

を使用して、jQuery は、cropper({}) で、previewを指定する。

$('#image').cropper({
     aspectRatio: 1 / 1,
     preview: '.img-preview'
}):

このように、class="img-preview" の部分にプレビューが表示されるが、
あくまでも、プレビューであり実際に切り取った画像イメージではない。
プレビュー部分で右クリック対象を画像保存すると、元の画像で保存される。
切り取ったイメージの画像を保存したければ、croppper()メソッドを実行させた結果から
canvas などに一度描画させて保存するしかない。

<canvas id="canvas"></canvas>

croppper()メソッドを実行

var x1 = 0;
var y1 = 0;
var width = 0;
var height = 0;
var result;

$('#image').cropper({
   aspectRatio: 1 / 1,
   preview: '.img-preview',
   crop: function(event){
      x1 = event.detail.x;
      y1 = event.detail.y;
      width = event.detail.width;
      height = event.detail.height;
   }
});

canvasへの書込み

var canvas = $('#canvas');
const canvasCtx = document.getElementById('canvas').getContext('2d');
canvasCtx.clearRect(0, 0, canvas.width(), canvas.height()); // 描画前にクリア
canvas.css('width', width );
canvas.css('height', height );
canvasCtx.drawImage(document.getElementById("image"), x1, y1, width, height, 0, 0, width, height);

でも、この canvasへの書込みは、ブラウザ表示では、height が2倍になってしまいます。
それに、canvas に、drawImage で描画しても小さくなってしまい役に立たない。
だから、cropper の preview 属性の指定で表示するプレビューは良いのだけど
切り取った画像ではないのが残念

Bootstrap モーダルをドラッグ移動可能にする

Bootstrap モーダルをドラッグ移動可能にするには、やはり、jQuery UI を使うのが簡単

<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>

id=getCroppedCanvasModal が、BootStrap のモーダルエリアなら、、

  $("#getCroppedCanvasModal").draggable({ cursor: "move" });

昨日、悩んだのは、jQuery UI を使わないでやる方法が面倒だったから。
oboe2uran.hatenablog.com

jquery-cropper を使ってみる。

画像のcrop(切取)を、GitHub - fengyuanchen/jquery-cropper: A jQuery plugin wrapper for Cropper.js.
を使って必要最低限の機能を試してみる。
HTMLページは以下のような画面レイアウト
f:id:posturan:20190107222911j:plain
BootStrap を使う。アイコンには fontawesome を使う。
HTMLのヘッダは、以下のようにする。
必要なのは、、
cropper.min.css
cropper.min.js
jquery-cropper.min.js

<meta charset="UTF-8">
<meta http-equiv="content-language" content="ja">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>cropper.html</title>
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" >
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet"/>
<link href="css/cropper.min.css" rel="stylesheet">
<link href="sample.css" rel="stylesheet">
<script src="../js/jquery-3.3.1.min.js" type="text/javascript"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
<script src="js/cropper.min.js" type="text/javascript"></script>
<script src="js/jquery-cropper.min.js" type="text/javascript"></script>
<script src="sample.js" type="text/javascript"></script>

HTMLコンテンツ部分、
イメージタグ img wicket:id="img" id="img" src="picture.jpg" は、Wicket 使用した場合の画像初期表示で
Java Page クラスで適切に画像リソースを割り当てる。静的ページなら、wicket:id 不要で id属性のみ
div class="docs-data" は、敢えて非表示になるように、CSSで設定する。
切り取りダイアログは、BootStrap のモーダルダイアログである。
div class="modal fade docs-cropped" id="getCroppedCanvasModal"
モーダルダイアログ表示サイズは、切取りサイズによって変わってしまうので
ある程度レスポンシブ表示であることが要求されるので、BootStrap のモーダルダイアログを
使用するのは、良いセンスである。
でもBootStrap のモーダルダイアログは、表示したダイアログをドラッグして動かせそうもないので、
(やり方があるのかもしれないが、まだ良くわからない)
Bootstrap モーダルをドラッグ移動可能にする - Oboe吹きプログラマの黙示録

困るのだが、そもそも画面を表示して切取り(CROP)を行うこと自体、
大きい画像に対して切り取るケースがよく想定されるであろう。ドラッグして隠れた部分を再確認、
見比べるにもドラッグ可能領域も狭いことが想定されてドラッグの意味もあまりないかもしれない。

<div class="container nav docs-buttons">
   <div>
      <button id="resetcrop" type="button" class="btn btn-primary">reset</button>
   </div>
   <div>
      <button id="setcrop" type="button" class="btn btn-primary">crop</button>
   </div>
   <div>
      <button id="getData" type="button" class="btn btn-primary" disabled="disabled">Get</button>
   </div>
   <div class="docs-toggles btn-group d-flex flex-nowrap" data-toggle="buttons">
      <label class="btn btn-primary active">
         <input type="radio" name="aspectRatio"
             value="1.7777777777777777" class="sr-only">
         <span> 16:9 </span>
      </label>
      <label class="btn btn-primary">
         <input type="radio" name="aspectRatio"
             value="1.3333333333333333" class="sr-only">
         <span> 4:3 </span>
      </label>
      <label class="btn btn-primary">
         <input type="radio" name="aspectRatio" value="1" class="sr-only">
         <span> 1:1 </span>
      </label>
      <label class="btn btn-primary">
         <input type="radio" name="aspectRatio"
             value="0.6666666666666666" class="sr-only">
         <span> 2:3 </span>
      </label>
      <label class="btn btn-primary">
         <input type="radio" name="aspectRatio" value="NaN" class="sr-only">
         <span> Free </span>
      </label>
   </div>
   <div>
      <label class="btn btn-primary btn-upload"
             for="inputImage" title="Upload image file">
         <input type="file" class="sr-only" id="inputImage"
             name="file" accept=".jpg,.jpeg,.png,.gif,.bmp,.tiff">
         <i class="fa fa-upload"></i>
      </label>
   </div>
</div>
<div class="img-target">
   <img wicket:id="img" id="img" src="picture.jpg">
</div>
<!-- ============= modal ============= -->
<div class="modal fade docs-cropped" id="getCroppedCanvasModal" 
aria-hidden="true" 
aria-labelledby="getCroppedCanvasTitle" role="dialog" tabindex="-1">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="getCroppedCanvasTitle">Cropped</h5>
        <button type="button" class="close" 
         data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" 
         data-dismiss="modal">Close</button>
        <a class="btn btn-primary" id="download" 
href="javascript:void(0);" download="cropped.jpg">Download</a>
      </div>
    </div>
  </div>
</div>
<!-- ===============================  -->
<div class="docs-data">
    <input type="text" id="dataX">
    <input type="text" id="dataY">
    <input type="text" id="dataWidth">
    <input type="text" id="dataHeight">
    <input type="text" id="dataRotate">
    <input type="text" id="dataScaleX">
    <input type="text" id="dataScaleY">
</div>

次に示す JSソース(jquery-cropper 配布元サンプルを流用)を見てのとおり、
crop 表示、切り取りダイアログ表示用に div class="nav docs-buttons"元のソースでは、 配下のHTMLタグ属性に、jquery-cropper の制御属性を書くのが、ミソであった。
でも、私は HTMLにロジックそのものが書かれるのは嫌なので、getData は別にした。
aspect 比率変更が前のソースのままだ。
sample.js ソース

$(function(){
   'use strict';
   var URL = window.URL || window.webkitURL;
   var $image = $('#img');
   var $download = $('#download');
   var $dataX = $('#dataX');
   var $dataY = $('#dataY');
   var $dataHeight = $('#dataHeight');
   var $dataWidth = $('#dataWidth');
   var $dataRotate = $('#dataRotate');
   var $dataScaleX = $('#dataScaleX');
   var $dataScaleY = $('#dataScaleY');
   var uploadedImageType = 'image/jpeg';
   var uploadedImageName = 'cropped.jpg';
   var options = {
      aspectRatio : 16 / 9,
      //viewMode: 3,  // viewMode 無しで端を含められる
      crop : function(e){
         $dataX.val(Math.round(e.detail.x));
         $dataY.val(Math.round(e.detail.y));
         $dataHeight.val(Math.round(e.detail.height));
         $dataWidth.val(Math.round(e.detail.width));
         $dataRotate.val(e.detail.rotate);
         $dataScaleX.val(e.detail.scaleX);
         $dataScaleY.val(e.detail.scaleY);
      }
   };
   var result;
   var uploadedImageURL;

   $('#setcrop').click(function(){
      $image.cropper(options);
      $('#getData').prop('disabled', false);
   });

   $('#resetcrop').click(function(){
      $image.cropper('destroy');
      $(".docs-toggles label:nth-child(1) input[name='aspectRatio']").trigger('click');
      options['aspectRatio'] = 1.7777777777777777
      $('#getData').prop('disabled', true);
   });
   // aspect 比率変更
   $('.docs-toggles').on('change', 'input', function(){
      var $this = $(this);
      var name = $this.attr('name');
      var type = $this.prop('type');
      var cropBoxData;
      var canvasData;
      if (!$image.data('cropper')){
         return;
      }
      if(type === 'radio'){
         options[name] = $this.val();
      }
      $image.cropper('destroy').cropper(options);
   });

   $('#getData').click(function(){
      if ($(this).prop('disabled') || $(this).hasClass('disabled')){
         return;
      }
      result = $image.cropper('getCroppedCanvas');
      // Bootstrap's Modal
      $('#getCroppedCanvasModal').modal().find('.modal-body').html(result);
      if (!$download.hasClass('disabled')){
         download.download = uploadedImageName;
         $download.attr('href', result.toDataURL(uploadedImageType));
      }
   });

   // Import image
   var $inputImage = $('#inputImage');
   $inputImage.change(function(){
      var files = this.files;
      var file;
      if (files && files.length){
         file = files[0];
         if (/^image\/\w+$/.test(file.type)){
            uploadedImageName = file.name;
            uploadedImageType = file.type;
            if (uploadedImageURL){
               URL.revokeObjectURL(uploadedImageURL);
            }
            uploadedImageURL = URL.createObjectURL(file);
            $image.cropper('destroy').attr('src', uploadedImageURL).cropper(options);
            $inputImage.val('');
         }else{
            window.alert('Please choose an image file.');
         }
      }
   });
   // モーダルドラッグ可能にする
   $("#getCroppedCanvasModal").draggable({ cursor: "move" });
});

CSSソース

@charset "UTF-8";

.nav{
   display: -webkit-flex;
   display: -moz-flex;
   display: -ms-flex;
   display: -o-flex;
   display: flex;
}
.nav div{ margin: 10px; }
.docs-data{
   width: 300px;
}

/****************/
.img-target{
   width: 800px;
   height: 400px;
}
img{
   width: 100%;
   height: 100%;
}
.docs-cropped .modal-body > canvas {
  max-width: 100%;
}

/* x,y,scale,width,height 値を非表示にする */
.docs-data{
   display: none;
}

切り取り実行、「Get」ボタンを押した時に以下のように
モーダルダイアログが表示される。
f:id:posturan:20190107225556j:plain

(注意)
jquery-cropper は、IE11 では動かない。
静的に作って、file::/ ~ で試す場合、最初に、 img タグ指定した初期表示では
DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
は、避けられない。ローカルPCでなくて、やはりサーバに置いて、http or https プロトコル
表示しないとならない。
静的に作って、file::/ ~ で試す場合でも、上に書いた // Import image 以下の
input type=file で画像を読み込ませる処理が走れば、HTMLの imgタグも、BLOBオブジェクトでDOMが生成されて

<img id="img" src="blob:null/225c8363-4b91-43ed-b36e-6b2b016a8470" class="cropper-hidden">

CORS の問題、キャンバスの汚染なく無事、切取り画像を downloadボタンに href にセットできる。

画像トリミング jQuery など

あまり画像処理のことは詳しくない。押し迫って着手することも少なかったせいか、
疎い方だ。
いいかげん気になってたので、少し触っていこうか。。。
github.com

昔、Windows PC に、GIMP を入れて使ったりしてたことあったけど、
https://github.com/oliver-moran/jimp

なんてあるので、笑った。

Java → Python プロセス起動の可能性

去年書いた
yipuran-core/ScriptExecutor.java at master · yipuran/yipuran-core · GitHub
このメソッド
public static int runStream(Supplier<String>, Supplier<Collection<String>>
, Consumer<InputStream>, BiConsumer<String, Throwable>)

public static int runStream(Supplier<String>, Consumer<InputStream>, BiConsumer<String, Throwable>)

このプロセス起動、プロセスの標準出力を Consumer<InputStream>で受信した処理をラムダ式で、
そしてプロセスが要求する入力を Supplier<Collection<String>> ラムダ式で与えるという方法は
いろんな拡がり可能性があるようだ。
PythonでPDF作成→Javaで受け取り→Wicket AJAXダウンロード - Oboe吹きプログラマの黙示録

改善する→「JavaからProcess起動で Python 実行して PDF を作らせる。」 - Oboe吹きプログラマの黙示録

JavaからProcess起動で Python 実行して PDF を作らせる。 - Oboe吹きプログラマの黙示録

↑ここまでは、単に Python にPDFを作らせる仕事をさせてそれを受け取るのだが、
画像系の処理でも同じことができるはずだ。

QRコード、バーコードを作成するくらいだったら、すでに Java の世界でも
https://github.com/zxing/zxing を使えば、Python使わなくても良いだろう。
でも、画像加工処理となってくると、ちょっとJavaじゃ辛い。良いのが見当たらないのだ。

前の投稿でも書いた Python に処理させて結果をバイナリの標準出力させれば、
Consumer<InputStream> での処理が成立する。

import sys
import io

io.BytesIO() オブジェクトに求めたい結果を出力して
getvalue() → sys.stdout.buffer.write でバイト単位出力
ができれば良いのである。

Python PILLOW 画像加工を処理するサンプル

Java Wicket で作成する Webページで画像加工前の画像をHTML img タグで表示と
Python PILLOW による簡単な加工結果を同様のHTML img タグで表示させる
(...同様のHTML img タグで表示 → 何、訳わからんことを!と思うかもしれないが
 そこが、Java Wicket を使って可能な面白さのところです。)

Pythonのコード:img_convert.jpg
引数で加工する画像ファイルPATHを受け取って、
白黒変換、ガウシアンブラー(ガウスぼかし)、リサイズします

# -*- coding: UTF-8 -*-
from PIL import Image, ImageFilter
import io
import sys

argv = sys.argv
img = Image.open(argv[1])

img2 = img.convert('L').filter(ImageFilter.GaussianBlur()).resize((120, 120),Image.LANCZOS)

po = io.BytesIO();
img2.save(po, format='JPEG')
sys.stdout.buffer.write(po.getvalue())

↑ format= の指定がないとダメです。

Webページ表示する HTMLソースです。wicket:id タグの方が加工前で
加工後は、WicketSharedResource で取得して表示するので、wicket:id が有りません。

<!-- 加工前の画像 -->
<div>
   <img wicket:id="lena" alt="lena" style="width: 120px;height: 120px">
</div>
<!-- 加工後の画像 -->
<div>
   <img src="lenaConvertImage">
</div>

Wicket
https://github.com/yipuran/yipuran-core/blob/master/src/main/java/org/yipuran/util/process/ScriptExecutor.java
を使用して書く Webページ Java です。
以下をPageクラスのコンストラクタ内で実行します。
sdir_path + "/img_convert.py" が、実行するPythonの指定で、getimagePath()が加工対象画像ファイルのパスです。

queue(new Image("lena", new PackageResourceReference(SamplePage.class, "lena.png")));

int sts = ScriptExecutor.runStream(()->"python " + sdir_path + "/img_convert.py " + getimagePath()
, inst->{
   try(ByteArrayOutputStream out = new ByteArrayOutputStream()){
      inst.transferTo(out);
      out.flush();
      getApplication().getSharedResources().add("lena_convert"
           , new ByteArrayResource("image/jpeg", out.toByteArray()));
      ((WebApplication)getApplication())
       .mountResource("lenaConvertImage", new SharedResourceReference("lena_convert"));
   }catch(Exception ex){
      throw new RuntimeException(ex);
   }
}, (e, x)->{
   logger.error(e);
   logger.error(x.getMessage(), x);
});

Python 加工したデータを標準出力から InputStream → ByteArrayOutputStream
→ メモリ上でWicket の共有リソースとするのです。

Webページの表示は、、
f:id:posturan:20190101113921j:plain

Webページコンストラクタで Python プロセス起動して結果受けるまでは
表示が待たされる作りです。凄い遅いと予測したが、思ったより遅くなかった。

PythonでPDF作成→Javaで受け取り→Wicket AJAXダウンロード

WebアプリをJava で構築していて、Javaでダウンロードすファイルを作るなら一時ファイル
(ディスクに一旦書き出すこと)
を作らずにダウンロードするものを作るのは容易ですが、言語、実行環境が異なる処理に作らせて
一時ファイルを生成することなくダウンロードするのはそれなりの処理が必要です。
Javaからプロセスで実行する Python が作成する PDF をPythpnの処理で標準出力で出力して
Javaがその出力を受け取ったら HTTPのレスポンスとして出力ストリーム(OutputStream) に出力します。
(まるで、横流し。。。)

Java Wicket Web ページの方からサンプルを紹介します。
Javaからプロセス実行は、
yipuran-core ver4.6 からの ScriptExecutor の
runStream(Supplier<String>, Supplier<Collection<String>>
     , Consumer<InputStream>, BiConsumer<String, Throwable>)
メソッドを使って
Script_exec · yipuran/yipuran-core Wiki · GitHub
行います。
AJAX のダウンロード処理として、InputStream でプロセスが出力する標準出力を受け取って
そのまま OutputStream に流してます。
String script = "python /pdf/sample_pdf.py /etc/template.pdf";
が実行する Python の処理プロセスです。
List<String> list = new ArrayList<>(); は、処理プロセスの標準入力に渡す(流す)ものです。

final AJAXDownload download = AJAXDownload.of(out->{
	String script = "python /pdf/sample_pdf.py /etc/template.pdf";

	// String test1~3 → list
	List<String> list = new ArrayList<>();
	list.add(SJutil.toUnicode(text1));
	list.add("\n");
	list.add(SJutil.toUnicode(text2));
	list.add("\n");
	list.add(SJutil.toUnicode(text3));
	list.add("\n");
	list.add("\n");
	int sts = ScriptExecutor.runStream(()->script, ()->list
	, inst->{
		try{
			byte[] b = new byte[1024];
			int len;
			while((len=inst.read(b, 0, b.length)) >= 0){
				out.write(b, 0, len);
			}
			out.flush();
			out.close();
		}catch(Exception ex){
			throw new RuntimeException(ex);
		}
	}, (e, x)->{
		logger.error(e);
		logger.error(x.getMessage(), x);
	});
	logger.debug("status = " + sts);
}, ()->"application/pdf", ()->"test.pdf");

/* submitボタン click で ↑ を callback */
queue(new Button("submit")
.add(AjaxFormSubmitBehavior.onSubmit("click",
SerialThrowableConsumer.of(t->{
	download.callBackDownload(t);
}, (u, x)->{
	logger.error(x.getMessage(), x);
}))).add(download));

実行する Python の処理 → sample_pdf.py
reportlab、pdfrw だけでなく、PyPDF3 を利用して作成した PdfFileReader でメモリ上に読み込んで
PdfFileWriter で、PDF出力するオブジェクトとして取込み、sys.stdout.buffer.write で
バイナリ標準出力します。
print ではだめです。
ファイルとしては生成しないメモリ上での生成として、io.BytesIO() を使用するのが重要です

以下、折角なのでページ番号書き出しと、フォント指定などページ全体を共通に使い回すために
pagenumCanvas.py → from pagenumCanvas import PageNumCanvas
ページコンテンツを書きだす samplepage_pdf.py は、page_pdf.py のクラスを継承します

sample_pdf.py

# -*- coding: utf-8 -*-
import sys
import io
from reportlab.platypus import SimpleDocTemplate, Paragraph
from reportlab.lib.styles import getSampleStyleSheet
from PyPDF3 import PdfFileWriter, PdfFileReader
from pagenumCanvas import PageNumCanvas
from samplepage_pdf import SamplePages
import webbrowser
import os

# PDF 作成
def create(template, inputlist):
    # Samplepage インスタンス、テンプレートと入力リストを渡す。
    sample = SamplePages(template, inputlist)

    packet = io.BytesIO()
    doc = SimpleDocTemplate(packet)
    doc.build([Paragraph("", getSampleStyleSheet()['Normal'])]
              , onFirstPage=sample.page
              , onLaterPages=sample.page
              , canvasmaker=PageNumCanvas)
    new_pdf = PdfFileReader(packet)
    output = PdfFileWriter()
    for i in range(new_pdf.getNumPages()):
        output.addPage(new_pdf.getPage(i))
    po = io.BytesIO()
    output.write(po)
    sys.stdout.buffer.write(po.getvalue())
#########################
if __name__ == '__main__':
    argv = sys.argv
    argvlen = len(argv)
    if argvlen != 2:
        sys.stderr.write('Error: argument [1]=template file required!')
        exit(1)
    else:
        inlist = []
        try:
            while True:
                inp = input('')
                if inp == '': break
                inlist.append(inp)
        except EOFError:
            pass
        # 文字フォントセット宣言
        #pdfmetrics.registerFont(UnicodeCIDFont('HeiseiKakuGo-W5'))

        # PDF作成実行
        create(argv[1], inlist)
        exit(0)


pagenumCanvas.py

# -*- coding: UTF-8 -*-
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm

class PageNumCanvas(canvas.Canvas):
    def __init__(self, *args, **kwargs):
        canvas.Canvas.__init__(self, *args, **kwargs)
        self.pages = []
    def showPage(self):
        self.pages.append(dict(self.__dict__))
        self._startPage()
    def save(self):
        page_count = len(self.pages)
        for page in self.pages:
            self.__dict__.update(page)
            self.draw_page_number(page_count)
            canvas.Canvas.showPage(self)
        canvas.Canvas.save(self)
    def draw_page_number(self, page_count):
        page = "Page %s of %s" % (self._pageNumber, page_count)
        self.setFont("Helvetica", 10)
        self.drawRightString(195 * mm, 272 * mm, page)


samplepage_pdf.py

# -*- coding: utf-8 -*-
from page_pdf import Pdfpage
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
import reportlab.rl_config

class SamplePages(Pdfpage):
    global page_template
    global inlist
    center_w = reportlab.rl_config.defaultPageSize[0] / 2.0
    center_h = reportlab.rl_config.defaultPageSize[1] / 2.0

    def __init__(self, template, inputlist):
        super().__init__()
        # テンプレート読込
        temp_page = PdfReader(template, decompress=False).pages
        SamplePages.page_template = pagexobj(temp_page[0])
        # 入力リスト取得 → global inlist
        SamplePages.inlist = inputlist

    def page(self, canvas, doc):
        # サンプルなので無理やりループする。
        for i in range(4):
            if i > 0: canvas.showPage()
            canvas.doForm(makerl(canvas, SamplePages.page_template))
            canvas.setFont(super().HeiseiKakugoW5, 9)
            canvas.saveState()
            # ----- 目的の canvas 出力 -----
            canvas.drawCentredString(SamplePages.center_w, SamplePages.center_h, "あいう テスト")
            h = 400
            for item in SamplePages.inlist:
                canvas.drawCentredString(SamplePages.center_w, h, item.encode().decode('unicode-escape') )
                h -= 20
            # ------------------------------
            canvas.restoreState()


page_pdf.py

# -*- coding: utf-8 -*-
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from reportlab.pdfbase import pdfmetrics

class Pdfpage():
    global HeiseiKakugoW5
    def __init__(self):
        Pdfpage.HeiseiKakugoW5 = 'HeiseiKakuGo-W5'
        # 文字フォントセット宣言
        pdfmetrics.registerFont(UnicodeCIDFont('HeiseiKakuGo-W5'))