mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
bd3274ff4a
commit
c90a748839
@ -80,9 +80,8 @@ class OptionRecommendation(object):
|
|||||||
raise ValueError('OpRec: %s: Recommended value not in choices'%
|
raise ValueError('OpRec: %s: Recommended value not in choices'%
|
||||||
self.option.name)
|
self.option.name)
|
||||||
if not (isinstance(self.recommended_value, (int, float, str, unicode)) or self.recommended_value is None):
|
if not (isinstance(self.recommended_value, (int, float, str, unicode)) or self.recommended_value is None):
|
||||||
raise ValueError('OpRec: %s:'%self.option.name +
|
raise ValueError('OpRec: %s:'%self.option.name + repr(
|
||||||
repr(self.recommended_value) +
|
self.recommended_value) + ' is not a string or a number')
|
||||||
' is not a string or a number')
|
|
||||||
|
|
||||||
|
|
||||||
class DummyReporter(object):
|
class DummyReporter(object):
|
||||||
@ -342,6 +341,13 @@ class OutputFormatPlugin(Plugin):
|
|||||||
return self.oeb.metadata.publication_type and \
|
return self.oeb.metadata.publication_type and \
|
||||||
unicode(self.oeb.metadata.publication_type[0]).startswith('periodical:')
|
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):
|
def specialize_css_for_output(self, log, opts, item, stylizer):
|
||||||
'''
|
'''
|
||||||
Can be used to make changes to the css during the CSS flattening
|
Can be used to make changes to the css during the CSS flattening
|
||||||
|
@ -142,14 +142,25 @@ class PDFOutput(OutputFormatPlugin):
|
|||||||
help=_('The size of the bottom page margin, in pts. Default is 72pt.'
|
help=_('The size of the bottom page margin, in pts. Default is 72pt.'
|
||||||
' Overrides the common bottom page margin setting, unless set to zero.')
|
' 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):
|
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
||||||
from calibre.gui2 import must_use_qt, load_builtin_fonts
|
from calibre.gui2 import must_use_qt, load_builtin_fonts
|
||||||
from calibre.ebooks.oeb.transforms.split import Split
|
from calibre.ebooks.oeb.transforms.split import Split
|
||||||
# Turn off hinting in WebKit (requires a patched build of QtWebKit)
|
# Turn off hinting in WebKit (requires a patched build of QtWebKit)
|
||||||
os.environ['CALIBRE_WEBKIT_NO_HINTING'] = '1'
|
os.environ['CALIBRE_WEBKIT_NO_HINTING'] = '1'
|
||||||
self.filtered_font_warnings = set()
|
self.filtered_font_warnings = set()
|
||||||
|
self.stored_page_margins = getattr(opts, '_stored_page_margins', {})
|
||||||
try:
|
try:
|
||||||
# split on page breaks, as the JS code to convert page breaks to
|
# split on page breaks, as the JS code to convert page breaks to
|
||||||
# column breaks will not work because of QWebSettings.LocalContentCanAccessFileUrls
|
# column breaks will not work because of QWebSettings.LocalContentCanAccessFileUrls
|
||||||
@ -192,7 +203,7 @@ class PDFOutput(OutputFormatPlugin):
|
|||||||
self.cover_data = item.data
|
self.cover_data = item.data
|
||||||
|
|
||||||
def process_fonts(self):
|
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.ebooks.oeb.base import urlnormalize
|
||||||
from calibre.utils.fonts.utils import remove_embed_restriction
|
from calibre.utils.fonts.utils import remove_embed_restriction
|
||||||
|
|
||||||
@ -244,6 +255,13 @@ class PDFOutput(OutputFormatPlugin):
|
|||||||
self.get_cover_data()
|
self.get_cover_data()
|
||||||
|
|
||||||
self.process_fonts()
|
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:
|
with TemporaryDirectory('_pdf_out') as oeb_dir:
|
||||||
from calibre.customize.ui import plugin_for_output_format
|
from calibre.customize.ui import plugin_for_output_format
|
||||||
|
@ -1081,6 +1081,7 @@ OptionRecommendation(name='search_replace',
|
|||||||
self.input_plugin.report_progress = ir
|
self.input_plugin.report_progress = ir
|
||||||
if self.for_regex_wizard:
|
if self.for_regex_wizard:
|
||||||
self.input_plugin.for_viewer = True
|
self.input_plugin.for_viewer = True
|
||||||
|
self.output_plugin.specialize_options(self.log, self.opts, self.input_fmt)
|
||||||
with self.input_plugin:
|
with self.input_plugin:
|
||||||
self.oeb = self.input_plugin(stream, self.opts,
|
self.oeb = self.input_plugin(stream, self.opts,
|
||||||
self.input_fmt, self.log,
|
self.input_fmt, self.log,
|
||||||
|
@ -15,6 +15,7 @@ import cssutils
|
|||||||
from cssutils.css import Property
|
from cssutils.css import Property
|
||||||
|
|
||||||
from calibre import guess_type
|
from calibre import guess_type
|
||||||
|
from calibre.ebooks import unit_convert
|
||||||
from calibre.ebooks.oeb.base import (XHTML, XHTML_NS, CSS_MIME, OEB_STYLES,
|
from calibre.ebooks.oeb.base import (XHTML, XHTML_NS, CSS_MIME, OEB_STYLES,
|
||||||
namespace, barename, XPath)
|
namespace, barename, XPath)
|
||||||
from calibre.ebooks.oeb.stylizer import Stylizer
|
from calibre.ebooks.oeb.stylizer import Stylizer
|
||||||
@ -218,6 +219,19 @@ class CSSFlattener(object):
|
|||||||
if epub3_nav is not None:
|
if epub3_nav is not None:
|
||||||
self.opts.epub3_nav_parsed = epub3_nav.data
|
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):
|
def get_embed_font_info(self, family, failure_critical=True):
|
||||||
efi = []
|
efi = []
|
||||||
body_font_family = None
|
body_font_family = None
|
||||||
|
@ -67,18 +67,7 @@ class PdfEngine(QPaintEngine):
|
|||||||
self.left_margin, self.top_margin = left_margin, top_margin
|
self.left_margin, self.top_margin = left_margin, top_margin
|
||||||
self.right_margin, self.bottom_margin = right_margin, bottom_margin
|
self.right_margin, self.bottom_margin = right_margin, bottom_margin
|
||||||
self.pixel_width, self.pixel_height = width, height
|
self.pixel_width, self.pixel_height = width, height
|
||||||
# Setup a co-ordinate transform that allows us to use co-ords
|
self.pdf_system = self.create_transform()
|
||||||
# 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.graphics = Graphics(self.pixel_width, self.pixel_height)
|
self.graphics = Graphics(self.pixel_width, self.pixel_height)
|
||||||
self.errors_occurred = False
|
self.errors_occurred = False
|
||||||
self.errors, self.debug = errors, debug
|
self.errors, self.debug = errors, debug
|
||||||
@ -95,6 +84,23 @@ class PdfEngine(QPaintEngine):
|
|||||||
if err:
|
if err:
|
||||||
raise RuntimeError('Failed to load qt_hack with err: %s'%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):
|
def apply_graphics_state(self):
|
||||||
self.graphics(self.pdf_system, self.painter())
|
self.graphics(self.pdf_system, self.painter())
|
||||||
|
|
||||||
@ -110,9 +116,12 @@ class PdfEngine(QPaintEngine):
|
|||||||
def do_stroke(self):
|
def do_stroke(self):
|
||||||
return self.graphics.current_state.do_stroke
|
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.content_written_to_current_page = False
|
||||||
|
if custom_margins is None:
|
||||||
self.pdf.transform(self.pdf_system)
|
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.pdf.apply_fill(color=(1, 1, 1)) # QPainter has a default background brush of white
|
||||||
self.graphics.reset()
|
self.graphics.reset()
|
||||||
self.pdf.save_stack()
|
self.pdf.save_stack()
|
||||||
@ -391,8 +400,8 @@ class PdfDevice(QPaintDevice): # {{{
|
|||||||
def end_page(self, *args, **kwargs):
|
def end_page(self, *args, **kwargs):
|
||||||
self.engine.end_page(*args, **kwargs)
|
self.engine.end_page(*args, **kwargs)
|
||||||
|
|
||||||
def init_page(self):
|
def init_page(self, custom_margins=None):
|
||||||
self.engine.init_page()
|
self.engine.init_page(custom_margins=custom_margins)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_page_rect(self):
|
def full_page_rect(self):
|
||||||
@ -414,6 +423,10 @@ class PdfDevice(QPaintDevice): # {{{
|
|||||||
return pt * (self.height()/self.page_height if vertical else
|
return pt * (self.height()/self.page_height if vertical else
|
||||||
self.width()/self.page_width)
|
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):
|
def set_metadata(self, *args, **kwargs):
|
||||||
self.engine.set_metadata(*args, **kwargs)
|
self.engine.set_metadata(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -365,6 +365,19 @@ class PDFWriter(QObject):
|
|||||||
''' % self.hyphenate_lang
|
''' % 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):
|
def do_paged_render(self):
|
||||||
if self.paged_js is None:
|
if self.paged_js is None:
|
||||||
import uuid
|
import uuid
|
||||||
@ -387,6 +400,20 @@ class PDFWriter(QObject):
|
|||||||
if self.opts.pdf_hyphenate:
|
if self.opts.pdf_hyphenate:
|
||||||
self.hyphenate(evaljs)
|
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('''
|
amap = json.loads(evaljs('''
|
||||||
document.body.style.backgroundColor = "white";
|
document.body.style.backgroundColor = "white";
|
||||||
paged_display.set_geometry(1, %d, %d, %d);
|
paged_display.set_geometry(1, %d, %d, %d);
|
||||||
@ -395,7 +422,7 @@ class PDFWriter(QObject):
|
|||||||
ret = book_indexing.all_links_and_anchors();
|
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
|
window.scrollTo(0, 0); // This is needed as getting anchor positions could have caused the viewport to scroll
|
||||||
JSON.stringify(ret);
|
JSON.stringify(ret);
|
||||||
'''%(self.margin_top, 0, self.margin_bottom)))
|
'''%(margin_top, 0, margin_bottom)))
|
||||||
|
|
||||||
if not isinstance(amap, dict):
|
if not isinstance(amap, dict):
|
||||||
amap = {'links':[], 'anchors':{}} # Some javascript error occurred
|
amap = {'links':[], 'anchors':{}} # Some javascript error occurred
|
||||||
@ -429,7 +456,7 @@ class PDFWriter(QObject):
|
|||||||
while True:
|
while True:
|
||||||
set_section(col, sections, 'current_section')
|
set_section(col, sections, 'current_section')
|
||||||
set_section(col, tl_sections, 'current_tl_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 self.header or self.footer:
|
||||||
if evaljs('paged_display.update_header_footer(%d)'%self.current_page_num) is True:
|
if evaljs('paged_display.update_header_footer(%d)'%self.current_page_num) is True:
|
||||||
self.load_header_footer_images()
|
self.load_header_footer_images()
|
||||||
|
@ -5,7 +5,7 @@ __copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__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.pdf_output_ui import Ui_Form
|
||||||
from calibre.gui2.convert import Widget
|
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_default_font_size', 'pdf_mono_font_size', 'pdf_page_numbers',
|
||||||
'pdf_footer_template', 'pdf_header_template', 'pdf_add_toc', 'toc_title',
|
'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_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
|
self.db, self.book_id = db, book_id
|
||||||
try:
|
try:
|
||||||
@ -48,13 +49,24 @@ class PluginWidget(Widget, Ui_Form):
|
|||||||
self.initialize_options(get_option, get_help, db, book_id)
|
self.initialize_options(get_option, get_help, db, book_id)
|
||||||
self.layout().setFieldGrowthPolicy(self.layout().ExpandingFieldsGrow)
|
self.layout().setFieldGrowthPolicy(self.layout().ExpandingFieldsGrow)
|
||||||
self.template_box.layout().setFieldGrowthPolicy(self.layout().AllNonFixedFieldsGrow)
|
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):
|
def setupUi(self, *a):
|
||||||
Ui_Form.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()
|
l = self.page_margins_box.l = QFormLayout()
|
||||||
r = self.page_margins_box.r = QFormLayout()
|
r = self.page_margins_box.r = QFormLayout()
|
||||||
h.addLayout(l), h.addLayout(r)
|
h.addLayout(l), h.addLayout(r)
|
||||||
|
v.addLayout(h)
|
||||||
|
|
||||||
def margin(which):
|
def margin(which):
|
||||||
w = QDoubleSpinBox(self)
|
w = QDoubleSpinBox(self)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user