技術提案書や見積書などの社外文書には、機密区分を示す「社外秘」の透かしや、会社ロゴのスタンプを入れる運用が一般的です。数通を手作業で処理するのは難しくありませんが、送付する文書が増えてくると、表記の統一と全ページへの漏れのない付与を人手で維持するのは現実的ではありません。
Python と Free Spire.PDF for Python を使うと、この作業をスクリプト化できます。テキスト透かしは PdfTilingBrush で全ページに敷き詰め、グラフィックス変換で回転角と不透明度を制御します。画像スタンプはページキャンバスに重ねて描画し、乗算(Multiply)ブレンドで本文の可読性を保ったまま配置できます。一括処理はファイルのループで実現します。以下、4 ページの技術提案書と 1 ページの見積書を例に進めます。
本記事で使用する Free Spire.PDF for Python は pip でインストールできます。
pip install spire.pdf.free
Free 版は 1 ファイルあたり 10 ページまでです(読み込み・作成の両方に適用)。今回のサンプル文書はこの範囲に収まっています。
1. サンプル文書の準備
まず処理対象となる文書を 2 つ用意します。1 つは各ページに 1 セクションを配置した 4 ページの技術提案書、もう 1 つは明細と合計を含む 1 ページの見積書です。どちらも游ゴシックを使用し、フォント埋め込みを有効にして、閲覧環境によらず表示が一致するようにします。
from spire.pdf.common import *
from spire.pdf import *
TITLE = PdfTrueTypeFont("Yu Gothic", 20.0, PdfFontStyle.Bold, True)
META = PdfTrueTypeFont("Yu Gothic", 10.5, PdfFontStyle.Regular, True)
HEAD = PdfTrueTypeFont("Yu Gothic", 14.0, PdfFontStyle.Bold, True)
BODY = PdfTrueTypeFont("Yu Gothic", 11.0, PdfFontStyle.Regular, True)
BLACK = PdfSolidBrush(PdfRGBColor(Color.get_Black()))
GRAY = PdfSolidBrush(PdfRGBColor(Color.get_Gray()))
RULE = PdfSolidBrush(PdfRGBColor(Color.get_Silver()))
MARGIN = 60.0
def wrap(text, per_line=38):
return [text[i:i + per_line] for i in range(0, len(text), per_line)]
def draw_para(page, text, y, leading=20.0):
for line in wrap(text):
page.Canvas.DrawString(line, BODY, BLACK, MARGIN, y)
y += leading
return y + 8.0
SECTIONS = [
("1. 背景と目的", [
"本提案書は、基幹システムの刷新計画に基づき、ハイブリッドクラウド基盤の構築方針と実施体制を提示するものです。対象範囲は東京・大阪の二拠点に分散する業務システムと、全社共通の運用監視基盤です。",
"現行環境は拠点ごとに個別構築されており、リソース利用率の低下と運用負荷の増大が課題となっています。統合基盤への移行により、運用コストを 3 割削減することを目標とします。"]),
("2. システム構成", [
"基盤はハイブリッドクラウド構成とし、機密性の高い業務はオンプレミス環境に、変動の大きいフロント系はパブリッククラウドに配置します。両環境は専用線で接続し、共通の認証基盤で統制します。",
"コンテナ基盤を採用し、業務アプリケーションのデプロイとスケーリングを自動化します。監視・ログ基盤は全系で共通化し、一次対応の自動化を進めます。"]),
("3. 実施スケジュール", [
"フェーズ 1 で基盤環境の構築と検証を 3 か月で完了させ、フェーズ 2 で基幹系の移行を 6 か月、フェーズ 3 で全系移行と運用移管を 3 か月で実施します。",
"各フェーズの完了時にレビューを実施し、移行範囲と切り戻し手順を関係部門と合意したうえで、次フェーズへ進みます。"]),
("4. 運用サポート体制", [
"運用移管後は、24 時間 365 日の一次受付と、平日日中帯の二次対応を提供します。月次で運用レポートを提出し、インシデント傾向と改善提案を報告します。",
"契約期間は運用開始から 1 年間とし、期間中の継続的な改善活動を含みます。"]),
]
# 技術提案書:1 ページ目にタイトルと第 1 節、以降は 1 節ずつ
doc = PdfDocument()
page = doc.Pages.Add()
page.Canvas.DrawString("クラウド基盤構築 技術提案書", TITLE, BLACK, MARGIN, 90.0)
page.Canvas.DrawString("文書番号:HS-2026-018 作成日:2026-09-21 機密区分:社外秘", META, GRAY, MARGIN, 132.0)
page.Canvas.DrawRectangle(RULE, RectangleF(PointF(MARGIN, 152.0), SizeF(page.Canvas.ClientSize.Width - 2 * MARGIN, 1.2)))
y = 190.0
head, paras = SECTIONS[0]
page.Canvas.DrawString(head, HEAD, BLACK, MARGIN, y)
y += 30.0
for p in paras:
y = draw_para(page, p, y)
for head, paras in SECTIONS[1:]:
page = doc.Pages.Add()
y = 90.0
page.Canvas.DrawString(head, HEAD, BLACK, MARGIN, y)
y += 30.0
for p in paras:
y = draw_para(page, p, y)
doc.SaveToFile("技術提案書.pdf")
doc.Close()
# 見積書:表題、明細、合計
doc2 = PdfDocument()
page = doc2.Pages.Add()
page.Canvas.DrawString("御見積書", TITLE, BLACK, MARGIN, 90.0)
page.Canvas.DrawString("見積番号:HS-QT-2026-033 発行日:2026-09-21 件名:クラウド基盤構築", META, GRAY, MARGIN, 132.0)
page.Canvas.DrawRectangle(RULE, RectangleF(PointF(MARGIN, 152.0), SizeF(page.Canvas.ClientSize.Width - 2 * MARGIN, 1.2)))
cols = [60.0, 115.0, 330.0, 395.0, 480.0]
headers = ["番号", "品目", "数量", "単価(円)", "金額(円)"]
rows = [
("1", "基盤設計・構築", "1", "4,800,000", "4,800,000"),
("2", "移行支援", "1", "3,200,000", "3,200,000"),
("3", "運用初期サポート", "1", "1,600,000", "1,600,000"),
]
TH = PdfTrueTypeFont("Yu Gothic", 11.0, PdfFontStyle.Bold, True)
y = 195.0
for x, h in zip(cols, headers):
page.Canvas.DrawString(h, TH, BLACK, x, y)
page.Canvas.DrawRectangle(RULE, RectangleF(PointF(MARGIN, y + 18.0), SizeF(page.Canvas.ClientSize.Width - 2 * MARGIN, 1.2)))
y += 36.0
for row in rows:
for x, v in zip(cols, row):
page.Canvas.DrawString(v, BODY, BLACK, x, y)
y += 28.0
page.Canvas.DrawRectangle(RULE, RectangleF(PointF(MARGIN, y + 2.0), SizeF(page.Canvas.ClientSize.Width - 2 * MARGIN, 1.2)))
y += 20.0
for label, value in [("小計", "9,600,000"), ("消費税(10%)", "960,000"), ("合計金額", "10,560,000")]:
page.Canvas.DrawString(label, TH, BLACK, 395.0, y)
page.Canvas.DrawString(value, TH, BLACK, 480.0, y)
y += 26.0
y += 24.0
page.Canvas.DrawString("本見積の有効期限は、発行日より 30 日間です。", META, GRAY, MARGIN, y)
doc2.SaveToFile("見積書.pdf")
doc2.Close()
キャンバスの座標はページ左上が原点で、本文の幅は page.Canvas.ClientSize.Width から取得し、左右に 60pt の余白を設けています。DrawString は自動で折り返さないため、段落は 1 行 38 文字で折る wrap 関数を通してから、y 座標を進めながら 1 行ずつ描画します。セクション間の区切り線は高さ 1.2pt の淡色の矩形で描いています。
生成した技術提案書(1 ページ目)は次のとおりです。見積書の明細は一括処理の節で示します。
2. 全ページのテキスト透かし
透かしを全ページに敷き詰める中心になるのが PdfTilingBrush です。これは一定サイズの「タイル」で、対象領域を繰り返し埋める画刷(ブラシ)として機能します。タイルの寸法が透かしの密度を決めます。ページ幅の 1/2、ページ高さの 1/3 に設定すると、1 ページあたり約 6 か所の文字が配置されます。文字はページに直接描くのではなく、タイルのグラフィックス上に描きます。原点をタイル中央に移動し、-30° 回転させてから中央揃えで文字を出力し、そのタイルでページ全体の矩形を塗りつぶす、という流れです。不透明度は 0.28 とし、画面上ではっきり確認でき、本文の読みやすさを損なわない濃さにしています。
PHRASE = "社外秘"
FONT = PdfTrueTypeFont("Yu Gothic", 30.0, PdfFontStyle.Regular, True)
BRUSH = PdfSolidBrush(PdfRGBColor(Color.get_DarkRed()))
def add_text_watermark(pdf_path, out_path, phrase, font, brush, opacity=0.28, angle=-30.0):
doc = PdfDocument()
doc.LoadFromFile(pdf_path)
for i in range(doc.Pages.Count):
page = doc.Pages[i]
tile = PdfTilingBrush(SizeF(page.Canvas.ClientSize.Width / 2.0, page.Canvas.ClientSize.Height / 3.0))
g = tile.Graphics
g.SetTransparency(opacity)
g.Save()
g.TranslateTransform(tile.Size.Width / 2.0, tile.Size.Height / 2.0)
g.RotateTransform(angle)
g.DrawString(phrase, font, brush, 0.0, 0.0, PdfStringFormat(PdfTextAlignment.Center))
g.Restore()
g.SetTransparency(1.0)
page.Canvas.DrawRectangle(tile, RectangleF(PointF(0.0, 0.0), page.Canvas.ClientSize))
n = doc.Pages.Count
doc.SaveToFile(out_path)
doc.Close()
return n
add_text_watermark("技術提案書.pdf", "技術提案書_テキスト透かし.pdf", PHRASE, FONT, BRUSH)
関数は文書の全ページに同じ塗りつぶしを行うため、ページごとの角度・間隔・不透明度は完全に揃い、ページ間でずれが生じません。文字列・フォント・色は引数で渡す構成なので、同じメソッドを「部外秘」や「取扱注意」などの表記に差し替えて使い回せます。
処理後の状態(1 ページ目):
3. 画像スタンプ
画像スタンプは、会社ロゴなどの図形を重ねて押す用途に使います。PNG のロゴを読み込み、ページ幅の 62% に等比拡大して中央に配置し、全ページに重ねて描画します。ポイントは不透明度とブレンド方式です。乗算(Multiply)と不透明度 0.16 の組み合わせでは、ロゴ画像の白い部分は下地の内容を変えず、「透明」と同じ振る舞いをします。ロゴのストロークだけがページの画素を濃くするため、本文の上に重ねても前後の文字が読め、白い矩形が浮くこともありません。
LOGO = "水印素材_北星テクノロジー.png"
doc = PdfDocument()
doc.LoadFromFile("技術提案書.pdf")
pdf_image = PdfImage.FromFile(LOGO)
for i in range(doc.Pages.Count):
page = doc.Pages[i]
w = page.Canvas.ClientSize.Width * 0.62
h = w * pdf_image.Height / pdf_image.Width
x = (page.Canvas.ClientSize.Width - w) / 2.0
y = (page.Canvas.ClientSize.Height - h) / 2.0
page.Canvas.Save()
page.Canvas.SetTransparency(0.16, 0.16, PdfBlendMode.Multiply)
page.Canvas.DrawImage(pdf_image, x, y, w, h)
page.Canvas.Restore()
doc.SaveToFile("技術提案書_画像スタンプ.pdf")
doc.Close()
Save / Restore でブレンド設定の適用範囲をスタンプの描画内に限定し、キャンバス上の他の要素に影響しないようにしています。サンプルのロゴは白地の 1200×420 の PNG です。実際のロゴ画像に差し替える場合は拡大率だけを調整すればよく、他のコードはそのまま使えます。
スタンプを重ねた結果:
4. 一括処理
1 文書分の処理をループに入れると、そのまま一括処理になります。次のスクリプトは前述の add_text_watermark 関数を再利用し、結果を「一括出力」ディレクトリへ書き出します。元のファイルは変更しません。
import os
OUT_DIR = "一括出力"
os.makedirs(OUT_DIR, exist_ok=True)
FILES = ["技術提案書.pdf", "見積書.pdf"]
for name in FILES:
n = add_text_watermark(name, os.path.join(OUT_DIR, name), PHRASE, FONT, BRUSH)
print("一括処理: %s -> 一括出力/(%d ページ)" % (name, n))
print("一括処理完了。")
実行結果:
一括処理: 技術提案書.pdf -> 一括出力/(4 ページ)
一括処理: 見積書.pdf -> 一括出力/(1 ページ)
一括処理完了。
一括出力ディレクトリの見積書です。明細、小計、消費税、合計金額はそのままに、全ページへ透かしが入ります。
5. パラメータと補足
不透明度と色は用途に合わせて調整します。テキスト透かしは 0.2〜0.3、画像スタンプは 0.12〜0.2 が、視認性と可読性のバランスが取りやすい範囲です。色は濃赤や紺灰などの低彩度色を選ぶと、白黒印刷にした場合でもコントラストを確保できます。
フォントは、サンプルのようにシステムフォントのファミリ名で指定するのが簡単です。4 番目の引数 embed=True でフォントのサブセットが PDF に埋め込まれ、閲覧環境のフォント有無に依存しなくなります。ファイルパスで指定する場合は単体の .ttf ファイルを使用してください。.ttc のフォントコレクションには対応していません。
一括処理の前に doc.Pages.Count でページ数を確認しておくと安全です。Free 版の上限(10 ページ)を超える文書を扱う場合は商業版が必要です。
最後に位置づけの整理です。透かしは文書の識別や抑止のための表示であり、改ざんを防ぐ機能そのものではありません。契約書や入札書類など真正性が求められる場面では、透かしに加えてデジタル署名やアクセス権限制御を併用します。
以上で、文書の生成からテキスト透かし・画像スタンプ・一括出力までがひとつの流れになりました。スクリプトは Spire.PDF 以外に依存せず、送付前のチェック工程やアーカイブ処理にそのまま組み込めます。



