開発

WeasyPrintとは:HTMLとCSSで帳票PDFを作る使い方と日本語フォント・改ページの設定【2026年10月】

WeasyPrintとは:HTMLとCSSで帳票PDFを作る使い方と日本語フォント・改ページの設定【2026年10月】

WeasyPrintは、HTMLとCSSを読み込んでPDFを書き出すPython製のオープンソースです。ブラウザを使わず、印刷向けのCSS仕様に沿って独自にページを組むため、請求書や報告書のような帳票を自社のテンプレートから出す用途に向いています。この記事では、2026年9月公開の70.0系を前提に、インストール方法、請求書をA4のPDFにするコード、ページ番号やヘッダーを出すCSS、日本語フォントの埋め込み、利用者の入力を扱うときの安全設定、wkhtmltopdfやChromium系との使い分けまでを実装者向けに整理します。

まとめ:WeasyPrintは自社テンプレートの帳票向け、JavaScript描画が要る画面は対象外

WeasyPrintが力を発揮するのは、サーバー側でHTMLを組み立て、印刷用CSSで用紙・余白・ページ番号を決める帳票です。Chromiumを抱えないので、Dockerイメージが軽く、Pythonのアプリから関数1つで呼べます。

逆に、グラフをJavaScriptで描く画面や、既存のWebページをそのままPDFにしたい場面には向きません。そこではPlaywrightやGotenbergなどChromium系を選びます。

導入時に押さえる点は3つです。70.0以上を入れること、日本語フォントをサーバーに入れること、利用者の入力を差し込む帳票ではURL fetcherで読み込み先を絞ることです。帳票の出力から保存・検索までをまとめて業務システムに組み込む場合は、文書管理システム開発として要件を一緒に整理すると手戻りを減らせます。

WeasyPrintの仕組みと70.0系の現状、ブラウザを使わないCSS組版

WebKitやGeckoを使わず、自前のレイアウトエンジンで組むPDF生成

GitHubのREADMEは、WeasyPrintを「多数のライブラリを土台にするが、WebKitやGeckoのような完全な描画エンジンには頼らない」と説明しています。CSSのレイアウト処理はPythonで書かれ、最初からページ分割を前提に設計されたものです。文字の配置はPango、PDFの書き出しはpydyfという専用ライブラリが担います。ライセンスはBSD-3-Clauseで、商用の業務システムに組み込んでもソースの公開義務は生じません。

ブラウザエンジンを持たないため、HTML内のscriptは実行されません。画面上でJavaScriptが組み立てる表やグラフは、PDFには出てこないと考えてください。

2026年9月の70.0とPython 3.10以上・Pango 1.44以上

PyPIによると、2026年10月時点の最新版は2026年9月8日公開の70.0です。67.0(2025年12月)、68.0(2026年1月)、69.0(2026年6月)と、年に数回のペースでメジャー番号が上がっています。

公式のFirst Stepsが挙げる前提は、Python 3.10以上とPango 1.44以上です。古いOSでPangoを上げられない場合は、Pangoの新機能を使わない52.5を使うよう案内されています。版を上げる前にはChangelogの互換性の欄を読みます。69.0では--srgbオプションが--output-intent=srgbに置き換わり、Python APIのsrgb引数も廃止されました。

Paged Media・grid・flexの対応範囲と、使えないCSSの見分け方

API Referenceには、CSS仕様ごとの対応状況が載っています。帳票で使う機能は多くが対応済みです。

仕様 対応している主な機能 制限・非対応
Paged Media Level 3 @page・余白ボックス・名前付きページ・size ページ単位のカウンターに一部制限
GCPM 欄外要素・脚注・:nth() element()のstart引数
Generated Content 文字列・参照先カウンター・リーダー 引用符(quotes)
Flexbox flex・align・justify・order 十分にテストされていない
Grid Level 2 fr・反復・最小最大・間隔 サブグリッド・行内グリッド・自動充填

表の欄外要素はrunning elements、文字列はstring-set、参照先カウンターはtarget-counter()です。Gridの反復はrepeat()、最小最大はminmax()、間隔はgapを指します。非対応欄のサブグリッドはsubgrid、行内グリッドはinline-grid、自動充填はrepeat()のauto-fill・auto-fitの表記です。

公式は、flexを「単純な場合は動くが十分にテストされていない」と書いています。明細表はtable要素で組み、flexとgridはヘッダーの左右配置程度に留めると、版を上げたときの崩れを減らせます。

WeasyPrintをインストールして請求書HTMLをA4のPDFに変換する手順

pip・apt・Homebrew・Windows実行ファイルから選ぶインストール方法

入れ方は実行環境で決めます。First Stepsが示す方法は次のとおりです。

  • Linuxのサーバー:仮想環境でpip install weasyprint。Debianでは事前にlibpango-1.0-0・libpangoft2-1.0-0・libharfbuzz-subset0を入れる
  • Debian 11以降・Ubuntu 20.04以降でコマンドだけ使う:apt install weasyprint
  • macOS:brew install weasyprint
  • Windows:公式の実行ファイル。ライブラリとして使う場合はMSYS2でPangoを入れてからpipで入れる

OSのパッケージは版が遅れることがあるため、アプリに組み込むならpipで版を固定するのが確実です。導入後はweasyprint --infoで、WeasyPrintとPangoの版を確認します。Windowsでは、ウイルス対策ソフトが実行ファイルを誤検知することがあると公式が注意しています。

コマンドラインとPythonから請求書HTMLをPDFへ書き出す最小のコード

まずはコマンドで、HTMLファイルからPDFを作れることを確かめます。

weasyprint invoice.html invoice.pdf
# 追加のCSSを当てる場合
weasyprint invoice.html invoice.pdf -s print.css

アプリから呼ぶときは、テンプレートエンジンでHTMLを組み立ててから渡します。Jinja2の自動エスケープを有効にすると、宛名などの入力がHTMLとして解釈されません。

from jinja2 import Environment, FileSystemLoader, select_autoescape
from weasyprint import HTML

env = Environment(
    loader=FileSystemLoader("templates"),
    autoescape=select_autoescape(["html"]),
)
html = env.get_template("invoice.html").render(invoice=invoice)

# base_urlを渡すと、テンプレート内の相対パス(画像・CSS)が解決できる
HTML(string=html, base_url="templates/").write_pdf("invoice.pdf")

HTML()はstring・filename・urlのどれか1つで入力を受け取ります。文字列で渡すと相対パスの起点が分からないため、base_urlを忘れると画像が空になります。Djangoで組む場合も、入力として渡せるのは同じくテンプレートの描画結果です。フレームワークの全体像はDjangoの特徴とMVTの仕組みで解説しています。

Dockerのslimイメージに依存ライブラリと日本語フォントを入れる構成

公式のDockerイメージはありません。First Stepsが紹介するのは第三者が保守するイメージだけなので、業務システムでは自前のDockerfileで組むほうが管理しやすくなります。

FROM python:3.12-slim
RUN apt-get update \
 && apt-get install -y --no-install-recommends \
      libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0 fonts-noto-cjk \
 && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir "weasyprint==70.0" jinja2
# ビルドログに版を残す
RUN weasyprint --info

Chromiumを含まないため、Chromium入りのイメージより小さく収まります。fonts-noto-cjkを忘れると日本語が出ないので、フォントの導入はDockerfileで固定しておきます。

印刷向けCSSで帳票を組む、用紙・ページ番号・ヘッダーと改ページの指定

@pageのsizeとmarginで用紙を決め、余白ボックスにページ番号を出す書き方

WeasyPrintでは、用紙と余白をコマンドの引数ではなくCSSで決めます。CSS Paged Media Module Level 3の@page規則を使い、余白の中にある「余白ボックス」へページ番号を入れます。

@page {
  size: A4;
  margin: 20mm 15mm 18mm;
  @bottom-center {
    content: counter(page) " / " counter(pages);
    font-size: 8pt;
  }
}
@page :first {
  @bottom-center { content: none; }  /* 表紙はページ番号を出さない */
}

counter(page)が現在のページ、counter(pages)が総ページ数です。余白ボックスは@top-leftから@bottom-rightまで16か所あり、日付や帳票番号を右上に出すこともできます。横向きにするならsize: A4 landscapeと書きます。

running elementsでロゴ入りのヘッダーを全ページに繰り返す方法

社名とロゴのように、画像や複数行を含むヘッダーは余白ボックスのcontentに文字列で書けません。CSS GCPMのrunning elementsを使い、本文中の要素をページの余白へ移します。

header.doc-header { position: running(docHeader); }
@page { @top-center { content: element(docHeader); } }

HTML側は、本文の先頭にheader class="doc-header"の要素を置くだけです。string-setを使うと、各ページに「取引先名」のような本文中の文字列を拾って出せます。複数の取引先をまとめて1つのPDFにする場合に便利です。

明細表の行が割れる、合計欄だけ次ページに送られる問題を抑えるCSS

明細の多い請求書では、1行が上下のページに割れたり、合計欄だけが次のページに送られたりします。改ページを制御するプロパティで抑えます。

tr, .total-box { break-inside: avoid; }   /* 行と合計欄を途中で割らない */
.terms { break-before: page; }            /* 約款は必ず新しいページから */
h2 { break-after: avoid; }                /* 見出しだけがページ末尾に残るのを防ぐ */

見出し行はtheadに入れておくと、改ページ後のページにも繰り返して描画されます。70.0のChangelogには、表の分割や罫線の描画に関する修正が並んでいます。古い版で表が崩れる場合は、CSSを直す前に版を上げて再出力するのが近道です。

日本語フォントの埋め込みとPDFサイズ、文字化けを防ぐフォント設定

豆腐になる原因はサーバー上のフォント不足、fc-listで確かめる手順

日本語が□(豆腐)になるとき、原因の大半はサーバーに日本語フォントが入っていないことです。WeasyPrintはPangoとFontconfigを通してOSのフォントを探すため、OSから見えないフォントは使えません。Debian系ならfonts-noto-cjkを入れ、fc-list :lang=jaで1件以上出ることを確かめます。

CSSのfont-familyには「Noto Sans CJK JP」のように、実際に入れたフォント名を書きます。開発機のmacOSやWindowsにあるフォント名を書いたままだと、サーバーでは別のフォントに置き換わり、改行位置やページ数が変わります。ページ数のずれは帳票の不具合として扱われやすいので、フォントは開発機とサーバーで揃えてください。

@font-face指定時のFontConfigurationの渡し方

ブランド用の書体などOSに入れないフォントは、@font-faceでファイルを指定します。API Referenceは、@font-faceを扱うときはFontConfigurationをCSSとwrite_pdf()の両方に同じものを渡すよう定めています。

from weasyprint import CSS, HTML
from weasyprint.text.fonts import FontConfiguration

font_config = FontConfiguration()
css = CSS(string="""
@font-face { font-family: Brand; src: url(fonts/brand.woff2); }
body { font-family: Brand, "Noto Sans CJK JP", sans-serif; }
""", base_url="templates/", font_config=font_config)

HTML("templates/invoice.html").write_pdf(
    "invoice.pdf", stylesheets=[css], font_config=font_config)

フォントの読み込みに失敗しても処理は止まらず、次の候補のフォントで描画されます。font-familyの末尾にOSの日本語フォントを並べておくと、指定ミスがあっても文字が消えません。

既定のサブセット埋め込みと、–full-fonts・画像の圧縮でのサイズ調整

WeasyPrintは既定で、PDFに使った文字だけをフォントから切り出して埋め込みます。日本語フォントは1ファイルが数十MBありますが、帳票1枚のPDFが数百KBに収まるのはこのためです。後からPDFを編集する運用でフォント全体が必要なら、--full-fontsで丸ごと埋め込めます。

ロゴや押印の画像で重くなる場合は、Common Use Casesの例にあるとおり、write_pdf()にoptimize_images=True・jpeg_quality=60・dpi=150を渡して画像を圧縮します。同じ画像を何度も使う一括出力では、cache引数に辞書を渡すと画像の読み込みが1回で済みます。

利用者の入力を含む帳票で守る、URL fetcherの制限と70.0以上への更新

公式が警告する信頼できないHTMLの危険と、2026年に修正された3件のCVE

First Stepsは、信頼できないHTMLやCSSを処理するとさまざまなセキュリティ問題につながると警告しています。挙げられているのは、file://経由でサーバー上のファイルがPDFに入る、巨大なページサイズ指定で長時間・大量メモリの処理になる、フォント一覧やネットワーク構成が漏れる、といった経路です。

Changelogでは、リリースごとにセキュリティ修正が入っています。68.0でCVE-2025-68616(HTTPリダイレクトもURL fetcherを通す)、69.0でCVE-2026-49452(--presentational-hints使用時のCSSインジェクション)、70.0でCVE-2026-55073(EPS画像を描画しない)が修正されました。利用者の入力を扱うなら70.0以上が前提です。社内の別サーバーへリクエストが飛ぶ危険の仕組みはSSRFの仕組みとクラウドメタデータ悪用の防御実装で解説しています。

読み込み先を帳票用アセットのフォルダだけに絞るURLFetcherの実装例

公式は、信頼できない入力を扱う場合にfile://を許さないか、パスで絞るカスタムのURL fetcherを使うよう勧めています。68.0以降はURLFetcherクラスを継承し、fetch()を上書きします。

from pathlib import Path
from urllib.parse import unquote, urlparse
from weasyprint import HTML
from weasyprint.urls import URLFetcher

ASSETS = Path("templates/assets").resolve()

class AssetsOnlyFetcher(URLFetcher):
    def fetch(self, url, headers=None):
        parsed = urlparse(url)
        if parsed.scheme == "data":
            return super().fetch(url, headers)
        if parsed.scheme != "file":
            raise ValueError(f"blocked: {url}")
        path = Path(unquote(parsed.path)).resolve()
        if not path.is_relative_to(ASSETS):
            raise ValueError(f"blocked: {url}")
        return super().fetch(url, headers)

HTML(string=html, base_url=str(ASSETS) + "/",
     url_fetcher=AssetsOnlyFetcher(timeout=5)).write_pdf("invoice.pdf")

外部への通信をすべて止め、ロゴやCSSはassetsフォルダからだけ読みます。既定のタイムアウトは10秒です。加えて、変換処理のプロセスにメモリと実行時間の上限をかけると、悪意のあるCSSで処理が止まらない事態を防げます。

WeasyPrintの採否条件とwkhtmltopdf・Chromium系の比較

帳票別のWeasyPrint・wkhtmltopdf等の選定と比較

HTMLからPDFを作る道具は、描画エンジンの違いで向く帳票が分かれます。

ツール 描画エンジン JavaScript 向く用途
WeasyPrint 70.0系 独自(CSS組版) 実行しない 自社テンプレートの請求書・報告書
wkhtmltopdf 0.12.6 Qt WebKit 実行する 既存資産の維持のみ(開発終了)
Playwright・Puppeteer Chromium 実行する グラフ入りの画面・既存ページの保存
Gotenberg 8系 Chromium・LibreOffice 実行する 複数システムから共通で呼ぶPDF変換

印刷用CSSの対応は、ページ組版を主目的にしたWeasyPrintのほうが厚く、running elementsや脚注、target-counter()による目次のページ番号はChromium系では素直に書けません。wkhtmltopdfからの移行を検討している場合は、wkhtmltopdfの開発終了後の現状と移行先の選び方で、使い続ける条件から整理しています。Chromium系で組む手順はPuppeteerのpage.pdfの設定とAWS Lambdaでの動かし方が参考になります。

採用してよいのはサーバー側で完結するテンプレート帳票、見送る3つの場面

判断を言い切ります。WeasyPrintを採用してよいのは、HTMLをサーバー側のテンプレートとDBの値だけで組み立て、レイアウトを印刷用CSSで決められる帳票です。請求書・納品書・見積書・検査報告書・月次レポートがこれに当たります。

次の3つの場面では採用しません。1つ目は、ChartやReactなどJavaScriptで描く画面をそのままPDFにしたい場合です。2つ目は、画面と1ピクセル単位で同じ見た目が求められる場合で、ブラウザと描画エンジンが違う以上、差は必ず出ます。3つ目は、Pythonを動かせない環境から呼ぶ場合で、HTTP APIで呼べるGotenbergのほうが運用は軽くなります。

なお、MarkdownやWord形式からPDFを作りたいだけなら、PandocでMarkdownからPDFへ変換する手順のほうが近道です。PandocもPDF生成の仕組みとしてWeasyPrintを指定できます。

PDF/A-3bでの保存と、帳票出力を業務システムに組み込むときの発注範囲

長期保存を前提にした帳票なら、PDF/Aで出力できます。write_pdf("invoice.pdf", pdf_variant="pdf/a-3b")またはコマンドの--pdf-variantで指定し、PDF/UA-1やPDF/X-4も選択可能です。ただしAPI Referenceは、生成したPDFが規格に適合する保証は無く、仕様に沿っているかの確認は利用者の責任だと明記しています。外部の検証ツールでの確認を工程に入れてください。

工数を大きく左右するのは、ライブラリの選定よりテンプレートの数と保存の要件です。帳票が数種類なら社内で組めます。数十種類あり、電子帳簿保存法に沿った保存や取引先への配信まで絡む場合は、出力・配信・保存をまとめて設計し直す時期です。進め方は帳票電子化の進め方:出力・配信・保存の順序と電子帳簿保存法の要件で整理しています。一創では、帳票テンプレートの作成から保存・検索の仕組みまでを文書管理システム開発として受託している範囲です。請求書の発行業務ごと見直すなら、請求書発行システムの比較と選定軸も判断材料になります。

よくある質問

WeasyPrintの導入や帳票づくりで寄せられやすい質問をまとめました。

WeasyPrintは無料で商用利用できますか?

できます。WeasyPrintのライセンスはBSD-3-Clauseで、利用料はかかりません。業務システムに組み込んで販売しても、自社のソースコードを公開する義務はありません。条件は、配布物に著作権表示とライセンス文を含めることです。依存するPangoはLGPLのため、WeasyPrintをWindowsの実行ファイルなどに同梱して配布する場合は、各ライブラリのライセンス表示もあわせて確認してください。

WindowsでWeasyPrintを使うにはどうすればよいですか?

コマンドとして使うだけなら、公式が配布するWindows向けの実行ファイルが手軽です。70.0からは、フォルダごと配置する形式の実行ファイルも用意されました。Pythonのライブラリとして使う場合は、MSYS2のUCRT64シェルでpacman -S mingw-w64-ucrt-x86_64-pangoを実行してPangoを入れ、仮想環境でpython -m pip install weasyprintを実行します。本番がLinuxなら、開発もDockerで揃えるとフォントの差が出にくくなります。

WeasyPrintで日本語が文字化けするのはなぜですか?

□や空白になるなら、サーバーに日本語フォントが入っていないのが原因です。Debian系ではfonts-noto-cjkを入れ、fc-list :lang=jaで確認してください。意味の通らない記号になる場合は文字コードの取り違えなので、HTMLの先頭に<meta charset="utf-8">を書くか、HTML()のencoding引数で指定します。CSSのfont-familyにはサーバーに入れたフォント名を書きます。

WeasyPrintでヘッダーやフッターにページ番号を入れる方法は?

CSSの@page規則で余白ボックスを指定します。たとえば@bottom-center { content: counter(page) " / " counter(pages); }と書くと、下中央に出るのは「現在のページ/総ページ数」です。ロゴ画像を含むヘッダーは、本文の要素にposition: running()を指定し、@top-centerのcontent: element()で呼び出します。表紙だけ番号を消すなら@page :firstでcontent: noneを指定します。

WeasyPrintでJavaScriptのグラフはPDFに出せますか?

そのままでは出せません。WeasyPrintはブラウザエンジンを持たず、HTML内のscriptを実行しないためです。グラフを入れたい場合は、Pythonのmatplotlibなどでサーバー側でSVGやPNGの画像を作り、imgタグで埋め込みます。WeasyPrintはSVGを描画できるため、SVGで出すと拡大しても線がぼやけません。画面のグラフをそのまま使いたいなら、PlaywrightやPuppeteerなどChromium系の道具を選んでください。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.29 テックブログ ソニー生命の不正アクセス:38.5万件の対象確認と窓口、停止中の手続き
  2. 2026.10.28 テックブログ リンツ不正アクセス:314万件の対象確認とパスワード強制リセット後の対処
  3. 2026.10.27 テックブログ Joshin(ジョーシン)不正アクセス:最大600万件の対象とサブスク初期パスワードの対処
  4. 2026.01.30 コラム 税務調査は個人と法人で何が違う?担当部署・件数・調べられる点と追徴税額を比較【令和6事務年度】
  5. 2026.10.31 テックブログ 無印良品の情報漏洩:MUJIアプリ最大2,726万件の対象確認となりすましメール対策

RELATED POSTS 関連記事

目次