PDF: Printable ToC

PDF Output: Add an option to generate a printable Table of Contents
(that lists page numbers). Useful if you intend to print out the PDF.
This commit is contained in:
Kovid Goyal 2013-05-30 13:02:08 +05:30
parent 5210324ff2
commit ec81b1a07e
7 changed files with 175 additions and 38 deletions

View File

@ -808,3 +808,26 @@ the page will be used.
bottom margins to large enough values, under the Page Setup section of the bottom margins to large enough values, under the Page Setup section of the
conversion dialog. conversion dialog.
You can also insert a printable Table of Contents at the end of the PDF that
lists the page numbers for every section. This is very useful if you intend to
print out the PDF to paper. If you wish to use the PDF on an electronic device,
then the PDF Outline provides this functionality and is generated by default.
You can customize the look of the the generated Table of contents by using the
Extra CSS conversion setting under the Look & Feel part of the conversion
dialog. The default css used is listed below, simply copy it and make whatever
changes you like.
.. code-block:: css
.calibre-pdf-toc table { width: 100%% }
.calibre-pdf-toc table tr td:last-of-type { text-align: right }
.calibre-pdf-toc .level-0 {
font-size: larger;
}
.calibre-pdf-toc .level-1 td:first-of-type { padding-left: 1.4em }
.calibre-pdf-toc .level-2 td:first-of-type { padding-left: 2.8em }

View File

@ -29,7 +29,7 @@ class PDFMetadata(object): # {{{
self.author = _(u'Unknown') self.author = _(u'Unknown')
self.tags = u'' self.tags = u''
if oeb_metadata != None: if oeb_metadata is not None:
if len(oeb_metadata.title) >= 1: if len(oeb_metadata.title) >= 1:
self.title = oeb_metadata.title[0].value self.title = oeb_metadata.title[0].value
if len(oeb_metadata.creator) >= 1: if len(oeb_metadata.creator) >= 1:
@ -109,6 +109,9 @@ class PDFOutput(OutputFormatPlugin):
OptionRecommendation(name='pdf_header_template', recommended_value=None, OptionRecommendation(name='pdf_header_template', recommended_value=None,
help=_('An HTML template used to generate %s on every page.' help=_('An HTML template used to generate %s on every page.'
' The strings _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_ will be replaced by their current values.')%_('headers')), ' The strings _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_ will be replaced by their current values.')%_('headers')),
OptionRecommendation(name='pdf_add_toc', recommended_value=False,
help=_('Add a Table of Contents at the end of the PDF that lists page numbers. '
'Useful if you want to print out the PDF. If this PDF is intended for electronic use, use the PDF Outline instead.')),
]) ])
def convert(self, oeb_book, output_path, input_plugin, opts, log): def convert(self, oeb_book, output_path, input_plugin, opts, log):
@ -122,7 +125,6 @@ class PDFOutput(OutputFormatPlugin):
self.metadata = oeb_book.metadata self.metadata = oeb_book.metadata
self.cover_data = None self.cover_data = None
if input_plugin.is_image_collection: if input_plugin.is_image_collection:
log.debug('Converting input as an image collection...') log.debug('Converting input as an image collection...')
self.convert_images(input_plugin.get_images()) self.convert_images(input_plugin.get_images())
@ -156,7 +158,8 @@ class PDFOutput(OutputFormatPlugin):
# fonts to Qt # fonts to Qt
family_map = {} family_map = {}
for item in list(self.oeb.manifest): for item in list(self.oeb.manifest):
if not hasattr(item.data, 'cssRules'): continue if not hasattr(item.data, 'cssRules'):
continue
remove = set() remove = set()
for i, rule in enumerate(item.data.cssRules): for i, rule in enumerate(item.data.cssRules):
if rule.type == rule.FONT_FACE_RULE: if rule.type == rule.FONT_FACE_RULE:
@ -195,11 +198,14 @@ class PDFOutput(OutputFormatPlugin):
# family name of the embedded font (they may be different in general). # family name of the embedded font (they may be different in general).
font_warnings = set() font_warnings = set()
for item in self.oeb.manifest: for item in self.oeb.manifest:
if not hasattr(item.data, 'cssRules'): continue if not hasattr(item.data, 'cssRules'):
continue
for i, rule in enumerate(item.data.cssRules): for i, rule in enumerate(item.data.cssRules):
if rule.type != rule.STYLE_RULE: continue if rule.type != rule.STYLE_RULE:
continue
ff = rule.style.getProperty('font-family') ff = rule.style.getProperty('font-family')
if ff is None: continue if ff is None:
continue
val = ff.propertyValue val = ff.propertyValue
for i in xrange(val.length): for i in xrange(val.length):
try: try:
@ -279,3 +285,5 @@ class PDFOutput(OutputFormatPlugin):
if close: if close:
out_stream.close() out_stream.close()

View File

@ -21,6 +21,7 @@ from calibre.ebooks.oeb.display.webview import load_html
from calibre.ebooks.pdf.render.common import (inch, cm, mm, pica, cicero, from calibre.ebooks.pdf.render.common import (inch, cm, mm, pica, cicero,
didot, PAPER_SIZES) didot, PAPER_SIZES)
from calibre.ebooks.pdf.render.engine import PdfDevice from calibre.ebooks.pdf.render.engine import PdfDevice
from calibre.ptempfile import PersistentTemporaryFile
def get_page_size(opts, for_comic=False): # {{{ def get_page_size(opts, for_comic=False): # {{{
use_profile = not (opts.override_profile_size or use_profile = not (opts.override_profile_size or
@ -36,7 +37,7 @@ def get_page_size(opts, for_comic=False): # {{{
page_size = (factor * w, factor * h) page_size = (factor * w, factor * h)
else: else:
page_size = None page_size = None
if opts.custom_size != None: if opts.custom_size is not None:
width, sep, height = opts.custom_size.partition('x') width, sep, height = opts.custom_size.partition('x')
if height: if height:
try: try:
@ -237,11 +238,23 @@ class PDFWriter(QObject):
if self.doc.errors_occurred: if self.doc.errors_occurred:
raise Exception('PDF Output failed, see log for details') raise Exception('PDF Output failed, see log for details')
def render_inline_toc(self):
self.rendered_inline_toc = True
from calibre.ebooks.pdf.render.toc import toc_as_html
raw = toc_as_html(self.toc, self.doc, self.opts)
pt = PersistentTemporaryFile('_pdf_itoc.htm')
pt.write(raw)
pt.close()
self.render_queue.append(pt.name)
self.render_next()
def render_book(self): def render_book(self):
if self.doc.errors_occurred: if self.doc.errors_occurred:
return self.loop.exit(1) return self.loop.exit(1)
try: try:
if not self.render_queue: if not self.render_queue:
if self.toc is not None and len(self.toc) > 0 and not hasattr(self, 'rendered_inline_toc'):
return self.render_inline_toc()
self.loop.exit() self.loop.exit()
else: else:
self.render_next() self.render_next()
@ -383,3 +396,4 @@ class PDFWriter(QObject):
amap['anchors']) amap['anchors'])

View File

@ -187,6 +187,12 @@ class PageTree(Dictionary):
def get_ref(self, num): def get_ref(self, num):
return self['Kids'][num-1] return self['Kids'][num-1]
def get_num(self, pageref):
try:
return self['Kids'].index(pageref) + 1
except ValueError:
return -1
class HashingStream(object): class HashingStream(object):
def __init__(self, f): def __init__(self, f):
@ -237,14 +243,14 @@ class PDFStream(object):
PATH_OPS = { PATH_OPS = {
# stroke fill fill-rule # stroke fill fill-rule
( False, False, 'winding') : 'n', (False, False, 'winding') : 'n',
( False, False, 'evenodd') : 'n', (False, False, 'evenodd') : 'n',
( False, True, 'winding') : 'f', (False, True, 'winding') : 'f',
( False, True, 'evenodd') : 'f*', (False, True, 'evenodd') : 'f*',
( True, False, 'winding') : 'S', (True, False, 'winding') : 'S',
( True, False, 'evenodd') : 'S', (True, False, 'evenodd') : 'S',
( True, True, 'winding') : 'B', (True, True, 'winding') : 'B',
( True, True, 'evenodd') : 'B*', (True, True, 'evenodd') : 'B*',
} }
def __init__(self, stream, page_size, compress=False, mark_links=False, def __init__(self, stream, page_size, compress=False, mark_links=False,
@ -329,12 +335,14 @@ class PDFStream(object):
(fmtnum(x) if isinstance(x, (int, long, float)) else x) + ' ') (fmtnum(x) if isinstance(x, (int, long, float)) else x) + ' ')
def draw_path(self, path, stroke=True, fill=False, fill_rule='winding'): def draw_path(self, path, stroke=True, fill=False, fill_rule='winding'):
if not path.ops: return if not path.ops:
return
self.write_path(path) self.write_path(path)
self.current_page.write_line(self.PATH_OPS[(stroke, fill, fill_rule)]) self.current_page.write_line(self.PATH_OPS[(stroke, fill, fill_rule)])
def add_clip(self, path, fill_rule='winding'): def add_clip(self, path, fill_rule='winding'):
if not path.ops: return if not path.ops:
return
self.write_path(path) self.write_path(path)
op = 'W' if fill_rule == 'winding' else 'W*' op = 'W' if fill_rule == 'winding' else 'W*'
self.current_page.write_line(op + ' ' + 'n') self.current_page.write_line(op + ' ' + 'n')
@ -503,3 +511,4 @@ class PDFStream(object):
self.write_line('%d'%startxref) self.write_line('%d'%startxref)
self.stream.write('%%EOF') self.stream.write('%%EOF')

View File

@ -0,0 +1,76 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import os
from lxml.html import tostring
from lxml.html.builder import (HTML, HEAD, BODY, TABLE, TR, TD, H1, STYLE)
def convert_node(toc, table, level, pdf):
tr = TR(
TD(toc.text or _('Unknown')), TD(),
)
tr.set('class', 'level-%d' % level)
anchors = pdf.links.anchors
path = toc.abspath or None
frag = toc.fragment or None
if path is None:
return
path = os.path.normcase(os.path.abspath(path))
if path not in anchors:
return None
a = anchors[path]
dest = a.get(frag, a[None])
num = pdf.page_tree.obj.get_num(dest[0])
tr[1].text = type('')(num)
table.append(tr)
def process_children(toc, table, level, pdf):
for child in toc:
convert_node(child, table, level, pdf)
process_children(child, table, level+1, pdf)
def toc_as_html(toc, pdf, opts):
pdf = pdf.engine.pdf
indents = []
for i in xrange(1, 7):
indents.extend((i, 1.4*i))
html = HTML(
HEAD(
STYLE(
'''
.calibre-pdf-toc table { width: 100%% }
.calibre-pdf-toc table tr td:last-of-type { text-align: right }
.calibre-pdf-toc .level-0 {
font-size: larger;
}
.calibre-pdf-toc .level-%d td:first-of-type { padding-left: %.1gem }
.calibre-pdf-toc .level-%d td:first-of-type { padding-left: %.1gem }
.calibre-pdf-toc .level-%d td:first-of-type { padding-left: %.1gem }
.calibre-pdf-toc .level-%d td:first-of-type { padding-left: %.1gem }
.calibre-pdf-toc .level-%d td:first-of-type { padding-left: %.1gem }
.calibre-pdf-toc .level-%d td:first-of-type { padding-left: %.1gem }
''' % tuple(indents) + (opts.extra_css or '')
)
),
BODY(
H1(_('Table of Contents')),
TABLE(),
)
)
body = html[1]
body.set('class', 'calibre-pdf-toc')
process_children(toc, body[1], 0, pdf)
return tostring(html, pretty_print=True, include_meta_content_type=True, encoding='utf-8')

View File

@ -23,7 +23,7 @@ class PluginWidget(Widget, Ui_Form):
'preserve_cover_aspect_ratio', 'pdf_serif_family', 'unit', 'preserve_cover_aspect_ratio', 'pdf_serif_family', 'unit',
'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font', 'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font',
'pdf_default_font_size', 'pdf_mono_font_size', 'pdf_page_numbers', 'pdf_default_font_size', 'pdf_mono_font_size', 'pdf_page_numbers',
'pdf_footer_template', 'pdf_header_template', 'pdf_footer_template', 'pdf_header_template', 'pdf_add_toc',
]) ])
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id

View File

@ -77,21 +77,21 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="4" column="0" colspan="2"> <item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="opt_preserve_cover_aspect_ratio"> <widget class="QCheckBox" name="opt_preserve_cover_aspect_ratio">
<property name="text"> <property name="text">
<string>Preserve &amp;aspect ratio of cover</string> <string>Preserve &amp;aspect ratio of cover</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0" colspan="2"> <item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="opt_pdf_page_numbers"> <widget class="QCheckBox" name="opt_pdf_page_numbers">
<property name="text"> <property name="text">
<string>Add page &amp;numbers to the bottom of every page</string> <string>Add page &amp;numbers to the bottom of every page</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0"> <item row="10" column="0">
<widget class="QLabel" name="label_4"> <widget class="QLabel" name="label_4">
<property name="text"> <property name="text">
<string>Se&amp;rif family:</string> <string>Se&amp;rif family:</string>
@ -101,10 +101,10 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="10" column="1">
<widget class="QFontComboBox" name="opt_pdf_serif_family"/> <widget class="QFontComboBox" name="opt_pdf_serif_family"/>
</item> </item>
<item row="7" column="0"> <item row="11" column="0">
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="text"> <property name="text">
<string>&amp;Sans family:</string> <string>&amp;Sans family:</string>
@ -114,10 +114,10 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="1"> <item row="11" column="1">
<widget class="QFontComboBox" name="opt_pdf_sans_family"/> <widget class="QFontComboBox" name="opt_pdf_sans_family"/>
</item> </item>
<item row="8" column="0"> <item row="12" column="0">
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
<property name="text"> <property name="text">
<string>&amp;Monospace family:</string> <string>&amp;Monospace family:</string>
@ -127,10 +127,10 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="1"> <item row="12" column="1">
<widget class="QFontComboBox" name="opt_pdf_mono_family"/> <widget class="QFontComboBox" name="opt_pdf_mono_family"/>
</item> </item>
<item row="9" column="0"> <item row="13" column="0">
<widget class="QLabel" name="label_7"> <widget class="QLabel" name="label_7">
<property name="text"> <property name="text">
<string>S&amp;tandard font:</string> <string>S&amp;tandard font:</string>
@ -140,10 +140,10 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="9" column="1"> <item row="13" column="1">
<widget class="QComboBox" name="opt_pdf_standard_font"/> <widget class="QComboBox" name="opt_pdf_standard_font"/>
</item> </item>
<item row="10" column="0"> <item row="14" column="0">
<widget class="QLabel" name="label_8"> <widget class="QLabel" name="label_8">
<property name="text"> <property name="text">
<string>Default font si&amp;ze:</string> <string>Default font si&amp;ze:</string>
@ -153,14 +153,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="10" column="1"> <item row="14" column="1">
<widget class="QSpinBox" name="opt_pdf_default_font_size"> <widget class="QSpinBox" name="opt_pdf_default_font_size">
<property name="suffix"> <property name="suffix">
<string> px</string> <string> px</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="11" column="0"> <item row="15" column="0">
<widget class="QLabel" name="label_9"> <widget class="QLabel" name="label_9">
<property name="text"> <property name="text">
<string>Monospace &amp;font size:</string> <string>Monospace &amp;font size:</string>
@ -170,14 +170,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="11" column="1"> <item row="15" column="1">
<widget class="QSpinBox" name="opt_pdf_mono_font_size"> <widget class="QSpinBox" name="opt_pdf_mono_font_size">
<property name="suffix"> <property name="suffix">
<string> px</string> <string> px</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="12" column="0" colspan="2"> <item row="16" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">
<property name="title"> <property name="title">
<string>Page headers and footers</string> <string>Page headers and footers</string>
@ -225,6 +225,13 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="opt_pdf_add_toc">
<property name="text">
<string>Add a printable &amp;Table of Contents at the end</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>