はじめに
PythonでPDFに透かしを追加しようとしたら、かなりはまったが最終的にうまくいったのでメモ。
特に以下の点を実現しようとするとなかなか思い通りにできなかった。
- なるべく一般的な(と思われる)Pythonのライブラリを使いたい。
- 透かしの文字は透明にして、斜めに配置したい。
処理の流れ
- ReportLabを使って透かしのPDFを作成する。
- pypdfでこの部分もできれば良かったが、文字を透明にしたり斜めに配置する方法が見つけられなかった。
- pypdfで元のPDFを読み込み、透かしPDFをマージして新たなPDFを作成する。
ReportLabはReportLab社が開発したライブラリだが、有償のReportLab PlusとオープンソースのReportLabがある。ここではオープンソース版だけを利用する。
pypdfはPDFを操作するための、オープンソースのライブラリ。
pypdf2, pypdf3, pypdf4などのバリエーションがあるが、現在はオリジナルのpypdfに取り込まれた様子。https://pypdf.readthedocs.io/en/latest/meta/history.html
手順
ReportLabとpypdfのインストール
pip install reportlab
pip install pypdf
ReportLabで透かしPDFの作成
from reportlab.pdfgen import canvas
import io
buf = io.BytesIO()
can = canvas.Canvas(buf, (width, height))
# can = canvas.Canvas('hoge.pdf', (width, height))
canvasを生成する。
- ReportLabでは白紙ページの「canvas」を用意して、文字や図を入力してPDFのページを作成していく。
- 透かしPDFは一時的にしか使わないので、canvasの保存先には io.BytesIO のバッファ(buf)を指定しておく。
- canvasのサイズは幅と高さを「pt=1/72インチ」で指定する。
- 透かしを追加したいPDFをAcrobat readerなどで開いて、ドキュメントのプロパティーで「13.33 x 7.50 in」のような値を確認して72倍した値(960, 540)。
- ドキュメントのプロパティーの値がmmの単位であれば72/25.4倍。
can.rotate(30) # 30度傾ける
can.setFillColorRGB(1, 0, 0) # 真っ赤
can.setFillAlpha(0.3) # 0~1の範囲、0だと完全に透明
can.setFont('Helvetica-Bold', 48) # フォント名とサイズ
can.drawCentredString(width/2, height/2, 'Confidential document NDA only') # 透かし文字の位置
can.showPage() # ページ入力の終了
can.save() # canvasの保存
次に透かしのテキストを作成する。
- まず透かし文字を斜めにするためにcanvasを30度傾ける。
- テキストの色と透明度を0から1の間の数字で指定する。透明度は0だと完全に透明。
- フォント名とフォントサイズを指定する。
- デフォルトで使えるフォントは限られ、日本語を使う場合はひと手間必要らしい。
- フォントサイズも「pt=1/72インチ」で指定する。
- 次いで透かし文字の位置と、透かし文字自体を指定する。
- drawCenterdString: 中央揃え
- drawString: 左揃え
- drawRightString: 右揃え
- showPage()でこのページの入力を終了し、save()で保存する。
buf.seek(0)
from pypdf import PdfWriter
writer = PdfWriter(buf)
writer.write('hoge.pdf')
ここで念のため透かしPDFが正しく作られているか確認する。
- bufをシークしてページ0に戻す。
- pypdfからPdfWriterをインポートし、作成したwriterオブジェクトにbufを渡す。
- writerオブジェクトのwriteメソッドでbufをPDFファイルとして出力できる。
結果。。。中心にテキストを置きたかったが、canvasを30度傾けたせいでずれてしまったらしい。
-30度分テキストの座標を移動させればよいので。
from reportlab.pdfgen import canvas
import io
import math
width, height = (960, 540)
rotate = 30
cos = math.cos(math.radians(-1 * rotate))
sin = math.sin(math.radians(-1 * rotate))
x, y = (width * 0.5, height * 0.5)
x, y = (x * cos - y * sin, x * sin + y * cos) # -30度の座標変換
buf = io.BytesIO()
can = canvas.Canvas(buf, (width, height))
can.rotate(rotate) # 30度傾ける
can.setFillColorRGB(1, 0, 0) # 真っ赤
can.setFillAlpha(0.3) # 0~1の範囲、0だと完全に透明
can.setFont('Helvetica-Bold', 48) # フォント名とサイズ
can.drawCentredString(x, y, 'Confidential document NDA only') # 透かし文字の位置
can.showPage() # ページ入力の終了
can.save() # canvasの保存
buf.seek(0)
from pypdf import PdfWriter
writer = PdfWriter(buf)
writer.write('hoge.pdf')
透かしPDFを元のPDFにマージ
from pypdf import PdfReader
from pypdf import PdfWriter
reader = PdfReader(buf)
wm_page = reader.pages[0]
writer = PdfWriter(clone_from='original.pdf')
for page in writer.pages:
page.merge_page(wm_page, over=True)
writer.write('watermarked.pdf')
- pypdfからPdfReaderとPdfWriterをインポート
- PdfReaderのオブジェクト(reader)にbufを渡し、1ページ目を透かしとして使う。
- PdfWriterのオブジェクト(writer)を元のPDFファイルのクローンで作成する。
- writerの各ページに、readerの1ページ目(透かし)をマージしていく。
- over=Trueは透かしを元のPDFの上に重ねる設定。Falseにすると下に重ねられる。
- 本当は上に重ねるのはStamp、下に重ねるのをWatermarkと呼ぶらしい。
- watermarked.pdfに出力する。
最終的なコード
from pypdf import PdfReader
from pypdf import PdfWriter
def watermark_page(text, color, alpha, font, width, height, rotate):
from reportlab.pdfgen import canvas
import io
import math
font_size = width / len(text) * 1.7
cos = math.cos(math.radians(-1 * rotate))
sin = math.sin(math.radians(-1 * rotate))
x, y = (width * 0.5, height * 0.5)
x, y = (x * cos - y * sin, x * sin + y * cos)
buf = io.BytesIO()
can = canvas.Canvas(buf, (width, height))
can.rotate(rotate)
can.setFillColorRGB(*color)
can.setFillAlpha(alpha)
can.setFont(font, font_size)
can.drawCentredString(x, y, text)
can.showPage()
can.save()
buf.seek(0)
return PdfReader(buf).pages[0]
if __name__ == '__main__':
file = 'original.pdf'
wm_file = file.replace('.pdf', '_wm.pdf')
writer = PdfWriter(clone_from=file)
text = 'Confidential document NDA only'
color = (1, 0, 0)
alpha = 0.3
font = 'Helvetica-Bold'
width = writer.pages[0].mediabox.width
height = writer.pages[0].mediabox.height
rotate = 30
wm_page = watermark_page(text, color, alpha, font, width, height, rotate)
for page in writer.pages:
page.merge_page(wm_page, over=True)
writer.write(wm_file)
- 元のPDFのサイズはmediabox.width、mediabox.heightで取得できる。
- フォントサイズは透かしの文字数に合わせて適当に調整。使うフォントによっては大きすぎるかも。
- 透かしの文字数が多い場合は2行にしたかったが今後の課題。
書き終わってみると、これだけのコードだが素人プログラマにとっては大変でした。
Streamlitからファイルや設定の入力ができるようにしたので、いずれ公開します。