投稿者: atmark

  • Python の CGI で reCAPTCHA v2 を使う

    ちょっと前までは人間でも読解困難な文字を読ませていた CAPTCHA ですが、Google の reCAPTCHA v2 では人間である可能性が高い場合は「私はロボットではありません」というチェックを入れるだけとなって、かなりユーザへの負担が軽減されました。

    既にクリックすら必要ない reCAPTCHA v3 のベータテストが行われているので、もしかしたらすぐに v3 へ移行してしまうかもしれませんが、現時点ではまだベータ版ということで v2 を選んでいます。

    reCAPTCHA v2 を自分のサイトに導入するには、クライアントに表示されるページとサーバの両方にコードの追加が必要となります(実体は両方ともサーバにあるファイルですね)。

    流れとしては下記の通りです。

    1. reCAPTHA のページで登録を行う
    2. ユーザに表示する form にコードを埋め込む(JavaScript の読み込みと HTML)
    3. form からの値を Google に投げて判別するコードをサーバ側に追加する

    reCAPTCHA の登録

    https://www.google.com/recaptcha/ を開き、”My reCAPTCHA” から登録を行います。余談ですが右下に reCAPTCHA のマークが表示されているので、このページ自体も reCAPTCHA で保護されていますね。

    reCAPTCHA トップ画面
    reCAPTCHA トップ画面

    “Register a new site” から新しくサイトを登録します。自分の区別しやすい名前とドメイン名を入力するだけです。

    reCAPTCHA register

    登録したサイトを開くと設定の仕方が載っているので、これを参考にソースやコードを書き換えます。癖で secret にモザイクかけてしまいましたが、HTML に書かれるから隠す意味はありませんね。

    reCAPTCHA secret

    クライアントに表示される HTML の変更

    2 点だけです。

    head 要素に JavaScript の読み込みを追加

    <script src='https://www.google.com/recaptcha/api.js'></script>

    form 要素に div 要素を追加する

    <div class="g-recaptcha" data-sitekey="XXXXXXXXYOURSECRETXXXXXXXXX"></div>

    これが reCAPTCHA v2 のチェックボックスになります。

    ここまでで次のような表示が現れたら OK です(ID: の入力欄は関係ありません)。

    reCAPTCHA form

    サーバ側 Python コードの修正

    今回は昔ながらの手法で POST された form を読み取る CGI を対象としています。

    使うモジュールは次の通り。

    import cgi
    import urllib, urllib2
    import json

    cgi は form の値を読み取るために、urllib, urllib2 は Google の API にアクセスするために、json は API の返答を読み取るために使います。

    後は通常の form のように ‘g-recaptcha-response’ の値を取得して、’https://www.google.com/recaptcha/api/siteverify’ に対して secret と ‘g-recaptcha-response’ の値を POST します。

    # form から g-recaptcha-response の値を取得
    form = cgi.FieldStorage()
    response = form.getfirst('g-recaptcha-response', '')
    
    # API で値を検証する
    url = 'https://www.google.com/recaptcha/api/siteverify'
    secret = 'XXXXXXXXYOURSECRETXXXXXXXXX'
    params = urllib.urlencode({'secret': secret, 'response': response})
    req = urllib2.Request(url, params)
    res = json.loads(urllib2.urlopen(req).read())
    
    # 検証結果
    if res['success']:
        # ここに reCAPTCHA 成功時の処理を書く
        print('Passed reCAPTCHA')
    else:
        # ここに reCAPTCHA 失敗時の処理を書く
        print('Failed to pass reCAPTCHA')

    response は JSON で帰ってきますが、簡単に使うだけなら ‘success’ の値だけを取得すれば OK です。response の JSON は次のような内容を含んでいます(https://developers.google.com/recaptcha/docs/verify)。

    {
      "success": true|false,
      "challenge_ts": timestamp,  // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
      "hostname": string,         // the hostname of the site where the reCAPTCHA was solved
      "error-codes": [...]        // optional
    }

    これだけなので、bot 対策に悩まされているなら試してみる価値はあります。

  • Python の bcrypt 実装色々

    Python の bcrypt 実装にはいくつかあって、PyPI で検索すると単体で使うもの以外にも Flask 用、Django 用など様々なものがあります。

    単体で使うには現在でもメンテが活発な bcrypt が良いと思いますが、導入する環境によっては libffi に依存する関係で pip install 中にビルドが通りません。環境を整えれば解決する話(apt なら install libffi-dev で OK)なのですが、共用サーバなのもあって他のもので解決できそうであればその方が良いと判断しました。

    ここで注意しなければいけないのが、上に挙げた 3 つの bcrypt 実装は全て import bcrypt して使うことになっています。これはつまり、異なるライブラリなのに import する名前が bcrypt で同じなので、今どのライブラリを使っているのかが判断できなくて予想外の問題を起す可能性があるということです。

    実際にこんな投稿がありました。

    import bcrypt する場合は開発環境と運用環境の違いに十分注意しないと、一見動いているように見えてもこのようにハマる可能性が高いです。

    bcrypt 3.1.4 から py-bcrypt 0.4 へ

    gensalt() の引数の違い

    試しに同じソースを使って、環境を bcrypt から py-bcrypt に変更してみました。

    <type 'exceptions.TypeError'>: gensalt() got an unexpected keyword argument 'rounds' 
     args = ("gensalt() got an unexpected keyword argument 'rounds'",) 
     message = "gensalt() got an unexpected keyword argument 'rounds'"

    gensalt() の引数が違うようです。

    bcrypt の src/bcrypt/__init__.py を読んでみると、rounds と prefix があります。

    def gensalt(rounds=12, prefix=b"2b"):
        if prefix not in (b"2a", b"2b"):
        raise ValueError("Supported prefixes are b'2a' or b'2b'")
    
        if rounds < 4 or rounds > 31:
        raise ValueError("Invalid rounds")
    
        salt = os.urandom(16)
        output = _bcrypt.ffi.new("char[]", 30)
        _bcrypt.lib.encode_base64(output, salt, len(salt))
    
        return (
            b"$" + prefix + b"$" + ("%2.2u" % rounds).encode("ascii") + b"$" +
            _bcrypt.ffi.string(output)
        )

    一方の py-bcrypt の bcrypt/__init__.py にある gensalt() の実装を見てみると、log_rounds だけです。

    def gensalt(log_rounds = 12):
        """Generate a random text salt for use with hashpw(). "log_rounds"
        defines the complexity of the hashing, increasing the cost as
        2**log_rounds."""
        return encode_salt(os.urandom(16), min(max(log_rounds, 4), 31))

    以上から、bcrypt から py-bcrypt に移行するときは prefix をなくして rounds を log_rounds に直せば OK です。逆の場合は 2a を指定したいときだけ prefix = b’2a’ を与えれば良いです。py-bcrypt は 2a 固定になっていました。

    hashpw() と checkpw()

    hashpw() と checkpw() は特に変更することなく、正常に使えているようです。

  • Gemini PDA のファームウェア更新(2018-06-12)

    FlashTool の導入や使い方は「Gemini PDA の root 取得 (Windows 10 環境)」と同様なのでそちらを参照してください。

    最新のファームウェアは Planet Computers 公式に用意されています。

    今回は LATEST – Gemini x27 Android FOTA3 12/06/2018 を、前回の 20180510 の root なシステムに上書きする形で進めました。Gemini_x27_FOTA3_12062018.zip のダウンロードが完了したら展開しておきます。

    FlashTool を起動して Scatter-loading file に展開したフォルダにある Gemini_Android.txt を指定します。一覧から userdata のチェックを外します。

    FlashTool 20181206
    FlashTool 20181206

    起動している Gemini PDA を PC に接続して、Download を押します。Gemini PDA の Esc を長押ししてメニューから再起動を選びます。大きなチェックマークが表示されたら完了です。

    ケーブルを抜いて Esc を長押しして Gemini PDA を起動し、設定の「端末情報」一番下「ビルド番号」確認して Gemini-7.1-Planet-12062018-V2 になっていることを確認して終了です。

    ビルド番号

    root 環境から非 root 環境に戻しましたが、特に問題なさそうです。スクリーンショットの撮影音が復活してしまったのが、やや煩わしいくらいでしょうか。

    キー配置の変更に関しては非 root 環境でも apk の導入でできるようになったようです。キー変更のためだけに root 化してた人はこれで十分そうですね。

  • さくらのレンタルサーバーで pip できない(SSLError)

    TL; DR

    古い OpenSSL が使われるのが原因です。Python ビルド時に新しい OpenSSL を指定すれば解決します。

    $ pyenv uninstall 3.6.5
    $ CPPFLAGS="-I/usr/local/ssl/include" LDFLAGS="-L/usr/local/ssl/lib" pyenv install 3.6.5

    経緯

    さくらのレンタルサーバーはプラン スタンダード以上で SSH が使えるので、pyenv を入れたり pip を入れたりできます。pyenv を使って Python の他のバージョンをインストールするときに注意しないといけないのが、さくらでは Python 導入時のビルドで古い OpenSSL が使われてしまうことです。これの影響で pip したときに次のような SSL のエラーを吐きます。

    $ pip install bcrypt
    Collecting bcrypt
     Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLError(1, '[SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:833)'),)': /simple/bcrypt/
     Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLError(1, '[SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:833)'),)': /simple/bcrypt/
     Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLError(1, '[SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:833)'),)': /simple/bcrypt/
     Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLError(1, '[SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:833)'),)': /simple/bcrypt/
     Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLError(1, '[SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:833)'),)': /simple/bcrypt/
     Could not fetch URL https://pypi.python.org/simple/bcrypt/: There was a problem confirming the ssl certificate: HTTPSConnectionPool(host='pypi.python.org', port=443): Max retries exceeded with url: /simple/bcrypt/ (Caused by SSLError(SSLError(1, '[SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:833)'),)) - skipping
     Could not find a version that satisfies the requirement bcrypt (from versions: )
    No matching distribution found for bcrypt

    OpenSSL のバージョンが古いのかな?とバージョンを確認しても最新版です。

    $ openssl version
    OpenSSL 1.0.2o 27 Mar 2018

    ですが pyenv install 3.6.5 した Python 上で確認してみると

    $ python
    Python 3.6.5 (default, Jun 26 2018, 10:35:21)
    [GCC 4.2.1 20070831 patched [FreeBSD]] on freebsd9
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import ssl
    >>> ssl.OPENSSL_VERSION
    'OpenSSL 0.9.8zf 19 Mar 2015'

    思いっきり 0.9.8zf と出ていますね。「Python – さくらレンタルサーバーでpip installができません(123028)|teratail」によると、普通にビルドすると最新版の OpenSSL ではなくて、古い方の OpenSSL をリンクしてしまうとのこと。

    さくらレンタルサーバでは普通にpythonをbuildすると古いopensslにつながってしまうようです。
    /usr/local/sslに新しいのが入ってるみたいなので、

    ./configure CPPFLAGS="-I/usr/local/ssl/include" LDFLAGS="-L/usr/local/ssl/lib"

    でpythonを作るとよさそうです。

    ということで一旦インストールした Python を uninstall して、OpenSSL のパスを指定して再度インストールし直します。

    $ pyenv uninstall 3.6.5
    $ CPPFLAGS="-I/usr/local/ssl/include" LDFLAGS="-L/usr/local/ssl/lib" pyenv install 3.6.5
    Downloading Python-3.6.5.tgz...
    -> https://www.python.org/ftp/python/3.6.5/Python-3.6.5.tgz
    Installing Python-3.6.5...
    Installed Python-3.6.5 to /home/USERNAME/.pyenv/versions/3.6.5

    無事インストールできたようなので、OpenSSL のバージョンを確認してみます。

    $ python
    Python 3.6.5 (default, Jun 26 2018, 10:35:21)
    [GCC 4.2.1 20070831 patched [FreeBSD]] on freebsd9
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import ssl
    >>> ssl.OPENSSL_VERSION
    'OpenSSL 1.0.2o 27 Mar 2018'

    大丈夫そうですね。pip の更新を兼ねて pip を試してみます。

    $ pip install -U pip
    Collecting pip
     Downloading https://files.pythonhosted.org/packages/0f/74/ecd13431bcc456ed390b44c8a6e917c1820365cbebcb6a8974d1cd045ab4/pip-10.0.1-py2.py3-none-any.whl (1.3MB)
     100% |################################| 1.3MB 890kB/s
    Installing collected packages: pip
     Found existing installation: pip 9.0.3
     Uninstalling pip-9.0.3:
     Successfully uninstalled pip-9.0.3
    Successfully installed pip-10.0.1

    ばっちり動きました。

    ですが……今回の目的だった bcrypt のインストールは、libffi がなくて結局できず。py-bcrypt なら pip 一発なのでソースコードを py-bcrypt に書き替えた方が早そう。ちょっと不完全燃焼です。

    参考

  • Gemini PDA のキーボード設定

    Gemini PDA のキーボード設定

    日本語入力は Gboard に落ち着きましたが、配列の設定がよくわかっていなくて記号が入力できない状態になってしまいました。調べながら弄っているうちにようやく満足に使える設定になったので、忘れないようにメモしておきます。

    Gboard も標準でインストールされているようだったので、初期設定が終ったら Play ストアからアップデートをかけておきます。

    Gemini キーボードという 4 ステップで設定するアプリも用意されていますが、Android 標準の設定だけで十分だったので今回は使いません。

    今回のゴールは次の通り。

    • キーボードだけで直接入力と日本語入力を切り替え
    • キーの刻印通りの入力
    • バックスラッシュ\(パイプ|)を長音-に変更
    • 句読点を ZXCVBNM,. の並びに変更
    • 句読点の並び伴ってカーソルを vi 配列(←↓↑→)に変更

    Gboard とキー配列の設定

    設定の「言語と入力」を開きます。

    「仮想キーボード」を開いて「キーボードを管理」から gboard を有効にします。どうせ使わないからと、このときついでに他のキーボードは全て無効にしました。

    一つ前の画面に戻り、仮想キーボードから Gboard を開きます。「言語」を開いて「日本語」と「英語(米国)」を追加します。

    「言語と入力」まで戻って「物理キーボード」を開きます。「Gboard – 多言語入力」を開いて「English (US) Default Gemini Keyboard」を選択します(「English (US) Gemini Keyboad)」でも大丈夫だと思うけど違いがわからない)。

    これで一度 Notes などで入力してみて、キーの刻印と入力される文字が一致しているか、Shift + Space で直接入力と日本語入力の切り替えができるか試してみてください。

    参考

    キー配列のカスタマイズ(要 root)

    日本語入力が行える環境が整ったら、次は使いやすいように配列を自分好みにカスタマイズします。と言っても先人がしているのと全く同じです。日本語の文章を打ってると、まず真っ先に、よく使う句読点と長音にストレスを感じたので、考えることは皆さん似ているんだと思います。

    バックスラッシュを長音に変更するだけであれば非 root でもできるようですが、他にも /system 以下を触りたかったので今回は root 前提で進めます。

    キー配列に関係するファイルはいくつかあるようです。今回はキーコードに対するキーの割当て(keylayout)と、同時押しのキー修飾に対する割当て(keychars)の 2 つを設定します。それぞれ次のファイルを書き換えます。

    • /system/usr/keylayout/Generic.kl
    • /system/usr/keychars/Generic.kcm

    目標とするのは次の画像のような配列。M の横に句読点が並び、最下段にカーソルキーが並ぶ配列。

    /system/usr/keylayout/Generic.kl

    ES テキストエディタで次の 6 行をさくっと書き換えてしまいます。

    key 40    BACKSLASH
    key 51    COMMA
    key 52    PERIOD
    key 103   DPAD_UP
    key 105   DPAD_LEFT
    key 108   DPAD_DOWN

    この 6 行を次のように書き換えます。変更しない途中の行は飛ばしています。間違えて消さないように。

    key 40    MINUS
    key 51    DPAD_LEFT
    key 52    COMMA
    key 103   PERIOD
    key 105   DPAD_DOWN
    key 108   DPAD_UP

    /system/usr/keychars/Generic.kcm

    \ を – に変更したので Fn + \ を押しても : が出せなくなります。新たに Fn + – に : を割り当てます。”key MINUS” を探して、次の一行を追加します。

    fn: ':'

    ここまで完了したら再起動して終了です。快適なキー配列を楽しみましょう。

    参考

  • Gemini PDA の root 取得 (Windows 10 環境)

    Gemini PDA の root 取得 (Windows 10 環境)

    ※ 2018-06-27 userdata について追記しました

    句読点のキー配列が何とも慣れないので、キー配列変更のために早速 root を取得することにしました。環境は Windows 10 Pro 64 ビット版です。

    基本的には Planete Computers 公式にすべて載っていますが、日本語の情報も先人が提供してくれています。

    FlashTool の取得

    Download and Install FlashTool on Windows から、以下のファイルをダウンロードします。

    • FlashToolWindows.zip
    • FlashToolDrivers.zip

    FlashToolDrivers.zip を展開して、Install.bat を実行します。

    Gemini PDA FlashToolDrivers.zip Gemini PDA FlashToolDrivers.zip

    UAC の昇格を求められるので、許可すると 64 ビット版のドライバをインストールしてくれたようです。

    Gemini PDA FlashToolDrivers Install.bat Gemini PDA FlashToolDrivers Install.bat

    ファームウェアのダウンロード

    次のファームウェアのイメージをダウンロードします。初期の x25 かセカンドロットの x27 でダウンロードするファイルが異なるので注意してください。

    今回は Gemini_x27_10052018.zip を使って進めます。ダウンロードすると約 3.15 GB ありました。ダウンロードしたら展開しますが、展開後に約 6.92 GB まで膨れたので展開先の空きにご注意ください。展開した場所を FlashTool で開くので、パスをおぼえておいてください。

    NVRAM パーティションのバックアップ

    公式には次のような記述があるので、素直に従って NVRAM パーティションのバックアップを先に取っておきます。

    Before flashing the device with a different firmware it is a good idea to backup the current NVRAM partition. This partition stores key information for your Gemini, including the IMEI number. If it gets lost or damaged, your Gemini will not be able to take or receive calls.

    FlashToolWindows.zip を展開して、flash_tool.exe を実行します。Readback タブを開き、Add を一度押します。

    FlashTool NVRAM Backup FlashTool NVRAM Backup

    Readback を押したら、Gemini PDA を PC に接続します。初めて接続するとドライバのインストールが始まるので、次の表示が出るまでしばらく我慢します。

    Gemini PDA ドライバインストール完了 Gemini PDA ドライバインストール完了

    次に Gemini PDA を再起動します。再起動するのですが……本体横の銀色ボタンで電源オプションが出ると思ったら出なくて、どこで再起動するか本気でわからずに 10 分ほど四苦八苦。答えは Fn + Esc Esc を長押しでした。

    再起動すると自動的に FlashTool が NVRAM のバックアップを取ってくれます。完了すると次のような画面が表示されます。

    NVRAM バックアップ完了 NVRAM バックアップ完了

    完了したら一度ケーブルを抜いて Esc を長押しして Gemini PDA を起動しておきます(追記: この手順はもしかしたらケーブルの挿し直しで省略できるかも?未検証)。

    ファームウェアの書き込み

    端末が初期化されるので必ずデータのバックアップを取ってから行ってください。 userdata のチェックを外せばアプリやデータを残せます。

    いよいよ root 化を行います。FlashTool の Download タブを開き、2 行目の Scatter-loading File に展開したファームウェアに含まれる Gemini_Android_Rooted.txt を指定します。3 行目のドロップダウンから Firmware Upgrade を選びます。

    アプリやユーザーデータを残したい場合は、一覧から userdata のチェックを外します。

    FlashTool root化
    FlashTool root化

    用意ができたら Download を押し、Gemini PDA を接続して再起動します。自動で書き込みが始まるのでしばらく待ちます。

    FlashTool root化 書込中
    FlashTool root化 書込中

    完了すると大きなチェックマークが出ます。ケーブルを抜いて Fn + Esc 長押しで Gemini PDA を起動します。

    Magisk Manager のインストール

    Gemini PDA が起動したら、公式の案内通りに Magisk Manager をインストールします。お約束ですが、設定のセキュリティから「提供元不明のアプリ」を許可しておいてください。

    パッケージをインストールしたら起動します。一番下から 2 番目に「インストール」があるので実行します。

    Magisk Manager インストール Magisk Manager インストール1
    Magisk Manager インストール2 Magisk Manager インストール2

    「方法の選択」は「直接インストール(推奨)」を選びます。

    Magisk Manager インストール3 Magisk Manager インストール3
    Magisk Manager インストール4 Magisk Manager インストール4

    インストールが終ったら「再起動」します。

    root 化確認

    root 権限を要求するようなアプリで確認します。Termux で su してみたところ、無事に昇格できました。

    Termux で su - root 化の確認 Termux で su – root 化の確認
  • Python 3 と ReportLab で PDF ファイルを生成する

    Python から PDF ファイルを作成できる ReportLab を試してみます。

    インストール

    ReportLab のインストールは pip で一発ですが

    $ python -V
    Python 3.6.5
    $ pip install reportlab
    ...<省略>...
     The headers or library files could not be found for jpeg,
     a required dependency when compiling Pillow from source.

    と Pillow のインストール時に JPEG のヘッダ類がないと怒られるので、libjpeg-dev を apt でインストールしておきます。

    $ sudo apt install libjpeg-dev
    $ pip install reportlab
    Collecting reportlab
     Using cached https://files.pythonhosted.org/packages/87/f9/53b34c58d3735a6df7d5c542bf4de60d699cfa6035e113ca08b3ecdcca3f/reportlab-3.4.0.tar.gz
    Collecting pillow>=2.4.0 (from reportlab)
     Using cached https://files.pythonhosted.org/packages/89/b8/2f49bf71cbd0e9485bb36f72d438421b69b7356180695ae10bd4fd3066f5/Pillow-5.1.0.tar.gz
    Requirement already satisfied: pip>=1.4.1 in /home/pi/.pyenv/versions/3.6.5/lib/python3.6/site-packages (from reportlab) (10.0.1)
    Requirement already satisfied: setuptools>=2.2 in /home/pi/.pyenv/versions/3.6.5/lib/python3.6/site-packages (from reportlab) (39.0.1)
    Installing collected packages: pillow, reportlab
     Running setup.py install for pillow ... done
     Running setup.py install for reportlab ... done
    Successfully installed pillow-5.1.0 reportlab-3.4.0

    PDF を生成する

    公式のユーザガイド(PDF)にあるサンプルコードを実行してみます。

    from reportlab.pdfgen import canvas
    
    def hello(c):
        c.drawString(100,100,"Hello World")
    
    c = canvas.Canvas("hello.pdf")
    hello(c)
    c.showPage()
    c.save()

    hello.pdf が生成されているはずなので開いて確認します。

    ReportLab PDF hello
    ReportLab PDF hello

    簡単ですね。ただし、このままだと日本語を出力しようとしても四角(■)で埋められるだけでした。エンコーディングも関係ないようだったのでフォントを指定する必要があるようです。

    座標系はページ左下を起点としています。先の例では左下から (100, 100) の座標に文字列を描画しています。

    日本語フォント

    こちらを参考にしました。

    ReportLab には予め HeiseiMin-W3, HeiseiKakuGo-W5 が用意されているそうです。ユーザガイドの 3.6 Asian Font Support に Asian Language Packs として記載されていますね。

    Japanese, Traditional Chinese (Taiwan/Hong Kong), Simplified Chinese (mainland China) and Korean are all supported and our software knows about the following fonts:

    • chs = Chinese Simplified (mainland): ‘STSong-Light’
    • cht = Chinese Traditional (Taiwan): ‘MSung-Light’, ‘MHei-Medium’
    • kor = Korean: ‘HYSMyeongJoStd-Medium’,’HYGothic-Medium’
    • jpn = Japanese: ‘HeiseiMin-W3’, ‘HeiseiKakuGo-W5’

    これらを参考に日本語を入れてみたのが次のソースになります。ファイルは UTF-8n で保存しています。

    from reportlab.pdfgen import canvas
    from reportlab.lib.pagesizes import A4, portrait
    from reportlab.pdfbase import pdfmetrics
    from reportlab.pdfbase.cidfonts import UnicodeCIDFont
    
    c = canvas.Canvas('sample.pdf', pagesize=portrait(A4))
    
    pdfmetrics.registerFont(UnicodeCIDFont('HeiseiKakuGo-W5'))
    
    c.setFont('HeiseiKakuGo-W5', 24)
    w, h = A4
    c.drawCentredString(w / 2, h / 2, '日本語PDFのサンプルです。')
    
    c.showPage()
    c.save()
    ReportLab PDF 日本語
    ReportLab PDF 日本語

    特に問題なく表示されていますね。

    TrueType フォント

    この他にも自分で TrueType フォントを用意して指定することもできます。試しに Windows 10 の游ゴシックを使ってみました。

    from reportlab.pdfgen import canvas
    from reportlab.lib.pagesizes import A4, portrait
    from reportlab.pdfbase import pdfmetrics
    from reportlab.pdfbase.ttfonts import TTFont
    
    c = canvas.Canvas('sample.pdf', pagesize=portrait(A4))
    
    pdfmetrics.registerFont(TTFont('Yu Gothic Light', 'YuGothL.ttc'))
    
    c.setFont('Yu Gothic Light', 24)
    w, h = A4
    c.drawCentredString(w / 2, h / 2, '日本語PDFのサンプルです。')
    
    c.showPage()
    c.save()
    ReportLab PDF 日本語TTF
    ReportLab PDF 日本語TTF

    ばっちりです。*.ttf だけでなく *.ttc でも大丈夫でした。

    描画メソッド

    直線

    • canvas.line(x1, y1, x2, y2)
    • canvas.lines(linelist)

    図形

    • canvas.grid(xlist, ylist)
    • canvas.bezier(x1, y1, x2, y2, x3, y3, x4, y4)
    • canvas.arc(x1, y1, x2, y2)
    • canvas.rect(x, y, width, height, stroke=1, fill=0)
    • canvas.ellipse(x1, y1, x2, y2, stroke=1, fill=0)
    • canvas.wedge(x1, y1, x2, y2, startAng, extent, stroke=1, fill=0)
    • canvas.circle(x_cen, y_cen, r, stroke=1, fill=0)
    • canvas.roundRect(x, y, width, height, radius, stroke=1, fill=0)

    一行文字列(改行なし文字列)

    • canvas.drawString(x, y, text)
    • canvas.drawRightString(x, y, text)
    • canvas.drawCentredString(x, y, text)

    テキストオブジェクト

    ユーザガイドには次のように記載されています。

    For the dedicated presentation of text in a PDF document, use a text object. The text object interface provides detailed control of text layout parameters not available directly at the canvas level. In addition, it results in smaller PDF that will render faster than many separate calls to the drawString methods.

    テキストが多いなら、一回一回 drawString() を呼び出すよりもこちらを使えってことらしいです。が、いざ使ってみると canvas に beginText() なんてメソッドはないと言われて使えず。

    textobject = canvas.beginText(x, y)
    canvas.drawText(textobject)
    • textobject.setTextOrigin(x, y)
    • textobject.setTextTransform(a, b, c, d, e, f)
    • textobject.moveCursor(dx, dy) # from start of current LINE
    • (x, y) = textobject.getCursor()
    • x = textobject.getX(); y = textobject.getY()
    • textobject.setFont(psfontname, size, leading = None)
    • textobject.textOut(text)
    • textobject.textLine(text=”)
    • textobject.textLines(stuff, trim=1)

    着色

    • canvas.setFillColorCMYK(c, m, y, k)
    • canvas.setStrikeColorCMYK(c, m, y, k)
    • canvas.setFillColorRGB(r, g, b)
    • canvas.setStrokeColorRGB(r, g, b)
    • canvas.setFillColor(acolor)
    • canvas.setStrokeColor(acolor)
    • canvas.setFillGray(gray)
    • canvas.setStrokeGray(gray)

    フォント

    • canvas.setFont(psfontname, size, leading = None)

    Platypus

    ここまでは低水準(low-level)の描画メソッドばかりでした。これらを駆使して複雑な文書を作れないこともないですが、大変な労力を要します。例えば改行や改頁といった処理も自分で行わないといけません。

    ReportLab には Platypus というテンプレートエンジンが用意されいて、一般的な文書の構成であればわりと簡単にページの構成を作ることができます。

    次はユーザガイドに載っている Platypus の最も簡単なサンプルを少しだけ整形したものです。

    from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
    from reportlab.lib.styles import getSampleStyleSheet
    from reportlab.rl_config import defaultPageSize
    from reportlab.lib.units import inch
    PAGE_HEIGHT=defaultPageSize[1]; PAGE_WIDTH=defaultPageSize[0]
    styles = getSampleStyleSheet()
    
    Title = "Hello world"
    pageinfo = "platypus example"
    def myFirstPage(canvas, doc):
        canvas.saveState()
        canvas.setFont('Times-Bold',16)
        canvas.drawCentredString(PAGE_WIDTH / 2.0, PAGE_HEIGHT - 108, Title)
        canvas.setFont('Times-Roman',9)
        canvas.drawString(inch, 0.75 * inch, "First Page / %s" % pageinfo)
        canvas.restoreState()
    
    def myLaterPages(canvas, doc):
        canvas.saveState()
        canvas.setFont('Times-Roman',9)
        canvas.drawString(inch, 0.75 * inch, "Page %d %s" % (doc.page, pageinfo))
        canvas.restoreState()
    
    doc = SimpleDocTemplate("phello.pdf")
    Story = [Spacer(1, 2 * inch)]
    style = styles["Normal"]
    for i in range(100):
        bogustext = ("This is Paragraph number %s. " % i) * 20
        p = Paragraph(bogustext, style)
        Story.append(p)
        Story.append(Spacer(1, 0.2 * inch))
    doc.build(Story, onFirstPage=myFirstPage, onLaterPages=myLaterPages)

    これによって生成された phello.pdf を表示すると、次のようになっています。

    ReportLab Platypus Sample 1/2
    ReportLab Platypus Sample 1/2
    ReportLab Platypus Sample 2/2
    ReportLab Platypus Sample 2/2

    <執筆中>

    参考文献