PDF(Portable Document Format)は、WYSIWYG(What You See is What You Get)形式ではありません。
プラットフォームに依存せず、基盤となるオペレーティングシステムやレンダリングエンジンに依存しないように開発されました。
これを実現するために、PDFはプログラミング言語のようなもので操作できるように構築されており、結果を得るためには一連の命令と操作に依存することになります。
実際、PDFはスクリプト言語であるPostScriptをベースにしています。
PostScriptは、デバイスに依存しない最初のページ記述言語でした。
この言語には、グラフィックの状態を変更する演算子があり、大まかには次のようなものです。
- フォントを “Helvetica “に設定する。
- 黒にストロークカラーを設定する
- (60,700)に移動
- グリフ “H “を描画する
これで、いくつかのことがわかります。
- PDFからテキストを曖昧さなく抽出することが困難な理由
- PDF文書を編集することが困難な理由
- ほとんどのPDFライブラリは、コンテンツ作成に非常に低レベルなアプローチを強制する理由(プログラマーは、テキストをレンダリングする座標、余白などを指定しなければならない)。
このガイドでは、PDF文書の読み取り、操作、生成に特化したPythonライブラリであるborbを使用して、PDF文書を作成することにします。
borbは低レベルモデル(正確な座標やレイアウトにアクセスできる)と高レベルモデル(余白や位置などの正確な計算をレイアウトマネージャーに委ねることができる)の両方を提供します。
ここでは、Pythonでborbを使ってPDFドキュメントを作成し、検査する方法と、LayoutElements
のいくつかを使ってバーコードやテーブルを追加する方法について見ていきます。
borbのインストール
borbはGitHubのソースからダウンロードするか、pip
経由でインストールすることができます。
$ pip install borb
Pythonでborbを使ったPDFドキュメントの作成方法
borbには直感的なキークラスである Document
と Page
があり、これらはドキュメントとその中のページを表します。
これらは、PDFドキュメントを作成するための主要なフレームワークです。
さらに、 PDF
クラスは作成した Document
を読み込んだり保存したりするための API を表しています。
このことを念頭に置いて、空のPDFファイルを作成してみましょう。
from borb.pdf.document import Document
from borb.pdf.page.page import Page
from borb.pdf.pdf import PDF
# Create an empty Document
document = Document()
# Create an empty page
page = Page()
# Add the Page to the Document
document.append_page(page)
# Write the Document to a file
with open("output.pdf", "wb") as pdf_file_handle:
PDF.dumps(pdf_file_handle, document)
コードの大部分はここで説明されています。
まず、空の Document
を作成し、次に append()
関数で空の Page
を Document
に追加して、最後に PDF.dumps()
でファイルを保存しています。
注目すべきは、Pythonにこのテキストをエンコードさせたくないので、バイナリモードで書き込むために "wb"
フラグを使用したことです。
この結果、ローカルファイルシステム上に output.pdf
という名前の空のPDFファイルが生成されます。
borbで「Hello World」ドキュメントを作成する
もちろん、空の PDF ドキュメントでは多くの情報を伝えることはできません。
そこで、 Document
インスタンスに追加する前に、 Page
に何らかのコンテンツを追加してみましょう。
先ほどの2つのインテグラルクラスと同じように、 Page
にコンテンツを追加するために、表示したいレイアウトのタイプを指定する PageLayout
を追加して、そのレイアウトに1つまたは複数の Paragraph
を追加します。
そのために、 Document
はオブジェクトの階層の最下層に位置し、 Paragraph
は最上層のインスタンスで PageLayout
の上に積み重なり、結果として Page
になります。
それでは、Paragraph
を Page
に追加してみましょう。
from borb.pdf.document import Document
from borb.pdf.page.page import Page
from borb.pdf.pdf import PDF
from borb.pdf.canvas.layout.paragraph import Paragraph
from borb.pdf.canvas.layout.page_layout.multi_column_layout import SingleColumnLayout
from borb.io.read.types import Decimal
document = Document()
page = Page()
# Setting a layout manager on the Page
layout = SingleColumnLayout(page)
# Adding a Paragraph to the Page
layout.add(Paragraph("Hello World", font_size=Decimal(20), font="Helvetica"))
document.append_page(page)
with open("output.pdf", "wb") as pdf_file_handle:
PDF.dumps(pdf_file_handle, document)
2つのオブジェクトが追加されていることに気がつくでしょう。
- このクラスは、
Page
のどこにコンテンツが追加されているか、どの領域が今後のコンテンツに使用できるか、Page
のマージンはどうなっているか、そしてリーディング(Paragraph
オブジェクト間のスペース)はどうなっているか、ということを記録しています。
ここでは1つのカラムだけを扱うので、SingleColumnLayout
を使用しています。
別の方法として、 MultiColumnLayout
を使用することもできます。
-
Paragraph
インスタンス: このクラスはテキストのブロックを表します。フォント、font_size、font_colorなどのプロパティを設定することができます。より多くの例については、ドキュメントを参照してください。
これで、私たちの Paragraph
を含む output.pdf
ファイルが生成されました。
borbによる生成されたPDFの検査
注意: PDFドキュメントの内部構造に興味がなければ、このセクションは完全に省略可能です。
しかし、この形式について少し知っておくと非常に便利です(古典的な「なぜ私のコンテンツが今このページに表示されるのか」という問題をデバッグするときなど)。
一般的に、PDFリーダーは文書を最後のバイトから読み始めます。
xref
0 11
0000000000 00000 f
0000000015 00000 n
0000002169 00000 n
0000000048 00000 n
0000000105 00000 n
0000000258 00000 n
0000000413 00000 n
0000000445 00000 n
0000000475 00000 n
0000000653 00000 n
0000001938 00000 n
trailer
<<61e6d144af4b84e0e0aa52deab87cfe9>]>>
startxref
2274
%%EOF
ここでは、ファイル終了マーカー(%%EOF
)と相互参照表(通常、xref
と省略されます)が表示されています。
xrefは
“startxref”と
“xref”` というトークンで区切られます。
xref`は、PDFリーダーのルックアップテーブルとして機能します。
これは、PDF内の各オブジェクトのバイトオフセット(ファイルの先頭から始まる)を含んでいます。
xrefの最初の行 (
0 11) は、この
xref` には11個のオブジェクトがあり、最初のオブジェクトは 0 番から始まるということを表しています。
それ以降の各行は、バイトオフセットと、いわゆる世代番号、そして f
または n
という文字で構成されています。
- f` でマークされたオブジェクトはフリーオブジェクトで、レンダリングされる予定はありません。
- n` でマークされたオブジェクトは “使用中” です。
xrefの下部には、トレイラー辞書があります。
PDFの構文では、辞書は<<と
` で区切られます。
この辞書は次のようなペアを持っています。
/Root 1 0 R
/Info 2 0 R
/Size 11
/ID [<61e6d144af4b84e0e0aa52deab87cfe9> <61e6d144af4b84e0e0aa52deab87cfe9>]
トレイラー辞書は、PDFリーダーの出発点であり、他のすべてのデータへの参照を含んでいます。
この場合
-
/Root
: これは、ドキュメントの実際のコンテンツにリンクしている別の辞書です。 -
/Info
: これは、ドキュメントのメタ情報(著者、タイトルなど)を含むディクショナリです。
1 0 Rのような文字列は、PDF の構文では「参照」と呼ばれます。
そして、ここでxref` テーブルが役に立ちます。
1 0 R` に関連するオブジェクトを見つけるには、オブジェクト1(世代番号0)を調べます。
xref` ルックアップテーブルは、このオブジェクトがドキュメントのバイト15にあることを教えてくれます。
それをチェックすると、次のようになります。
1 0 obj
<>
endobj
このオブジェクトは 1 0 obj
で始まり、 endobj
で終わっていることに注意してください。
これは、私たちが実際にオブジェクト1を扱っていることを示すもう一つの確認です。
この辞書は、ドキュメントのページがオブジェクト 3 にあることを教えてくれます。
3 0 obj
<>
endobj
これは /Pages
辞書で、このドキュメントには1ページがあることを示しています (/Count
のエントリ)。
Kids` のエントリは典型的な配列で、1ページにつき1つのオブジェクト参照があります。
最初のページはオブジェクト 4 にあると考えられます。
4 0 obj
<>
endobj
この辞書には、いくつかの興味深いエントリが含まれています。
-
/MediaBox
: ページの物理的な大きさ(この場合はA4サイズのページ)。 -
/Contents
: PDF コンテンツ オペレータの (通常圧縮された) ストリームへの参照。 -
/Resources
: このページをレンダリングするために使用されるすべてのリソース(フォント、画像など)を含む辞書への参照。
オブジェクト 5 をチェックして、このページで実際に何がレンダリングされているのかを見てみましょう。
5 0 obj
<>
stream
xÚãR@
È<§`a¥£šÔw3T0É
€!K¡š3Benl7'§999ù
åùE9)
!Y(!8õÂyšT*î
endstream
endobj
先に述べたように、この(コンテンツの)ストリームは圧縮されています。
どの圧縮方法が使われたかは、/Filter
の項目で知ることができます。
オブジェクト 5 に解凍 (unzip
) を適用すると、実際のコンテンツオペレータを取得できるはずです。
5 0 obj
<>
stream
q
BT
0.000000 0.000000 0.000000 rg
/F1 1.000000 Tf
20.000000 0 0 20.000000 60.000000 738.000000 Tm
(Hello world) Tj
ET
Q
endstream
endobj
最後に、コンテンツをデコードできるレベルまで来ました。
それぞれの行は、引数とその演算子で構成されています。
早速、演算子について見ていきましょう。
- q`: 現在のグラフィックの状態を保持します (スタックにプッシュします)。
-
BT
: テキストを開始します。 -
0 0 0 rg
: 現在の描画色を (0,0,0
) rgb に設定します。これは黒です。 -
/F1 1 Tf
: 現在のフォントを/F1
に設定し (これは先ほどのリソース辞書のエントリです)、フォントサイズを1
に設定します。 -
20.000000 0 0 20.000000 60.000000 738.000000 Tm
: テキストマトリクスを設定します。テキストマトリックスについては、独自のガイドが必要です。このマトリックスは、フォントのサイズとテキストの位置を調整するものであると言えば、十分でしょう。ここでは、フォントをfont-size 20
に拡大縮小し、テキストを描画するカーソルを60,738
に設定しています。PDFの座標系はページの左下から始まっています。つまり、60,738
はページの左上付近ということになります(ページの高さが842
単位であることを考慮すると)。 -
(Hello world) Tj
: PDFの構文における文字列は、(
と)
で区切られます。このコマンドは、先ほどテキストマトリクスで指定した位置に、文字列 “Hello world” を、その前のコマンドで指定したフォント、サイズ、色で表示するようにPDFリーダーに指示します。 -
ET
: テキストを終了します。 -
Q
: スタックからグラフィックスの状態をポップアップします(したがって、グラフィックスの状態を復元します)。
他のborb LayoutElementsをページに追加する
borbには様々な
LayoutElementオブジェクトが付属しています。
前の例では、Paragraphについて簡単に説明しました。
しかし、他にもUnorderedList,
OrderedList,
Image,
Shape,
Barcode,
Table` といった要素も用意されています。
少し難しい例ですが、 Table
と Barcode
を使って作ってみましょう。
テーブルは
TableCellから構成され、
Table` のインスタンスに追加します。
バーコードは BarcodeType
と呼ばれるもので、ここでは QR
を使用します。
from borb.pdf.document import Document
from borb.pdf.page.page import Page
from borb.pdf.pdf import PDF
from borb.pdf.canvas.layout.paragraph import Paragraph
from borb.pdf.canvas.layout.page_layout import SingleColumnLayout
from borb.io.read.types import Decimal
from borb.pdf.canvas.layout.table import Table, TableCell
from borb.pdf.canvas.layout.barcode import Barcode, BarcodeType
from borb.pdf.canvas.color.color import X11Color
document = Document()
page = Page()
# Layout
layout = SingleColumnLayout(page)
# Create and add heading
layout.add(Paragraph("DefaultCorp Invoice", font="Helvetica", font_size=Decimal(20)))
# Create and add barcode
layout.add(Barcode(data="0123456789", type=BarcodeType.QR, width=Decimal(64), height=Decimal(64)))
# Create and add table
table = Table(number_of_rows=5, number_of_columns=4)
# Header row
table.add(TableCell(Paragraph("Item", font_color=X11Color("White")), background_color=X11Color("SlateGray")))
table.add(TableCell(Paragraph("Unit Price", font_color=X11Color("White")), background_color=X11Color("SlateGray")))
table.add(TableCell(Paragraph("Amount", font_color=X11Color("White")), background_color=X11Color("SlateGray")))
table.add(TableCell(Paragraph("Price", font_color=X11Color("White")), background_color=X11Color("SlateGray")))
# Data rows
for n in [("Lorem", 4.99, 1), ("Ipsum", 9.99, 2), ("Dolor", 1.99, 3), ("Sit", 1.99, 1)]:
table.add(Paragraph(n[0]))
table.add(Paragraph(str(n[1])))
table.add(Paragraph(str(n[2])))
table.add(Paragraph(str(n[1] * n[2])))
# Set padding
table.set_padding_on_all_cells(Decimal(5), Decimal(5), Decimal(5), Decimal(5))
layout.add(table)
# Append page
document.append_page(page)
# Persist PDF to file
with open("output4.pdf", "wb") as pdf_file_handle:
PDF.dumps(pdf_file_handle, document)
いくつかの実装の詳細です。
- BARCODE
は様々なカラーモデルをサポートしています。また、
RGBColor,
HexColor,
X11Color,
HSVColor` などの様々なカラーモデルをサポートしています。 - テーブルオブジェクトに直接レイアウト要素を追加することもできますが、それらを
TableCell
オブジェクトでラップすることもできます。 - もし、
font
、font_size
、font_color
を指定しなかった場合、Paragraph
はデフォルトでHelvetica
、size 12
、black
を使用します。
この結果は
結論
このガイドでは、PDFファイルを読み書きしたり操作したりするためのライブラリであるborbについて見てきました。
Documentや
Pageといった主要なクラスや、
Paragraph、
Barcode、
PageLayout` といった要素について見てきました。
最後に、様々な内容のPDFファイルをいくつか作成し、PDFがどのようにデータを保存しているかを調べました。