diff --git a/src/calibre/customize/conversion.py b/src/calibre/customize/conversion.py index ae854f655e..2fccd85a21 100644 --- a/src/calibre/customize/conversion.py +++ b/src/calibre/customize/conversion.py @@ -80,9 +80,8 @@ class OptionRecommendation(object): raise ValueError('OpRec: %s: Recommended value not in choices'% self.option.name) if not (isinstance(self.recommended_value, (int, float, str, unicode)) or self.recommended_value is None): - raise ValueError('OpRec: %s:'%self.option.name + - repr(self.recommended_value) + - ' is not a string or a number') + raise ValueError('OpRec: %s:'%self.option.name + repr( + self.recommended_value) + ' is not a string or a number') class DummyReporter(object): @@ -342,6 +341,13 @@ class OutputFormatPlugin(Plugin): return self.oeb.metadata.publication_type and \ unicode(self.oeb.metadata.publication_type[0]).startswith('periodical:') + def specialize_options(self, log, opts, input_fmt): + ''' + Can be used to change the values of conversion options, as used by the + conversion pipeline. + ''' + pass + def specialize_css_for_output(self, log, opts, item, stylizer): ''' Can be used to make changes to the css during the CSS flattening diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index 8e5a63c41c..e06c9d4cba 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -142,14 +142,25 @@ class PDFOutput(OutputFormatPlugin): help=_('The size of the bottom page margin, in pts. Default is 72pt.' ' Overrides the common bottom page margin setting, unless set to zero.') ), + OptionRecommendation(name='pdf_use_document_margins', recommended_value=False, + help=_('Use the page margins specified in the input document via @page CSS rules.' + ' This will cause the margins specified in the conversion settings to be ignored.' + ' If the document does not specify page margins, the conversion settings will be used as a fallback.') + ), ]) + def specialize_options(self, log, opts, input_fmt): + if opts.pdf_use_document_margins: + # Prevent the conversion pipeline from overwriting document margins + opts.margin_left = opts.margin_right = opts.margin_top = opts.margin_bottom = -1 + def convert(self, oeb_book, output_path, input_plugin, opts, log): from calibre.gui2 import must_use_qt, load_builtin_fonts from calibre.ebooks.oeb.transforms.split import Split # Turn off hinting in WebKit (requires a patched build of QtWebKit) os.environ['CALIBRE_WEBKIT_NO_HINTING'] = '1' self.filtered_font_warnings = set() + self.stored_page_margins = getattr(opts, '_stored_page_margins', {}) try: # split on page breaks, as the JS code to convert page breaks to # column breaks will not work because of QWebSettings.LocalContentCanAccessFileUrls @@ -192,7 +203,7 @@ class PDFOutput(OutputFormatPlugin): self.cover_data = item.data def process_fonts(self): - ''' Make sure all fonts are embeddable. Also remove some fonts that causes problems. ''' + ''' Make sure all fonts are embeddable. Also remove some fonts that cause problems. ''' from calibre.ebooks.oeb.base import urlnormalize from calibre.utils.fonts.utils import remove_embed_restriction @@ -244,6 +255,13 @@ class PDFOutput(OutputFormatPlugin): self.get_cover_data() self.process_fonts() + if self.opts.pdf_use_document_margins and self.stored_page_margins: + import json + for href, margins in self.stored_page_margins.iteritems(): + item = oeb_book.manifest.hrefs.get(href) + root = item.data + if hasattr(root, 'xpath') and margins: + root.set('data-calibre-pdf-output-page-margins', json.dumps(margins)) with TemporaryDirectory('_pdf_out') as oeb_dir: from calibre.customize.ui import plugin_for_output_format diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index c8e3ab6bf6..a361b6be2f 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -1081,6 +1081,7 @@ OptionRecommendation(name='search_replace', self.input_plugin.report_progress = ir if self.for_regex_wizard: self.input_plugin.for_viewer = True + self.output_plugin.specialize_options(self.log, self.opts, self.input_fmt) with self.input_plugin: self.oeb = self.input_plugin(stream, self.opts, self.input_fmt, self.log, diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py index 7f43fc2447..667a1dabdd 100644 --- a/src/calibre/ebooks/oeb/transforms/flatcss.py +++ b/src/calibre/ebooks/oeb/transforms/flatcss.py @@ -15,6 +15,7 @@ import cssutils from cssutils.css import Property from calibre import guess_type +from calibre.ebooks import unit_convert from calibre.ebooks.oeb.base import (XHTML, XHTML_NS, CSS_MIME, OEB_STYLES, namespace, barename, XPath) from calibre.ebooks.oeb.stylizer import Stylizer @@ -218,6 +219,19 @@ class CSSFlattener(object): if epub3_nav is not None: self.opts.epub3_nav_parsed = epub3_nav.data + self.store_page_margins() + + def store_page_margins(self): + self.opts._stored_page_margins = {} + for item, stylizer in self.stylizers.iteritems(): + margins = self.opts._stored_page_margins[item.href] = {} + for prop, val in stylizer.page_rule.items(): + p, w = prop.partition('-')[::2] + if p == 'margin': + margins[w] = unit_convert( + val, stylizer.profile.width_pts, stylizer.body_font_size, + stylizer.profile.dpi, body_font_size=stylizer.body_font_size) + def get_embed_font_info(self, family, failure_critical=True): efi = [] body_font_family = None diff --git a/src/calibre/ebooks/pdf/render/engine.py b/src/calibre/ebooks/pdf/render/engine.py index bde07acfd5..bc6a379ce6 100644 --- a/src/calibre/ebooks/pdf/render/engine.py +++ b/src/calibre/ebooks/pdf/render/engine.py @@ -67,18 +67,7 @@ class PdfEngine(QPaintEngine): self.left_margin, self.top_margin = left_margin, top_margin self.right_margin, self.bottom_margin = right_margin, bottom_margin self.pixel_width, self.pixel_height = width, height - # Setup a co-ordinate transform that allows us to use co-ords - # from Qt's pixel based co-ordinate system with its origin at the top - # left corner. PDF's co-ordinate system is based on pts and has its - # origin in the bottom left corner. We also have to implement the page - # margins. Therefore, we need to translate, scale and reflect about the - # x-axis. - dy = self.page_height - self.top_margin - dx = self.left_margin - sx = (self.page_width - self.left_margin - self.right_margin) / self.pixel_width - sy = (self.page_height - self.top_margin - self.bottom_margin) / self.pixel_height - - self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy) + self.pdf_system = self.create_transform() self.graphics = Graphics(self.pixel_width, self.pixel_height) self.errors_occurred = False self.errors, self.debug = errors, debug @@ -95,6 +84,23 @@ class PdfEngine(QPaintEngine): if err: raise RuntimeError('Failed to load qt_hack with err: %s'%err) + def create_transform(self, left_margin=None, top_margin=None, right_margin=None, bottom_margin=None): + # Setup a co-ordinate transform that allows us to use co-ords + # from Qt's pixel based co-ordinate system with its origin at the top + # left corner. PDF's co-ordinate system is based on pts and has its + # origin in the bottom left corner. We also have to implement the page + # margins. Therefore, we need to translate, scale and reflect about the + # x-axis. + left_margin = self.left_margin if left_margin is None else left_margin + top_margin = self.top_margin if top_margin is None else top_margin + right_margin = self.right_margin if right_margin is None else right_margin + bottom_margin = self.bottom_margin if bottom_margin is None else bottom_margin + dy = self.page_height - top_margin + dx = left_margin + sx = (self.page_width - left_margin - right_margin) / self.pixel_width + sy = (self.page_height - top_margin - bottom_margin) / self.pixel_height + return QTransform(sx, 0, 0, -sy, dx, dy) + def apply_graphics_state(self): self.graphics(self.pdf_system, self.painter()) @@ -110,9 +116,12 @@ class PdfEngine(QPaintEngine): def do_stroke(self): return self.graphics.current_state.do_stroke - def init_page(self): + def init_page(self, custom_margins=None): self.content_written_to_current_page = False - self.pdf.transform(self.pdf_system) + if custom_margins is None: + self.pdf.transform(self.pdf_system) + else: + self.pdf.transform(self.create_transform(*custom_margins)) self.pdf.apply_fill(color=(1, 1, 1)) # QPainter has a default background brush of white self.graphics.reset() self.pdf.save_stack() @@ -391,8 +400,8 @@ class PdfDevice(QPaintDevice): # {{{ def end_page(self, *args, **kwargs): self.engine.end_page(*args, **kwargs) - def init_page(self): - self.engine.init_page() + def init_page(self, custom_margins=None): + self.engine.init_page(custom_margins=custom_margins) @property def full_page_rect(self): @@ -414,6 +423,10 @@ class PdfDevice(QPaintDevice): # {{{ return pt * (self.height()/self.page_height if vertical else self.width()/self.page_width) + def to_pt(self, px, vertical=True): + return px * (self.page_height / self.height() if vertical else + self.page_width / self.width()) + def set_metadata(self, *args, **kwargs): self.engine.set_metadata(*args, **kwargs) diff --git a/src/calibre/ebooks/pdf/render/from_html.py b/src/calibre/ebooks/pdf/render/from_html.py index b987700744..567252ca82 100644 --- a/src/calibre/ebooks/pdf/render/from_html.py +++ b/src/calibre/ebooks/pdf/render/from_html.py @@ -365,6 +365,19 @@ class PDFWriter(QObject): ''' % self.hyphenate_lang ) + def convert_page_margins(self, doc_margins): + ans = [0, 0, 0, 0] + + def convert(name, idx, vertical=True): + m = doc_margins.get(name) + if m is None: + ans[idx] = getattr(self.doc.engine, '{}_margin'.format(name)) + else: + ans[idx] = m + + convert('left', 0, False), convert('top', 1), convert('right', 2, False), convert('bottom', 3) + return ans + def do_paged_render(self): if self.paged_js is None: import uuid @@ -387,6 +400,20 @@ class PDFWriter(QObject): if self.opts.pdf_hyphenate: self.hyphenate(evaljs) + margin_top, margin_bottom = self.margin_top, self.margin_bottom + page_margins = None + if self.opts.pdf_use_document_margins: + doc_margins = evaljs('document.documentElement.getAttribute("data-calibre-pdf-output-page-margins")') + try: + doc_margins = json.loads(doc_margins) + except Exception: + doc_margins = None + if doc_margins and isinstance(doc_margins, dict): + doc_margins = {k:float(v) for k, v in doc_margins.iteritems() if isinstance(v, (float, int)) and k in {'right', 'top', 'left', 'bottom'}} + if doc_margins: + margin_top = margin_bottom = 0 + page_margins = self.convert_page_margins(doc_margins) + amap = json.loads(evaljs(''' document.body.style.backgroundColor = "white"; paged_display.set_geometry(1, %d, %d, %d); @@ -395,7 +422,7 @@ class PDFWriter(QObject): ret = book_indexing.all_links_and_anchors(); window.scrollTo(0, 0); // This is needed as getting anchor positions could have caused the viewport to scroll JSON.stringify(ret); - '''%(self.margin_top, 0, self.margin_bottom))) + '''%(margin_top, 0, margin_bottom))) if not isinstance(amap, dict): amap = {'links':[], 'anchors':{}} # Some javascript error occurred @@ -429,7 +456,7 @@ class PDFWriter(QObject): while True: set_section(col, sections, 'current_section') set_section(col, tl_sections, 'current_tl_section') - self.doc.init_page() + self.doc.init_page(page_margins) if self.header or self.footer: if evaljs('paged_display.update_header_footer(%d)'%self.current_page_num) is True: self.load_header_footer_images() diff --git a/src/calibre/gui2/convert/pdf_output.py b/src/calibre/gui2/convert/pdf_output.py index 85684b3116..c1a707b840 100644 --- a/src/calibre/gui2/convert/pdf_output.py +++ b/src/calibre/gui2/convert/pdf_output.py @@ -5,7 +5,7 @@ __copyright__ = '2009, John Schember ' __docformat__ = 'restructuredtext en' -from PyQt5.Qt import QHBoxLayout, QFormLayout, QDoubleSpinBox +from PyQt5.Qt import QHBoxLayout, QFormLayout, QDoubleSpinBox, QCheckBox, QVBoxLayout from calibre.gui2.convert.pdf_output_ui import Ui_Form from calibre.gui2.convert import Widget @@ -30,6 +30,7 @@ class PluginWidget(Widget, Ui_Form): 'pdf_default_font_size', 'pdf_mono_font_size', 'pdf_page_numbers', 'pdf_footer_template', 'pdf_header_template', 'pdf_add_toc', 'toc_title', 'pdf_page_margin_left', 'pdf_page_margin_top', 'pdf_page_margin_right', 'pdf_page_margin_bottom', + 'pdf_use_document_margins', ]) self.db, self.book_id = db, book_id try: @@ -48,13 +49,24 @@ class PluginWidget(Widget, Ui_Form): self.initialize_options(get_option, get_help, db, book_id) self.layout().setFieldGrowthPolicy(self.layout().ExpandingFieldsGrow) self.template_box.layout().setFieldGrowthPolicy(self.layout().AllNonFixedFieldsGrow) + self.toggle_margins() + + def toggle_margins(self): + enabled = not self.opt_pdf_use_document_margins.isChecked() + for which in 'left top right bottom'.split(): + getattr(self, 'opt_pdf_page_margin_' + which).setEnabled(enabled) def setupUi(self, *a): Ui_Form.setupUi(self, *a) - h = self.page_margins_box.h = QHBoxLayout(self.page_margins_box) + v = self.page_margins_box.v = QVBoxLayout(self.page_margins_box) + self.opt_pdf_use_document_margins = c = QCheckBox(_('Use page margins from the &document being converted')) + v.addWidget(c) + c.stateChanged.connect(self.toggle_margins) + h = self.page_margins_box.h = QHBoxLayout() l = self.page_margins_box.l = QFormLayout() r = self.page_margins_box.r = QFormLayout() h.addLayout(l), h.addLayout(r) + v.addLayout(h) def margin(which): w = QDoubleSpinBox(self)