PDF Output: Add an option to use page margins from the input document, specified via @page CSS rules. Allows individual HTML files in the input document to have different page margins in the output PDF. Fixes #1773319 [ePub-to-PDF conversion: display of full-page images](https://bugs.launchpad.net/calibre/+bug/1773319)

This commit is contained in:
Kovid Goyal 2018-05-31 13:04:35 +05:30
parent bd3274ff4a
commit c90a748839
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
7 changed files with 115 additions and 24 deletions

View file

@ -80,9 +80,8 @@ def validate_parameters(self):
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 @@ def is_periodical(self):
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

View file

@ -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 @@ def get_cover_data(self):
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 @@ def convert_text(self, oeb_book):
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

View file

@ -1081,6 +1081,7 @@ def run(self):
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,

View file

@ -15,6 +15,7 @@
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 @@ def __call__(self, oeb, context):
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

View file

@ -67,18 +67,7 @@ def __init__(self, file_object, page_width, page_height, left_margin,
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 @@ def __init__(self, file_object, page_width, page_height, left_margin,
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 @@ def do_fill(self):
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 @@ def metric(self, m):
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 @@ def to_px(self, pt, vertical=True):
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)

View file

@ -365,6 +365,19 @@ def hyphenate(self, evaljs):
''' % 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 @@ def do_paged_render(self):
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 @@ def do_paged_render(self):
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 @@ def set_section(col, sections, attr):
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()

View file

@ -5,7 +5,7 @@
__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 @@ def __init__(self, parent, get_option, get_help, db=None, book_id=None):
'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 @@ def __init__(self, parent, get_option, get_help, db=None, book_id=None):
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)