Portable Document Format (PDF) は、WYSIWYG (What You See is What You Get) フォーマットではありません。
プラットフォームに依存せず、基盤となるオペレーティングシステムやレンダリングエンジンから独立したフォーマットとして開発されました。
これを実現するために、PDFはプログラミング言語のようなもので操作できるように構築されており、結果を得るためには一連の命令と操作に依存することになります。
実際、PDFはスクリプト言語であるPostScriptをベースにしています。
PostScriptは、デバイスに依存しない最初のページ記述言語でした。
このガイドでは、PDF文書の読み取り、操作、生成に特化したPythonのライブラリであるborbを使用することにします。
borbは低レベルモデル(正確な座標やレイアウトにアクセスできる)と高レベルモデル(余白や位置などの正確な計算をレイアウトマネージャーに委ねることができる)の両方を提供します。
ここでは、Pythonでborbを使用してPDFの請求書を作成する方法を見ていきます。
borbのインストール
borbはGitHubのソースからダウンロードするか、pip
経由でインストールすることができます。
$ pip install borb
Pythonでborbを使ってPDFの請求書を作成する。
borbには直感的なキークラスである Document
と Page
があり、これらはドキュメントとその中のページを表現します。
さらに、 PDF
クラスは、作成した Document
をロードしたり保存したりするための API を表します。
それでは、請求書を追加するための白紙の状態として Document()
と Page()
を作成しましょう。
from borb.pdf.document import Document
from borb.pdf.page.page import Page
# Create document
pdf = Document()
# Add page
page = Page()
pdf.append_page(page)
座標の計算をしたくないので、すべてのコンテンツとその位置を管理する PageLayout
に委ねることができます。
# New imports
from borb.pdf.canvas.layout.page_layout.multi_column_layout import SingleColumnLayout
from decimal import Decimal
page_layout = SingleColumnLayout(page)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)
ここでは、すべてのコンテンツが1つのカラムになるように SingleColumnLayout
を使用しています。
また、ここでは垂直方向のマージンを小さくしています。
デフォルトの値では、ページの高さの上部10%がマージンとして切り取られていますが、会社のロゴや名前にこのスペースを使いたいので、これを2%に減らしています。
そういえば、会社のロゴをレイアウトに追加してみましょう。
# New import
from borb.pdf.canvas.layout.image.image import Image
page_layout.add(
Image(
"https://s3.stackabuse.com/media/articles/creating-an-invoice-in-python-with-ptext-1.png",
width=Decimal(128),
height=Decimal(128),
))
ここでは、レイアウトに要素を追加しています – Image()
です。
そのコンストラクタを通じて、画像リソースを指すURLを追加し、width
とheight
を設定しています。
画像の下には、架空の会社情報(名前、住所、ウェブサイト、電話番号)と請求書情報(請求書番号、日付、支払期日)を追加します。
請求書のデータを格納するためにテーブルを使用するのが、簡略化のための一般的な方法です(ついでにコードもすっきりします)。
請求書情報をテーブルに格納するヘルパーメソッドを別途作成し、それを使ってメインメソッドで請求書にテーブルを追加するだけでよいようにしましょう。
# New imports
from borb.pdf.canvas.layout.table.fixed_column_width_table import FixedColumnWidthTable as Table
from borb.pdf.canvas.layout.text.paragraph import Paragraph
from borb.pdf.canvas.layout.layout_element import Alignment
from datetime import datetime
import random
def _build_invoice_information():
table_001 = Table(number_of_rows=5, number_of_columns=3)
table_001.add(Paragraph("[Street Address]"))
table_001.add(Paragraph("Date", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT))
now = datetime.now()
table_001.add(Paragraph("%d/%d/%d" % (now.day, now.month, now.year)))
table_001.add(Paragraph("[City, State, ZIP Code]"))
table_001.add(Paragraph("Invoice #", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT))
table_001.add(Paragraph("%d" % random.randint(1000, 10000)))
table_001.add(Paragraph("[Phone]"))
table_001.add(Paragraph("Due Date", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT))
table_001.add(Paragraph("%d/%d/%d" % (now.day, now.month, now.year)))
table_001.add(Paragraph("[Email Address]"))
table_001.add(Paragraph(" "))
table_001.add(Paragraph(" "))
table_001.add(Paragraph("[Company Website]"))
table_001.add(Paragraph(" "))
table_001.add(Paragraph(" "))
table_001.set_padding_on_all_cells(Decimal(2), Decimal(2), Decimal(2), Decimal(2))
table_001.no_borders()
return table_001
ここでは、5行3列のシンプルな Table
を作成します。
行は、住所、都道府県、電話番号、メールアドレス、会社のウェブサイトに対応しています。
各行は 0..3
の値 (カラム) を持つことになります。
各テキスト要素は Paragraph
として追加され、 Alignment.RIGHT
によって右寄せにし、 font
などのスタイリング引数を受け付けるようにしました。
最後に、すべてのセルにパディングを追加して、セルの境界付近にテキストが不格好に配置されないようにしました。
さて、メインメソッドに戻って、 _build_invoice_information()
を呼び出してテーブルを作成し、それをレイアウトに追加します。
page_layout = SingleColumnLayout(page)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)
page_layout.add(
Image(
"https://s3.stackabuse.com/media/articles/creating-an-invoice-in-python-with-ptext-1.png",
width=Decimal(128),
height=Decimal(128),
))
# Invoice information table
page_layout.add(_build_invoice_information())
# Empty paragraph for spacing
page_layout.add(Paragraph(" "))
さて、このPDFドキュメントがどのようなものかを見るために、本当に素早くビルドしてみましょう。
これには PDF
モジュールを使用します。
# New import
from borb.pdf.pdf import PDF
with open("output.pdf", "wb") as pdf_file_handle:
PDF.dumps(pdf_file_handle, pdf)
素晴らしい! さて、請求先と配送先の情報も追加したいと思います。
これは会社情報と同じようにテーブルの中に置くと便利です。
簡潔にするために、この情報を作成するヘルパー関数を別に作り、それをメインメソッドに追加することにします。
# New imports
from borb.pdf.canvas.color.color import HexColor, X11Color
def _build_billing_and_shipping_information():
table_001 = Table(number_of_rows=6, number_of_columns=2)
table_001.add(
Paragraph(
"BILL TO",
background_color=HexColor("263238"),
font_color=X11Color("White"),
)
)
table_001.add(
Paragraph(
"SHIP TO",
background_color=HexColor("263238"),
font_color=X11Color("White"),
)
)
table_001.add(Paragraph("[Recipient Name]")) # BILLING
table_001.add(Paragraph("[Recipient Name]")) # SHIPPING
table_001.add(Paragraph("[Company Name]")) # BILLING
table_001.add(Paragraph("[Company Name]")) # SHIPPING
table_001.add(Paragraph("[Street Address]")) # BILLING
table_001.add(Paragraph("[Street Address]")) # SHIPPING
table_001.add(Paragraph("[City, State, ZIP Code]")) # BILLING
table_001.add(Paragraph("[City, State, ZIP Code]")) # SHIPPING
table_001.add(Paragraph("[Phone]")) # BILLING
table_001.add(Paragraph("[Phone]")) # SHIPPING
table_001.set_padding_on_all_cells(Decimal(2), Decimal(2), Decimal(2), Decimal(2))
table_001.no_borders()
return table_001
最初の段落の background_color
はロゴの色に合わせて #263238
(グレーブルー) に設定し、 font_color
は White
に設定しています。
これをmainメソッド内でも呼び出してみましょう。
# Invoice information table
page_layout.add(_build_invoice_information())
# Empty paragraph for spacing
page_layout.add(Paragraph(" "))
# Billing and shipping information table
page_layout.add(_build_billing_and_shipping_information())
もう一度スクリプトを実行すると、より多くの情報を含む新しいPDFファイルができあがります。
基本的な情報(会社情報、請求書/配送情報)が整理されたので、項目別の説明を追加したいと思います。
これは、私たちの会社が誰かに提供した商品/サービスであり、通常、すでに追加した情報の下にテーブルのような方法で行われます。
繰り返しますが、テーブルを生成してデータを入力するヘルパー関数を作成し、後でレイアウトに追加することができます。
# New import
from borb.pdf.canvas.layout.table.fixed_column_width_table import FixedColumnWidthTable as Table
from borb.pdf.canvas.layout.table.table import TableCell
def _build_itemized_description_table(self):
table_001 = Table(number_of_rows=15, number_of_columns=4)
for h in ["DESCRIPTION", "QTY", "UNIT PRICE", "AMOUNT"]:
table_001.add(
TableCell(
Paragraph(h, font_color=X11Color("White")),
background_color=HexColor("016934"),
)
)
odd_color = HexColor("BBBBBB")
even_color = HexColor("FFFFFF")
for row_number, item in enumerate([("Product 1", 2, 50), ("Product 2", 4, 60), ("Labor", 14, 60)]):
c = even_color if row_number % 2 == 0 else odd_color
table_001.add(TableCell(Paragraph(item[0]), background_color=c))
table_001.add(TableCell(Paragraph(str(item[1])), background_color=c))
table_001.add(TableCell(Paragraph("$ " + str(item[2])), background_color=c))
table_001.add(TableCell(Paragraph("$ " + str(item[1] * item[2])), background_color=c))
# Optionally add some empty rows to have a fixed number of rows for styling purposes
for row_number in range(3, 10):
c = even_color if row_number % 2 == 0 else odd_color
for _ in range(0, 4):
table_001.add(TableCell(Paragraph(" "), background_color=c))
table_001.add(TableCell(Paragraph("Subtotal", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT,), col_span=3,))
table_001.add(TableCell(Paragraph("$ 1,180.00", horizontal_alignment=Alignment.RIGHT)))
table_001.add(TableCell(Paragraph("Discounts", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT,),col_span=3,))
table_001.add(TableCell(Paragraph("$ 177.00", horizontal_alignment=Alignment.RIGHT)))
table_001.add(TableCell(Paragraph("Taxes", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT), col_span=3,))
table_001.add(TableCell(Paragraph("$ 100.30", horizontal_alignment=Alignment.RIGHT)))
table_001.add(TableCell(Paragraph("Total", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT ), col_span=3,))
table_001.add(TableCell(Paragraph("$ 1163.30", horizontal_alignment=Alignment.RIGHT)))
table_001.set_padding_on_all_cells(Decimal(2), Decimal(2), Decimal(2), Decimal(2))
table_001.no_borders()
return table_001
実際には、小計、税金、合計価格に関するハードコードされた文字列を、実際の価格の計算で置き換えることになるでしょう。
しかし、これは Product
モデルの基本的な実装に大きく依存するので、抽象化のために代用品を追加しました。
このテーブルをドキュメントに追加したら、それを再構築して見てみましょう。
メインメソッド全体は、次のような感じになります。
# Create document
pdf = Document()
# Add page
page = Page()
pdf.append_page(page)
page_layout = SingleColumnLayout(page)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)
page_layout.add(
Image(
"https://s3.stackabuse.com/media/articles/creating-an-invoice-in-python-with-ptext-1.png",
width=Decimal(128),
height=Decimal(128),
))
# Invoice information table
page_layout.add(_build_invoice_information())
# Empty paragraph for spacing
page_layout.add(Paragraph(" "))
# Billing and shipping information table
page_layout.add(_build_billing_and_shipping_information())
# Itemized description
page_layout.add(_build_itemized_description_table())
with open("output2.pdf", "wb") as pdf_file_handle:
PDF.dumps(pdf_file_handle, pdf)
このコード片を実行すると
アウトラインの作成
PDFは完成し、提供する準備が整いました。
しかし、2つの小さな追加をすることで、より良いものにすることができます。
これはAdobeのようなリーダーがPDFをナビゲートしたり、メニューを生成したりするのに役立ちます。
# New import
from borb.pdf.page.page import DestinationType
# Outline
pdf.add_outline("Your Invoice", 0, DestinationType.FIT, page_nr=0)
add_outline()`関数は、いくつかの引数を受け取ります。
-
title
: サイドメニューに表示されるタイトル -
level
: ツリーのどの部分まで表示するか。レベル0はルートレベルです。 - “destination “を構成するいくつかの引数
デスティネーションはハイパーリンクのターゲットと考えることができます。
ページ全体にリンクすることもできますが(この例ではそうしています)、ページの特定の部分にリンクすることもできます(たとえば、y座標350の部分にぴったりとリンクします)。
さらに、読者がそのページをどのように表示すべきかを指定する必要があります。
たとえば、そのページまで単にスクロールしてズームしないようにするのか?例えば、そのページまで単純にスクロールして、ズームはしないようにしたいのか、対象領域だけを表示し、読者はその特定の領域に完全にズームインするようにしたいのか。
このコードでは、ページ0(最初のページ)を表示し、それがリーダーのウィンドウに収まるようにする(必要に応じてズームイン/ズームアウトする)ことをリーダーに要求しているのです。
アウトラインを追加したら、選択したリーダーで表示されるのを確認できるはずです。
複数のページがある場合、より複雑なアウトラインを作成して add_outline()
からリンクさせると、ナビゲーションが容易になります。
PDF請求書へのJSONドキュメントの埋め込み
PDFはコンピュータにあまりやさしくないので、請求書を自動処理する場合、よりコンピュータにやさしい形式を追加したいことがあります。
ドイツ発祥のZUGFeRD(後にEUで採用)という請求書の規格では、請求書を記述するXMLなど、よりコンピュータで読みやすいファイル形式で請求書をPDF化し、簡単にパースできるようになっています。
これらに加えて、条件や契約、返金規定など、請求書に関連する他の文書を埋め込むことも可能です。
borbを使って、PDFファイルに何らかの追加ファイルを埋め込むには、append_embedded_file()
関数を使用します。
まず、請求書のデータをJSONで保存するための辞書を作成し、それを invoice_json
ファイルに保存してみましょう。
import json
# Creating a JSON file
invoice_json = {
"items": [
{
"Description": "Product1",
"Quantity": 2,
"Unit Price": 50,
"Amount": 100,
},
{
"Description": "Product2",
"Quantity": 4,
"Unit Price": 60,
"Amount": 100,
},
{
"Description": "Labor",
"Quantity": 14,
"Unit Price": 60,
"Amount": 100,
},
],
"Subtotal": 1180,
"Discounts": 177,
"Taxes": 100.30,
"Total": 1163.30,
}
invoice_json_bytes = bytes(json.dumps(invoice_json, indent=4), encoding="latin1")
このファイルをPDFの請求書に埋め込めばよいのです。
pdf.append_embedded_file("invoice.json", invoice_json_bytes)
もう一度スクリプトを実行し、ドキュメントを保存したら、完了です。
結論
このガイドでは、Pythonでborbを使用して請求書を作成する方法について見てきました。
次に、ナビゲーションが簡単になるようにPDFファイルにアウトラインを追加し、PDFの内容にプログラムからアクセスするために添付ファイルや埋め込みファイルを追加する方法について見てきました。