【Python】reportlab と pdfrw で目視 diff 用の PDF を生成する

4 min read読了の目安(約3800字

環境:

> python --version
Python 3.8.5

出力イメージ

2つの PDF を互い違いに組み合わせて単一の PDF にし、各ページの左下に元のファイル名を透かしとして入れます。
単ページ状態でスクロールしてくと変化している箇所がパッと目につくので紙で探すより格段に差分を探しやすくなります。

コード

  1. 2つの PDF のそれぞれについて、元ファイル名が左下に入っただけの透かし用 PDF を生成する。
  2. 透かし用ファイルを元ファイルの上に重ねて、透かし入りファイルとして保存。
  3. 2つの透かし入りファイルを互い違いに結合する。

以上の処理を一時フォルダ内で行います。

zip-to-diff.py
import os
import sys
import datetime
import tempfile

from pdfrw import PdfReader, PdfWriter, PageMerge

from reportlab.pdfgen import canvas
from reportlab.lib.colors import Color
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.units import inch

# 透かしとして入れる PDF を生成する関数
def new_temp_pdf(path, w_pt, h_pt, text):
    w_inch = round(w_pt / 72 * inch)
    h_inch = round(h_pt / 72 * inch)
    c = canvas.Canvas(path)
    c.setPageSize((w_inch, h_inch))
    pdfmetrics.registerFont(TTFont("localfont", r"C:\Windows\Fonts\msgothic.ttc")) # MSゴシックを指定
    c.setFont("localfont", 24)
    c.setFillColor(Color(200, 200, 200, alpha=0.6))
    c.drawString(10, 10, text)
    c.save()

# 透かしを入れて新規ファイルに保存する関数
def new_watermarked_file(file_path, out_dir):

    file_name = os.path.basename(file_path)

    pdf_reader = PdfReader(file_path)

    box = pdf_reader.pages[0].MediaBox # 先頭ページのサイズを取得
    d = datetime.datetime.today()
    ts = d.strftime("%Y%m%d%f")
    watermark_path = os.path.join(out_dir, "{}.pdf".format(ts))

    new_temp_pdf(path=watermark_path, w_pt=float(box[2]), h_pt=float(box[3]), text=file_name)

    watermark = PageMerge().add(PdfReader(watermark_path).pages[0])[0]
    for page in pdf_reader.pages:
        PageMerge(page).add(watermark, prepend=False).render()

    out_path = os.path.join(out_dir, file_name)
    PdfWriter(out_path, trailer=pdf_reader).write()

    return PdfReader(out_path)


def main(odd_file, even_file, out_path):
    writer = PdfWriter()

    with tempfile.TemporaryDirectory() as tmpdir:
        odd_watermarked = new_watermarked_file(odd_file, tmpdir)
        even_watermarked = new_watermarked_file(even_file, tmpdir)

        odd_pages = odd_watermarked.pages
        even_pages = even_watermarked.pages

        counter = 0
        for i, page in enumerate(odd_pages):
            writer.addpage(page)
            if i < len(even_pages):
                writer.addpage(even_pages[i])
            counter = i

        if counter < len(even_pages):
            for j in range(counter+1, len(even_pages)):
                writer.addpage(even_pages[j])

        writer.write(out_path)


if __name__ == '__main__':
    main(*sys.argv[1:4])

reportlab はインチ、 pdfrw はポイントで単位を扱うので相互変換が必要です。

あまり用途としては考えられませんが、2つの文書のページ数が異なる場合は余った文書をそのまま後ろにつなげます(イメージ↓)。

使用方法

奇数ページの PDF を第1引数、偶数ページの PDF を第2引数、出力ファイルを第3引数に指定します。

Python zip-to-diff.py odd_page.pdf even_page.pdf output.pdf

PowerShell ラッパー

例のごとく、普段づかいの PowerShell から呼び出すためのコマンドレットを書きました。
スクリプトと同じ階層に python というディレクトリを作ってその中に上記の .py スクリプトを置いて呼び出します。

function Invoke-PdfZipToDiffWithPython {
    param (
        [string]$oddFile,
        [string]$evenFile,
        [string]$outName = "out"
    )
    $odd = Get-Item $oddFile
    $even = Get-Item $evenFile
    if (($odd.Extension -ne ".pdf") -or ($even.Extension -ne ".pdf")) {
        "non-pdf file!" | Write-Error
        return
    }
    $outPath = $PWD.Path | Join-Path -ChildPath "$($outName).pdf"
    if (Test-Path $outPath) {
        "'{0}.pdf' already exists!" -f $outName | Write-Error
        return
    }
    $pyCodePath = $PSScriptRoot | Join-Path -ChildPath "python\zip-to-diff.py"
    'python -B "{0}" "{1}" "{2}" "{3}"' -f $pyCodePath, $odd.Fullname, $even.Fullname, $outPath | Invoke-Expression
}