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 @@ 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

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 @@ 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

View File

@ -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,

View File

@ -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

View File

@ -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
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)

View File

@ -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()

View File

@ -5,7 +5,7 @@ __copyright__ = '2009, John Schember <john@nachtimwald.com>'
__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)