背景
前回 サイボウズLiveからエクスポートしたCSVファイルをPDF化しました。
さて、いよいよサイボウズLiveサービス終了間近ということでいくつかアップデートしました。
目次機能
先頭に目次をつけるようにしました。また、目次をリンクにすることで記事にスムーズにジャンプできるようになりました。
変更点としては以下のとおりです。
BaseDocTemplate
を継承したクラスの作成
Paragraph
クラスである要素(=flowable)である場合、そのヘッダから目次のエントリを作成します。ちなみに、TOCEntry
を notify
することで自動的に目次のエントリに追加されるそうです。
34 class DocTemplate(BaseDocTemplate):
35 def __init__(self, filename, **kw):
36 self.allowSplitting = 0
37 BaseDocTemplate.__init__(self, filename, **kw)
38 template = PageTemplate('normal', [Frame(2.5*cm, 2.5*cm, 15*cm, 25*cm, id='F1')])
39 self.addPageTemplates(template)
40
41 def afterFlowable(self, flowable):
42 if flowable.__class__.__name__ == 'Paragraph':
43 text = flowable.getPlainText()
44 style = flowable.style.name
45 if style == 'header':
46 self.notify('TOCEntry', (0, text, self.page, flowable._bookmark))
マニュアルに以下の記載があります。
Entries to the table of contents can be done either manually by calling the addEntry method on the
TableOfContents object or automatically by sending a 'TOCEntry' notification in the
afterFlowable method of the DocTemplate you are using.
目次の作成
最初に目次(=TOC: Table Of Contents)の作成コードを追加しただけです。
338 def gen_pdf(generator, output, toc=True):
339 addMapping(DEFAULT_FONT, 1, 1, DEFAULT_FONT)
340
341 story = []
342
343 if toc:
344 title_style = ParagraphStyle(
345 fontName=DEFAULT_FONT,
346 fontSize=15,
347 name='TOC',
348 spaceAfter=10
349 )
350 story.append(Paragraph('目次', title_style))
351
352 toc = TableOfContents()
353 toc.levelStyles = [
354 ParagraphStyle(
355 fontName=DEFAULT_FONT,
356 fontSize=8,
357 name='body',
358 spaceAfter=4,
359 justifyBreaks=1
360 )
361 ]
362
363 story.append(toc)
364 story.append(PageBreak())
365
366 story.extend(generator.convert())
367
368 doc = DocTemplate(output)
369 pdfmetrics.registerFont(TTFont(DEFAULT_FONT, DEFAULT_FONT_FILE))
370 doc.multiBuild(story)
リンクの作成
まず、記事を一意に識別できる名前を作ります。後で考えたら記事のIDで良かったのですが、今回はタイトルの文字列をxxHashしたものを識別名としました。
DocTemplate
クラス内のself.notify
ですが、第二引数のタプル(0, text, self.page, flowable._bookmark)
の4要素目に先程の識別名を代入することでリンクとして動作します。ただし、この識別名を渡す手段が特に用意されていないので_bookmark
のような適当なメンバを作成して代入してます(111行目)。
また、実際のジャンプ先に<a>
タグを設定する必要があるようです。そこで、この識別名をname
属性に持つ<a>
タグをheader
に入れます(108行目)。
101 def convert(self):
102 import xxhash
103 story = []
104 for board in self.boards:
105 title = '{} [{}]'.format(board.title, board.id)
106 creator = '{} ({})'.format(board.creator, board.create_time)
107 digest = xxhash.xxh32_hexdigest(title)
108 header = '{} / {} <a name={} />'.format(title, creator, digest)
109
110 hp = Paragraph(header, self.header_style)
111 hp._bookmark = digest
112 story.append(hp)
113 story.append(Paragraph(board.body, self.body_style))
114 story.extend(CommentGenerator(board.comments).convert())
115 story.append(PageBreak())
116
117 return story
目次の作り方はこちらのコードが参考になります。
見た目の変更
以下の変更を行いました。
- 作成日順にソート(CSVは最終更新日順に並んでいるようです)
- トピックのタイトルを矩形、本文を網掛けで装飾
- コメントの改行が正しく表示されない問題の修正
使い方
シンプル
$ python generate.py live_board.csv board.pdf
作成日を指定(2018年の記事だけをPDF化したい等の用途)
python generate.py -f 2018/01/01 -t 2019/01/01 live_board.csv board.pdf