こうかの雑記

こうかの雑記

昔の懐かしいこと、ubuntuのこと、その他いろいろ

markdownファイルをpdfファイルに変換する

 markdown記法で簡単に資料が作れるようになってブラウザでも簡単に見られるようになって印刷もできるようになれば、ワープロソフト(例えばWordとかWriterとか)の出番が少なくなると思います。多分、体裁が気になるような資料だけがワープロソフトの対象として残るだけになるのではないかと思います。

 直接印刷することは望んでなくてpdfファイルに出力できれば良いので、今回はmdファイルからpdfファイルにする機能をpythonで作成しました。 他のツールもありますが、事前の下調べに手間暇掛けて自作の方法をとりました。

必要なツールとインストール

 mdファイルからpdfファイルにするために次の様なソフトウェアがあります。

  • Python-Markdown
    これはmd記法で書かれたテキストをhtmlに変換するツールです。
  • wkhtmltopdf
    これは単独でも使えるhtmlからpdfに変換するツールです。直接にはpythonから使うことは出来ませんが、次のpdfkitを設定して通じて使うことができます。
  • pdfkit
    pythonから多くの引数を設定してwkhtmltopdfを呼び出すことが出来るます。

 この中のPython-Markdownとpdfkitをライブラリーとしてpythonでimportして使います。

 インストール方法は次の通りです。

 端末画面から次のコマンドを実行します。

$ pip3 install markdown
$ pip3 install pdfkit

 次に、https://wkhtmltopdf.org/downloads.htmlからシステムに応じたパッケージをダウンロードします。ubuntuの場合は現時点でUbuntu 18.04 (bionic) のamd64 / i386になります。。  ubuntuだとダウンロードしたファイルをダブルクリックでインストールできます。コマンドでインストールする場合は

$ sudo apt install -y wkhtmltopdf

スクリプトの仕様

  • mdファイルとそれに必要な画像ファイルは同じフォルダーに保存されているものとします。
  • cssファイルは別の場所に保存していても良いものとします。
  • GUIまたはコマンドラインでmdファイルとcssファイルを指定すると、mdファイルと同じ場所にpdfファイルを出力します。 画像を含まない場合は、htmlファイルも出力します。
  • 画像はmd記法の![ alt文字列](画像ファイル名)で書かれていると画像ファイルそのままの大きさになるので、htmlの記法で<img alt="代替え文字列" src="画像ファイル名.JPG" width="400" または height="400" >で書くことを勧めます。
  • pdfの用紙サイズは'A4'固定、余白も固定とします。
  • mdファイル中に[TOC]が書かれていると目次に置き換わります。

使い方

 ubuntuの場合、スクリプトファイルに実行権限を付けておくとGUI画面でクリックするだけで起動できます。GUIで起動した場合はmdファイルとcssファイルを画面で選択入力できます。

 コマンドラインで実行する場合はmdファイルがあるフォルダーに移動してから次の書式でコマンド入力します。(スクリプトファイル名をmd2pdf.pyとした場合)

md2pdf.py mdファイル cssファイル

 cssファイルmdファイルとは違う場所も可

例)md2pdf.py  mdfile.md  /デスクトップ/css/md2pdf.css

 この場合mdファイルとcssファイルの順番はいれかわっても大丈夫です。処理したいmdファイルが多い時はコマンドラインで実行した方が便利です。

 テストしていますが、全てのパターンをテストできたわけではありません。利用される場合は自己責任でお願いします。出力されたpdfは必ず確認して下さい。

スクリプト

 ネットで見つけた例ではPython-Markdown拡張機能の指定が['tables', 'toc']の二つを指定されているものばかりでしたが、よく使われる数字付きリストや枠付きコードブロックが正常に処理されませんでした。以下のスクリプトでは['extra', 'toc']として対応しています。

 画像ファイルの扱いで困ってネットで探したところ、posturanさんの記事を見つけました。base64関係のコードをそのまま使わせて頂きました。posturanさん、ありがとうござました。
なお、画像の表示サイズの指定をする前提でコードを書き直しています。

#!/usr/bin/env python3
'''
    mdファイルとcssファイルから、mdファイルと同じ場所にpdfファイルを作る。
'''
import markdown
import pdfkit
import codecs
import tkinter
from tkinter import messagebox
from tkinter import filedialog
import os
import re
import base64
import sys
'''
    画像ファイルをBase64エンコードテキストに
    http://oboe2uran.hatenablog.com/entry/2019/01/25/150934から拝借
'''
def imageToB64encode(path):
    with open(path, 'rb') as f:
        return base64.b64encode(f.read()).decode('utf-8')  #バイナリーをstrに
'''
    markdownファイルを読み込み、htmlテキストに変換する。
'''
def md2html(md_file):
    with codecs.open(md_file, mode = 'r', encoding = 'utf-8') as f:
        md_txt = f.read()
        md_txt = re.sub('@import ".+"\n', '', md_txt)
    html_body = markdown.Markdown(extensions = ['extra', 'toc']).convert(md_txt)
    # 画像を<img src=data:image/png;base64,base64エンコード文字列"/> にして埋め込む。
    # ループ部分は http://oboe2uran.hatenablog.com/entry/2019/01/25/150934 から拝借後
    # 画像表示サイズ指定に対応させた。
    img_sw = False
    for imgtag in re.findall('<img .* src=".+"', html_body):
        # 画像ファイルの名前を得る
        s = re.search('src=".+"', imgtag).group(0).replace('src="', '').replace('"', '')
        imgfname = s.split(' ',1)[0]    # 続くオプションをカット
        # imgval = re.search('<img .* ', imgtag).group(0)
        # imgext = imgfname[-3:]
        imgval = 'src=data:image/' + imgfname[-3:] + ';base64,' + imageToB64encode(imgfname) 
        imgsrc = 'src="' + imgfname+ '"'
        img_sw = True
        html_body = html_body.replace(imgsrc, imgval)
    return html_body, img_sw
'''
    cssを埋め込む
'''
def set_html(html_body, css_file):
    with codecs.open(css_file, mode = 'r', encoding = 'utf-8') as f:
            css_txt = f.read()
    html_txt = '<html>\n<head>\n<meta charset="utf-8">\n<style>' + css_txt + '\n</style>\n</head><boby>\n' + html_body + '\n</body>\n</html>'
    return html_txt
'''
    htmlファイルをカレントディレクトリに出力する。
'''
def output_html(md_name, html_txt):
    with open(md_name.replace('md', 'html'), 'w') as f:
        f.write(html_txt)
'''
    準備処理 対象mdファイルとそのディレクトリ、cssファイルのパスを取得
'''
def preparat_rtn(argv):
    if len(argv) == 3:
        if argv[1][-3:] == '.md':
            md_file = argv[1]
        else:
            if argv[1][-4:] == '.css':
                css_file = argv[1]
        if argv[2][-4:] == '.css':
            css_file = argv[2]
        else:
            argv[2][-3:] == '.md'
            md_file =argv[2]
        md_name = md_file
    else :
        root = tkinter.Tk()
        root.withdraw()
        tkinter.messagebox.showinfo('md2pdf.py',
                                '対象mdファイルを選択して下さい。')
        md_file = tkinter.filedialog.askopenfilename(title = 'md2pdf.py mdファイルを選択') 
        if md_file == '':
            return '', '', '', ''
        md_name = os.path.basename(md_file)
        md_dir  = os.path.dirname(md_file)
        os.chdir(md_dir)
        tkinter.messagebox.showinfo('md2pdf.py',
                                'cssファイルを選択して下さい。')
        root.destroy()
        css_file = tkinter.filedialog.askopenfilename(title = 'md2pdf.py cssファイルを選択') 
        if css_file == '':
            return '', '', '', ''
    pdf_file = md_name.replace('.md', '.pdf')
    return md_file, md_name, css_file, pdf_file

def main_rtn():
    md_file, md_name, css_file, pdf_file = preparat_rtn(sys.argv)  #準備処理
    if md_file == '':
        print('処理中断しました')
    else:
        html_body, img_sw = md2html(md_file)
        html_txt  = set_html(html_body, css_file)
        pdf_options = {'page-size': 'A4',
                       'margin-top': '0.75in',
                       'margin-right': '0.75in',
                       'margin-bottom': '0.75in',
                       'margin-left': '0.75in',
                       'encoding': "UTF-8"
        }
        # htmlからPDFを出力
        pdfkit.from_string(html_txt, pdf_file, options=pdf_options)
        if img_sw == False:     # 画像がなければhtmlを出力
            output_html(md_name, html_txt)
if __name__ == '__main__':
    main_rtn()

cssファイル

 cssは簡単なものを用意しました。
 行頭の字下げは、pタグの設定で対応しました。

body {
    font-size : 150%;
    }
p   {
    text-indent: 1em;
    }
th,td {
    border: solid 1px;  /* 枠線指定 */
    padding: 5px;      /* 余白指定 */
    }
table {
    border-collapse:  collapse; /* セルの線を重ねる */
    }
pre {
    padding: 5px;
    overflow: auto;                 /* 折り返し */
    white-space: pre-wrap;
    word-wrap: break-word;
    background-color:#EEEEEE;
    }

その他情報

 Python-Markdownでのmdからhtmlへの変換で少しmdの解釈が違うことが有りましたので、ここで紹介します。

 行頭に全角空白で字下げしても半角空白と同様に無視されます。
cssファイルの中で、pタグにtext-indent: 1em;を付加して対応しました。
余談ですが、マイクロソフトVsCodeでも行頭の字下げが無視されますが、同じことだと思います。

 リストをネストする場合、ネスト部分の字下げ数がビューワを実装した人によって解釈が違うようで、typora、VsCodeでは空白3文字でしたが、このライブラリーでは空白4文字の字下げが必要でした。また他のビューワでは空白1文字の場合もありました。

 下記は空白3文字の時の例

* リスト1
   * リスト1−1
   * リスト1−2
* リスト2

では出力されるhtmlは

<ul>
<li>リスト1</li>
<li>リスト1−1</li>
<li>リスト1−2</li>
<li>リスト2</li>
</ul>

となり、ネストされませんでした。

 mdファイルをpdfにする手段は他にもあり、次のソフトでも出来ます。

  • typora
    ゆくゆく有料ソフトになることが予定されています。
  • VsCode
    汎用の高機能テキストエディターです。マークダウンのための拡張機能にpdf出力も備えています。行頭の字下げが無視されます。cssを直すことが出来ればなぁと思います。
  • pandoc
    各種ファイル様式間でのファイルコンバーターです。色々できて便利そうなのですがコマンドラインでしか使えません。そのコマンドのパラメーターが多く慣れていない人には辛いものがあります。またエラーが出た時の対処する方法が私にはわかりませんでした。

関連情報

Using Markdown as a Python Library

Python-Markdown拡張機能

pdfkit 0.6.1

wkhtmltopdf