diff --git a/manual/conversion.rst b/manual/conversion.rst index fe5594a705..0747ffaba9 100644 --- a/manual/conversion.rst +++ b/manual/conversion.rst @@ -808,3 +808,26 @@ the page will be used. bottom margins to large enough values, under the Page Setup section of the 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 } + diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index dc943eec30..4db9c07c48 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -21,7 +21,7 @@ UNITS = ['millimeter', 'centimeter', 'point', 'inch' , 'pica' , 'didot', PAPER_SIZES = [u'a0', u'a1', u'a2', u'a3', u'a4', u'a5', u'a6', u'b0', u'b1', u'b2', u'b3', u'b4', u'b5', u'b6', u'legal', u'letter'] -class PDFMetadata(object): # {{{ +class PDFMetadata(object): # {{{ def __init__(self, oeb_metadata=None): from calibre import force_unicode from calibre.ebooks.metadata import authors_to_string @@ -29,7 +29,7 @@ class PDFMetadata(object): # {{{ self.author = _(u'Unknown') self.tags = u'' - if oeb_metadata != None: + if oeb_metadata is not None: if len(oeb_metadata.title) >= 1: self.title = oeb_metadata.title[0].value if len(oeb_metadata.creator) >= 1: @@ -109,6 +109,9 @@ class PDFOutput(OutputFormatPlugin): OptionRecommendation(name='pdf_header_template', recommended_value=None, 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')), + 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): @@ -122,7 +125,6 @@ class PDFOutput(OutputFormatPlugin): self.metadata = oeb_book.metadata self.cover_data = None - if input_plugin.is_image_collection: log.debug('Converting input as an image collection...') self.convert_images(input_plugin.get_images()) @@ -156,7 +158,8 @@ class PDFOutput(OutputFormatPlugin): # fonts to Qt family_map = {} for item in list(self.oeb.manifest): - if not hasattr(item.data, 'cssRules'): continue + if not hasattr(item.data, 'cssRules'): + continue remove = set() for i, rule in enumerate(item.data.cssRules): 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). font_warnings = set() 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): - if rule.type != rule.STYLE_RULE: continue + if rule.type != rule.STYLE_RULE: + continue ff = rule.style.getProperty('font-family') - if ff is None: continue + if ff is None: + continue val = ff.propertyValue for i in xrange(val.length): try: @@ -279,3 +285,5 @@ class PDFOutput(OutputFormatPlugin): if close: out_stream.close() + + diff --git a/src/calibre/ebooks/pdf/render/from_html.py b/src/calibre/ebooks/pdf/render/from_html.py index ae1083bb56..5b9f58e326 100644 --- a/src/calibre/ebooks/pdf/render/from_html.py +++ b/src/calibre/ebooks/pdf/render/from_html.py @@ -21,8 +21,9 @@ from calibre.ebooks.oeb.display.webview import load_html from calibre.ebooks.pdf.render.common import (inch, cm, mm, pica, cicero, didot, PAPER_SIZES) 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 opts.output_profile.short_name == 'default' or opts.output_profile.width > 9999) @@ -36,7 +37,7 @@ def get_page_size(opts, for_comic=False): # {{{ page_size = (factor * w, factor * h) else: page_size = None - if opts.custom_size != None: + if opts.custom_size is not None: width, sep, height = opts.custom_size.partition('x') if height: try: @@ -57,7 +58,7 @@ def get_page_size(opts, for_comic=False): # {{{ return page_size # }}} -class Page(QWebPage): # {{{ +class Page(QWebPage): # {{{ def __init__(self, opts, log): self.log = log @@ -107,7 +108,7 @@ def draw_image_page(page_rect, painter, p, preserve_aspect_ratio=True): nw, nh = page_rect.width(), page_rect.height() if aspect_ratio > 1: nh = int(page_rect.width()/aspect_ratio) - else: # Width is smaller than height + else: # Width is smaller than height nw = page_rect.height()*aspect_ratio __, nnw, nnh = fit_image(nw, nh, page_rect.width(), page_rect.height()) @@ -237,11 +238,23 @@ class PDFWriter(QObject): if self.doc.errors_occurred: 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): if self.doc.errors_occurred: return self.loop.exit(1) try: 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() else: self.render_next() @@ -344,7 +357,7 @@ class PDFWriter(QObject): amap = self.bridge_value if not isinstance(amap, dict): - amap = {'links':[], 'anchors':{}} # Some javascript error occurred + amap = {'links':[], 'anchors':{}} # Some javascript error occurred sections = self.get_sections(amap['anchors']) col = 0 @@ -383,3 +396,4 @@ class PDFWriter(QObject): amap['anchors']) + diff --git a/src/calibre/ebooks/pdf/render/serialize.py b/src/calibre/ebooks/pdf/render/serialize.py index fd10e0756f..4630afca86 100644 --- a/src/calibre/ebooks/pdf/render/serialize.py +++ b/src/calibre/ebooks/pdf/render/serialize.py @@ -187,6 +187,12 @@ class PageTree(Dictionary): def get_ref(self, num): return self['Kids'][num-1] + def get_num(self, pageref): + try: + return self['Kids'].index(pageref) + 1 + except ValueError: + return -1 + class HashingStream(object): def __init__(self, f): @@ -237,14 +243,14 @@ class PDFStream(object): PATH_OPS = { # stroke fill fill-rule - ( False, False, 'winding') : 'n', - ( False, False, 'evenodd') : 'n', - ( False, True, 'winding') : 'f', - ( False, True, 'evenodd') : 'f*', - ( True, False, 'winding') : 'S', - ( True, False, 'evenodd') : 'S', - ( True, True, 'winding') : 'B', - ( True, True, 'evenodd') : 'B*', + (False, False, 'winding') : 'n', + (False, False, 'evenodd') : 'n', + (False, True, 'winding') : 'f', + (False, True, 'evenodd') : 'f*', + (True, False, 'winding') : 'S', + (True, False, 'evenodd') : 'S', + (True, True, 'winding') : 'B', + (True, True, 'evenodd') : 'B*', } 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) + ' ') 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.current_page.write_line(self.PATH_OPS[(stroke, fill, fill_rule)]) def add_clip(self, path, fill_rule='winding'): - if not path.ops: return + if not path.ops: + return self.write_path(path) op = 'W' if fill_rule == 'winding' else 'W*' self.current_page.write_line(op + ' ' + 'n') @@ -503,3 +511,4 @@ class PDFStream(object): self.write_line('%d'%startxref) self.stream.write('%%EOF') + diff --git a/src/calibre/ebooks/pdf/render/toc.py b/src/calibre/ebooks/pdf/render/toc.py new file mode 100644 index 0000000000..9036aba166 --- /dev/null +++ b/src/calibre/ebooks/pdf/render/toc.py @@ -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 ' + +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') diff --git a/src/calibre/gui2/convert/pdf_output.py b/src/calibre/gui2/convert/pdf_output.py index 889a99a66a..d06f22b672 100644 --- a/src/calibre/gui2/convert/pdf_output.py +++ b/src/calibre/gui2/convert/pdf_output.py @@ -23,7 +23,7 @@ class PluginWidget(Widget, Ui_Form): 'preserve_cover_aspect_ratio', 'pdf_serif_family', 'unit', 'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font', '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 diff --git a/src/calibre/gui2/convert/pdf_output.ui b/src/calibre/gui2/convert/pdf_output.ui index a3cd131ba3..d7ee899ffc 100644 --- a/src/calibre/gui2/convert/pdf_output.ui +++ b/src/calibre/gui2/convert/pdf_output.ui @@ -77,21 +77,21 @@ - + Preserve &aspect ratio of cover - + Add page &numbers to the bottom of every page - + Se&rif family: @@ -101,10 +101,10 @@ - + - + &Sans family: @@ -114,10 +114,10 @@ - + - + &Monospace family: @@ -127,10 +127,10 @@ - + - + S&tandard font: @@ -140,10 +140,10 @@ - + - + Default font si&ze: @@ -153,14 +153,14 @@ - + px - + Monospace &font size: @@ -170,14 +170,14 @@ - + px - + Page headers and footers @@ -225,6 +225,13 @@ + + + + Add a printable &Table of Contents at the end + + +