PDF(Portable Document Format)は、WYSIWYG(What You See is What You Get)形式ではありません。
プラットフォームに依存せず、基盤となるオペレーティングシステムやレンダリングエンジンに依存しないように開発されました。
これを実現するために、PDFはプログラミング言語のようなもので操作できるように構築されており、結果を得るためには一連の命令と操作に依存することになります。
実際、PDFはスクリプト言語であるPostScriptをベースにしています。
PostScriptは、デバイスに依存しない最初のページ記述言語でした。
このガイドでは、PDF文書の読み取り、操作、生成に特化したPythonライブラリであるborbを使用します。
borbは、低レベルモデル(正確な座標やレイアウトにアクセスできる)と高レベルモデル(余白や位置などの正確な計算をレイアウトマネージャーに委ねることができる)の両方を提供します。
>
> このガイドでは、(Project Gutenbergから)UTF-8の書籍をPDF文書に変換する方法について見ていきます。
Project Gutenbergの電子書籍は、そのほとんどが米国の著作権法で保護されていないため、米国内では自由に利用することができます。
他の国では著作権で保護されていない可能性があります。
borbのインストール
borbはGitHubのソースからダウンロードするか、pip
経由でインストールすることができます。
$ pip install borb
unidecodeのインストール
このプロジェクトでは unidecode
も使用します。
これはテキストをUTF-8からASCIIに変換する素晴らしい小さなライブラリです。
UTF-8のすべての文字がASCII文字として表現できるわけではないことに留意してください。
これは原則的に非可逆変換なので、変換を行うたびに何らかの不一致が発生します。
$ pip install unidecode
borb による PDF 文書の作成
borbを使用したPDFドキュメントの作成は、通常、毎回同じ手順で行われます。
from borb.pdf.document import Document
from borb.pdf.page.page import Page
from borb.pdf.pdf import PDF
import typing
import re
from borb.pdf.canvas.layout.page_layout.multi_column_layout import SingleColumnLayout
from borb.pdf.canvas.layout.page_layout.page_layout import PageLayout
# Create empty Document
pdf = Document()
# Create empty Page
page = Page()
# Add Page to Document
pdf.append_page(page)
# Create PageLayout
layout: PageLayout = SingleColumnLayout(page)
borbによる電子書籍の作成
注:生のテキストブックを扱うことになります。
各書籍は異なる構造を持ち、各書籍はレンダリングに異なるアプローチを必要とします。
これは非常に主観的(スタイリング)かつ書籍に依存した作業ですが、しかし、一般的なプロセスは同じです。
今回ダウンロードする書籍は、UTF-8でエンコードされています。
すべてのフォントがすべての文字をサポートしているわけではありません。
実際、PDFの仕様では14種類の標準フォントが定義されていますが(すべてのリーダー/ライターが埋め込んでいるはず)、どれもUTF-8の全範囲をサポートしているわけではありません。
そこで、私たちの生活を少し楽にするために、str
をASCIIに変換するこの小さなユーティリティ関数を使うことにしましょう。
from unidecode import unidecode
def to_ascii(s: str) -> str:
s_out: str = ""
for c in s:
if c == '“' or c == '”' or c == 'â':
s_out += '"'
else:
s_out += unidecode(c)
return s_out
次に、メインメソッドで、UTF-8ブックをダウンロードします。
この例ではAgatha Christieの “The Mysterious affair at Styles “を使います。
この本はProject Gutenbergから簡単にrawフォーマットで入手できます。
# Define which ebook to fetch
url = 'https://www.gutenberg.org/files/863/863-0.txt'
# Download text
import requests
txt = requests.get(url).text
print("Downloaded %d bytes of text..." % len(txt))
# Split to lines
lines_of_text: typing.List[str] = re.split('
', txt)
lines_of_text = [to_ascii(x) for x in lines_of_text]
# Debug
print("This ebook contains %d lines... " % len(lines_of_text))
これを印刷する。
Downloaded 361353 bytes of text...
This ebook contains 8892 lines...
最初の行は、Project Gutenbergが追加した一般的なヘッダーです。
私たちの電子書籍には必要ないので、行頭があるパターンで始まっているかどうかをチェックし、スライス記法で切り取ることによって、単純に削除することにします。
# Skip header
header_offset: int = 0
for i in range(0, len(lines_of_text)):
if lines_of_text[i].startswith("*** START OF THE PROJECT GUTENBERG EBOOK"):
header_offset = i + 1
break
while lines_of_text[header_offset].isspace():
header_offset += 1
lines_of_text = lines_of_text[header_offset:]
print("The first %d lines are the gutenberg header..." % header_offset)
これは印刷されます。
The first 24 lines are the gutenberg header...
同様に、最後の行のテキストは単なる著作権表示です。
それも削除する。
# Skip footer
footer_offset: int = len(lines_of_text)
for i in range(0, len(lines_of_text)):
if "*** END OF THE PROJECT GUTENBERG EBOOK" in lines_of_text[i]:
footer_offset = i
break
lines_of_text = lines_of_text[0:footer_offset]
print("The last %d lines are the gutenberg footer .." % (len(lines_of_text) - footer_offset))
これで、本文を処理することができました。
このコードは試行錯誤の末に完成したものです。
章のタイトルをいつ入れるか、新しい段落をいつ始めるか、目次はどうするかなど、本によって異なります。
この機会にborbで少し遊んでみて、別の本で入力を自分で解析してみてください。
from borb.pdf.canvas.layout.text.paragraph import Paragraph
from borb.pdf.canvas.layout.text.heading import Heading
from borb.pdf.canvas.color.color import HexColor, X11Color
from decimal import Decimal
# Main processing loop
i: int = 0
while i < len(lines_of_text):
# Process lines
paragraph_text: str = ""
while i < len(lines_of_text) and not len(lines_of_text[i]) == 0:
paragraph_text += lines_of_text[i]
paragraph_text += " "
i += 1
# Empty line
if len(paragraph_text) == 0:
i += 1
continue
# Space
if paragraph_text.isspace():
i += 1
continue
# Contains the word 'CHAPTER' multiple times (likely to be table of contents)
if sum([1 for x in paragraph_text.split(' ') if 'CHAPTER' in x]) > 2:
i += 1
continue
# Debug
print("Processing line %d / %d" % (i, len(lines_of_text)))
# Outline
if paragraph_text.startswith("CHAPTER"):
print("Adding Header of %d bytes .." % len(paragraph_text))
try:
page = Page()
pdf.append_page(page)
layout = SingleColumnLayout(page)
layout.add(Heading(paragraph_text, font_color=HexColor("13505B"), font_size=Decimal(20)))
except:
pass
continue
# Default
try:
layout.add(Paragraph(paragraph_text))
except:
pass
# Default behaviour
i += 1
あとは最終的なPDFドキュメントを保存するだけです。
with open("output.pdf", "wb") as pdf_file_handle:
PDF.dumps(pdf_file_handle, pdf)
結論
このガイドでは、borbを使って大きなテキストを処理し、そこから自動的にPDFを作成する方法を学びました。
生のテキストファイルから本を作るのは標準的なプロセスではないので、いろいろ試して、ループやテキストの扱い方を弄って、うまくいくようにしなければなりません。