From b5b0891421508152d9e915a238970e29280784fb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 3 Jul 2019 17:00:18 +0530 Subject: [PATCH] Start work on porting the PDF output plugin to use web engine --- setup/extensions.json | 8 - src/calibre/constants.py | 1 - .../ebooks/conversion/plugins/pdf_output.py | 102 +--- src/calibre/ebooks/docx/writer/container.py | 2 +- src/calibre/ebooks/pdf/image_writer.py | 96 +++ src/calibre/ebooks/pdf/render/engine.py | 433 -------------- src/calibre/ebooks/pdf/render/from_html.py | 565 ------------------ src/calibre/ebooks/pdf/render/qt_hack.cpp | 96 --- src/calibre/ebooks/pdf/render/qt_hack.h | 20 - src/calibre/ebooks/pdf/render/qt_hack.sip | 16 - src/calibre/ebooks/pdf/render/serialize.py | 7 +- src/calibre/ebooks/pdf/render/test.py | 139 ----- 12 files changed, 135 insertions(+), 1350 deletions(-) create mode 100644 src/calibre/ebooks/pdf/image_writer.py delete mode 100644 src/calibre/ebooks/pdf/render/engine.py delete mode 100644 src/calibre/ebooks/pdf/render/from_html.py delete mode 100644 src/calibre/ebooks/pdf/render/qt_hack.cpp delete mode 100644 src/calibre/ebooks/pdf/render/qt_hack.h delete mode 100644 src/calibre/ebooks/pdf/render/qt_hack.sip delete mode 100644 src/calibre/ebooks/pdf/render/test.py diff --git a/setup/extensions.json b/setup/extensions.json index f78304a28a..146963f752 100644 --- a/setup/extensions.json +++ b/setup/extensions.json @@ -144,14 +144,6 @@ "sip_files": "calibre/utils/imageops/imageops.sip", "inc_dirs": "calibre/utils/imageops" }, - { - "name": "qt_hack", - "sources": "calibre/ebooks/pdf/render/qt_hack.cpp", - "headers": "calibre/ebooks/pdf/render/qt_hack.h", - "sip_files": "calibre/ebooks/pdf/render/qt_hack.sip", - "inc_dirs": "calibre/ebooks/pdf/render", - "qt_private": "core gui" - }, { "name": "lzma_binding", "sources": "lzma/*.c", diff --git a/src/calibre/constants.py b/src/calibre/constants.py index e046eb4148..e718f3b6d2 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -176,7 +176,6 @@ class Plugins(collections.Mapping): 'html', 'freetype', 'imageops', - 'qt_hack', 'hunspell', '_patiencediff_c', 'bzzdec', diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index 4bfffbb69b..88c5208d4f 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -11,17 +11,16 @@ Convert OEB ebook format to PDF. import glob, os -from calibre.constants import iswindows from calibre.customize.conversion import (OutputFormatPlugin, OptionRecommendation) from calibre.ptempfile import TemporaryDirectory from polyglot.builtins import iteritems, unicode_type -UNITS = ['millimeter', 'centimeter', 'point', 'inch' , 'pica' , 'didot', - 'cicero', 'devicepixel'] +UNITS = ('millimeter', 'centimeter', 'point', 'inch' , 'pica' , 'didot', + 'cicero', 'devicepixel') -PAPER_SIZES = ['a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'b0', 'b1', - 'b2', 'b3', 'b4', 'b5', 'b6', 'legal', 'letter'] +PAPER_SIZES = ('a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'b0', 'b1', + 'b2', 'b3', 'b4', 'b5', 'b6', 'legal', 'letter') class PDFMetadata(object): # {{{ @@ -53,7 +52,7 @@ class PDFOutput(OutputFormatPlugin): author = 'Kovid Goyal' file_type = 'pdf' commit_name = 'pdf_output' - ui_data = {'paper_sizes': PAPER_SIZES, 'units': UNITS, 'font_types': ['serif', 'sans', 'mono']} + ui_data = {'paper_sizes': PAPER_SIZES, 'units': UNITS, 'font_types': ('serif', 'sans', 'mono')} options = { OptionRecommendation(name='use_profile_size', recommended_value=False, @@ -63,13 +62,13 @@ class PDFOutput(OutputFormatPlugin): OptionRecommendation(name='unit', recommended_value='inch', level=OptionRecommendation.LOW, short_switch='u', choices=UNITS, help=_('The unit of measure for page sizes. Default is inch. Choices ' - 'are %s ' - 'Note: This does not override the unit for margins!') % UNITS), + 'are {} ' + 'Note: This does not override the unit for margins!').format(UNITS)), OptionRecommendation(name='paper_size', recommended_value='letter', level=OptionRecommendation.LOW, choices=PAPER_SIZES, help=_('The size of the paper. This size will be overridden when a ' 'non default output profile is used. Default is letter. Choices ' - 'are %s') % PAPER_SIZES), + 'are {}').format(PAPER_SIZES)), OptionRecommendation(name='custom_size', recommended_value=None, help=_('Custom size of the document. Use the form widthxheight ' 'e.g. `123x321` to specify the width and height. ' @@ -163,44 +162,34 @@ class PDFOutput(OutputFormatPlugin): 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 - Split()(oeb_book, opts) - must_use_qt() - load_builtin_fonts() + must_use_qt() + load_builtin_fonts() - self.oeb = oeb_book - self.input_plugin, self.opts, self.log = input_plugin, opts, log - self.output_path = output_path - from calibre.ebooks.oeb.base import OPF, OPF2_NS - from lxml import etree - from io import BytesIO - package = etree.Element(OPF('package'), - attrib={'version': '2.0', 'unique-identifier': 'dummy'}, - nsmap={None: OPF2_NS}) - from calibre.ebooks.metadata.opf2 import OPF - self.oeb.metadata.to_opf2(package) - self.metadata = OPF(BytesIO(etree.tostring(package))).to_book_metadata() - self.cover_data = None + self.oeb = oeb_book + self.input_plugin, self.opts, self.log = input_plugin, opts, log + self.output_path = output_path + from calibre.ebooks.oeb.base import OPF, OPF2_NS + from lxml import etree + from io import BytesIO + package = etree.Element(OPF('package'), + attrib={'version': '2.0', 'unique-identifier': 'dummy'}, + nsmap={None: OPF2_NS}) + from calibre.ebooks.metadata.opf2 import OPF + self.oeb.metadata.to_opf2(package) + self.metadata = OPF(BytesIO(etree.tostring(package))).to_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()) - else: - log.debug('Converting input as a text based book...') - self.convert_text(oeb_book) - finally: - os.environ.pop('CALIBRE_WEBKIT_NO_HINTING', None) + if input_plugin.is_image_collection: + log.debug('Converting input as an image collection...') + self.convert_images(input_plugin.get_images()) + else: + log.debug('Converting input as a text based book...') + self.convert_text(oeb_book) def convert_images(self, images): - from calibre.ebooks.pdf.render.from_html import ImagePDFWriter - self.write(ImagePDFWriter, images, None) + from calibre.ebooks.pdf.image_writer import convert + convert(images, self.output_path, self.opts) def get_cover_data(self): oeb = self.oeb @@ -210,8 +199,8 @@ class PDFOutput(OutputFormatPlugin): self.cover_data = item.data def process_fonts(self): - ''' Make sure all fonts are embeddable. Also remove some fonts that cause problems. ''' - from calibre.ebooks.oeb.base import urlnormalize, css_text + ''' Make sure all fonts are embeddable ''' + from calibre.ebooks.oeb.base import urlnormalize from calibre.utils.fonts.utils import remove_embed_restriction processed = set() @@ -240,19 +229,6 @@ class PDFOutput(OutputFormatPlugin): if nraw != raw: ff.data = nraw self.oeb.container.write(path, nraw) - elif iswindows and rule.type == rule.STYLE_RULE: - from tinycss.fonts3 import parse_font_family, serialize_font_family - s = rule.style - f = s.getProperty('font-family') - if f is not None: - font_families = parse_font_family(css_text(f.propertyValue)) - ff = [x for x in font_families if x.lower() != 'courier'] - if len(ff) != len(font_families): - if 'courier' not in self.filtered_font_warnings: - # See https://bugs.launchpad.net/bugs/1665835 - self.filtered_font_warnings.add('courier') - self.log.warn('Removing courier font family as it does not render on windows') - f.propertyValue.cssText = serialize_font_family(ff or ['monospace']) def convert_text(self, oeb_book): from calibre.ebooks.metadata.opf2 import OPF @@ -271,18 +247,6 @@ class PDFOutput(OutputFormatPlugin): if hasattr(root, 'xpath') and margins: root.set('data-calibre-pdf-output-page-margins', json.dumps(margins)) - # Remove javascript - for item in self.oeb.spine: - root = item.data - if hasattr(root, 'xpath'): - for script in root.xpath('//*[local-name()="script"]'): - script.text = None - script.attrib.clear() - for elem in root.iter('*'): - for attr in tuple(elem.attrib): - if attr.startswith('on'): - elem.set(attr, '') - with TemporaryDirectory('_pdf_out') as oeb_dir: from calibre.customize.ui import plugin_for_output_format oeb_output = plugin_for_output_format('oeb') diff --git a/src/calibre/ebooks/docx/writer/container.py b/src/calibre/ebooks/docx/writer/container.py index ae6108dbfb..1c0d25afa0 100644 --- a/src/calibre/ebooks/docx/writer/container.py +++ b/src/calibre/ebooks/docx/writer/container.py @@ -14,10 +14,10 @@ from calibre import guess_type from calibre.constants import numeric_version, __appname__ from calibre.ebooks.docx.names import DOCXNamespace from calibre.ebooks.metadata import authors_to_string +from calibre.ebooks.pdf.render.common import PAPER_SIZES from calibre.utils.date import utcnow from calibre.utils.localization import canonicalize_lang, lang_as_iso639_1 from calibre.utils.zipfile import ZipFile -from calibre.ebooks.pdf.render.common import PAPER_SIZES from polyglot.builtins import iteritems, map, unicode_type, native_string_type diff --git a/src/calibre/ebooks/pdf/image_writer.py b/src/calibre/ebooks/pdf/image_writer.py new file mode 100644 index 0000000000..1751bd17e7 --- /dev/null +++ b/src/calibre/ebooks/pdf/image_writer.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2019, Kovid Goyal + +from __future__ import absolute_import, division, print_function, unicode_literals + +from PyQt5.Qt import ( + QMarginsF, QPageLayout, QPageSize, QPainter, QPdfWriter, QSize +) + +from calibre import fit_image +from calibre.ebooks.docx.writer.container import cicero, cm, didot, inch, mm, pica +from calibre.utils.img import image_from_path + +# Page layout {{{ + + +def get_page_size(opts, for_comic=False): + use_profile = opts.use_profile_size and opts.output_profile.short_name != 'default' and opts.output_profile.width <= 9999 + if use_profile: + w = (opts.output_profile.comic_screen_size[0] if for_comic else + opts.output_profile.width) + h = (opts.output_profile.comic_screen_size[1] if for_comic else + opts.output_profile.height) + dpi = opts.output_profile.dpi + factor = 72.0 / dpi + page_size = QPageSize(QSize(factor * w, factor * h), matchPolicy=QPageSize.ExactMatch) + else: + page_size = None + if opts.custom_size is not None: + width, sep, height = opts.custom_size.partition('x') + if height: + try: + width = float(width.replace(',', '.')) + height = float(height.replace(',', '.')) + except: + pass + else: + if opts.unit == 'devicepixel': + factor = 72.0 / opts.output_profile.dpi + else: + factor = { + 'point':1.0, 'inch':inch, 'cicero':cicero, + 'didot':didot, 'pica':pica, 'millimeter':mm, + 'centimeter':cm + }[opts.unit] + page_size = QPageSize(QSize(factor*width, factor*height), matchPolicy=QPageSize.ExactMatch) + if page_size is None: + page_size = QPageSize(getattr(QPageSize, opts.paper_size.capitalize())) + return page_size + + +def get_page_layout(opts, for_comic=False): + page_size = get_page_size(opts, for_comic) + + def m(which): + return max(0, getattr(opts, 'pdf_page_margin_' + which) or getattr(opts, 'margin_' + which)) + + margins = QMarginsF(m('left'), m('top'), m('right'), m('bottom')) + ans = QPageLayout(page_size, QPageLayout.Portrait, margins) + return ans +# }}} + + +def draw_image_page(painter, img, preserve_aspect_ratio=True): + page_rect = painter.viewport() + if preserve_aspect_ratio: + aspect_ratio = float(img.width())/img.height() + 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 + nw = page_rect.height()*aspect_ratio + __, nnw, nnh = fit_image(nw, nh, page_rect.width(), + page_rect.height()) + dx = int((page_rect.width() - nnw)/2.) + dy = int((page_rect.height() - nnh)/2.) + page_rect.translate(dx, dy) + page_rect.setHeight(nnh) + page_rect.setWidth(nnw) + painter.drawImage(page_rect, img) + + +def convert(images, output_path, opts): + writer = QPdfWriter(output_path) + writer.setPageLayout(get_page_layout(opts, for_comic=True)) + painter = QPainter() + painter.begin(writer) + try: + for i, path in enumerate(images): + if i > 0: + writer.newPage() + img = image_from_path(path) + draw_image_page(painter, img) + finally: + painter.end() diff --git a/src/calibre/ebooks/pdf/render/engine.py b/src/calibre/ebooks/pdf/render/engine.py deleted file mode 100644 index f082dae14f..0000000000 --- a/src/calibre/ebooks/pdf/render/engine.py +++ /dev/null @@ -1,433 +0,0 @@ -#!/usr/bin/env python2 -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai -from __future__ import absolute_import, division, print_function, unicode_literals - -__license__ = 'GPL v3' -__copyright__ = '2012, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - -import sys, traceback, math -from collections import namedtuple -from functools import wraps, partial -from polyglot.builtins import map, zip - -from PyQt5.Qt import (QPaintEngine, QPaintDevice, Qt, QTransform, QBrush) - -from calibre.constants import plugins -from calibre.ebooks.pdf.render.serialize import (PDFStream, Path) -from calibre.ebooks.pdf.render.common import inch, A4, fmtnum -from calibre.ebooks.pdf.render.graphics import convert_path, Graphics -from calibre.utils.fonts.sfnt.container import Sfnt, UnsupportedFont -from calibre.utils.fonts.sfnt.metrics import FontMetrics -from polyglot.builtins import codepoint_to_chr, itervalues - -Point = namedtuple('Point', 'x y') -ColorState = namedtuple('ColorState', 'color opacity do') -GlyphInfo = namedtuple('GlyphInfo', 'name size stretch positions indices') - - -def repr_transform(t): - vals = map(fmtnum, (t.m11(), t.m12(), t.m21(), t.m22(), t.dx(), t.dy())) - return '[%s]'%' '.join(vals) - - -def store_error(func): - - @wraps(func) - def errh(self, *args, **kwargs): - try: - func(self, *args, **kwargs) - except: - self.errors_occurred = True - self.errors(traceback.format_exc()) - - return errh - - -class Font(FontMetrics): - - def __init__(self, sfnt): - FontMetrics.__init__(self, sfnt) - self.glyph_map = {} - - -class PdfEngine(QPaintEngine): - - FEATURES = QPaintEngine.AllFeatures & ~( - QPaintEngine.PorterDuff | QPaintEngine.PerspectiveTransform | QPaintEngine.ObjectBoundingModeGradients | QPaintEngine.RadialGradientFill | QPaintEngine.ConicalGradientFill) # noqa - - def __init__(self, file_object, page_width, page_height, left_margin, - top_margin, right_margin, bottom_margin, width, height, - errors=print, debug=print, compress=True, - mark_links=False, opts=None, page_margins=(0, 0, 0, 0)): - QPaintEngine.__init__(self, self.FEATURES) - self.file_object = file_object - self.compress, self.mark_links = compress, mark_links - self.page_height, self.page_width = page_height, page_width - 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 - 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 - self.fonts = {} - self.current_page_num = 1 - self.current_page_inited = False - self.content_written_to_current_page = False - self.qt_hack, err = plugins['qt_hack'] - self.has_footers = opts is not None and (opts.pdf_page_numbers or opts.pdf_footer_template is not None) - self.has_headers = opts is not None and opts.pdf_header_template is not None - ml, mr, mt, mb = page_margins - self.header_height = mt - self.footer_height = mb - 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()) - - def resolve_fill(self, rect): - self.graphics.resolve_fill(rect, self.pdf_system, - self.painter().transform()) - - @property - def do_fill(self): - return self.graphics.current_state.do_fill - - @property - def do_stroke(self): - return self.graphics.current_state.do_stroke - - 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() - self.current_page_inited = True - - def begin(self, device): - if not hasattr(self, 'pdf'): - try: - self.pdf = PDFStream(self.file_object, (self.page_width, - self.page_height), compress=self.compress, - mark_links=self.mark_links, - debug=self.debug) - self.graphics.begin(self.pdf) - except: - self.errors(traceback.format_exc()) - self.errors_occurred = True - return False - return True - - def end_page(self, is_last_page=False): - if self.current_page_inited: - self.pdf.restore_stack() - drop_page = is_last_page and not self.content_written_to_current_page - self.pdf.end_page(drop_page=drop_page) - self.current_page_inited = False - self.current_page_num += 0 if drop_page else 1 - return self.content_written_to_current_page - - def end(self): - try: - self.end_page() - self.pdf.end() - except: - self.errors(traceback.format_exc()) - self.errors_occurred = True - return False - finally: - self.pdf = self.file_object = None - return True - - def type(self): - return QPaintEngine.Pdf - - def add_image(self, img, cache_key): - if img.isNull(): - return - return self.pdf.add_image(img, cache_key) - - @store_error - def drawTiledPixmap(self, rect, pixmap, point): - self.content_written_to_current_page = 'drawTiledPixmap' - self.apply_graphics_state() - brush = QBrush(pixmap) - bl = rect.topLeft() - color, opacity, pattern, do_fill = self.graphics.convert_brush( - brush, bl-point, 1.0, self.pdf_system, - self.painter().transform()) - self.pdf.save_stack() - self.pdf.apply_fill(color, pattern) - self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(), - stroke=False, fill=True) - self.pdf.restore_stack() - - @store_error - def drawPixmap(self, rect, pixmap, source_rect): - self.content_written_to_current_page = 'drawPixmap' - self.apply_graphics_state() - source_rect = source_rect.toRect() - pixmap = (pixmap if source_rect == pixmap.rect() else - pixmap.copy(source_rect)) - image = pixmap.toImage() - ref = self.add_image(image, pixmap.cacheKey()) - if ref is not None: - self.pdf.draw_image(rect.x(), rect.y(), rect.width(), - rect.height(), ref) - - @store_error - def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor): - self.content_written_to_current_page = 'drawImage' - self.apply_graphics_state() - source_rect = source_rect.toRect() - image = (image if source_rect == image.rect() else - image.copy(source_rect)) - ref = self.add_image(image, image.cacheKey()) - if ref is not None: - self.pdf.draw_image(rect.x(), rect.y(), rect.width(), - rect.height(), ref) - - @store_error - def updateState(self, state): - self.graphics.update_state(state, self.painter()) - - @store_error - def drawPath(self, path): - self.content_written_to_current_page = 'drawPath' - self.apply_graphics_state() - p = convert_path(path) - fill_rule = {Qt.OddEvenFill:'evenodd', - Qt.WindingFill:'winding'}[path.fillRule()] - self.pdf.draw_path(p, stroke=self.do_stroke, - fill=self.do_fill, fill_rule=fill_rule) - - @store_error - def drawPoints(self, points): - self.content_written_to_current_page = 'drawPoints' - self.apply_graphics_state() - p = Path() - for point in points: - p.move_to(point.x(), point.y()) - p.line_to(point.x(), point.y() + 0.001) - self.pdf.draw_path(p, stroke=self.do_stroke, fill=False) - - @store_error - def drawRects(self, rects): - self.apply_graphics_state() - with self.graphics: - for rect in rects: - self.resolve_fill(rect) - bl = rect.topLeft() - if self.do_stroke or self.do_fill: - self.content_written_to_current_page = 'drawRects' - self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(), - stroke=self.do_stroke, fill=self.do_fill) - - def create_sfnt(self, text_item): - get_table = partial(self.qt_hack.get_sfnt_table, text_item) - try: - ans = Font(Sfnt(get_table)) - except UnsupportedFont as e: - raise UnsupportedFont('The font %s is not a valid sfnt. Error: %s'%( - text_item.font().family(), e)) - glyph_map = self.qt_hack.get_glyph_map(text_item) - gm = {} - ans.ignore_glyphs = set() - for uc, glyph_id in enumerate(glyph_map): - if glyph_id not in gm: - gm[glyph_id] = codepoint_to_chr(uc) - if uc in (0xad, 0x200b): - ans.ignore_glyphs.add(glyph_id) - ans.full_glyph_map = gm - return ans - - @store_error - def drawTextItem(self, point, text_item): - # return super(PdfEngine, self).drawTextItem(point, text_item) - self.apply_graphics_state() - gi = GlyphInfo(*self.qt_hack.get_glyphs(point, text_item)) - if not gi.indices: - return - metrics = self.fonts.get(gi.name) - if metrics is None: - from calibre.utils.fonts.utils import get_all_font_names - try: - names = get_all_font_names(gi.name, True) - names = ' '.join('%s=%s'%(k, names[k]) for k in sorted(names)) - except Exception: - names = 'Unknown' - self.debug('Loading font: %s' % names) - try: - self.fonts[gi.name] = metrics = self.create_sfnt(text_item) - except UnsupportedFont: - self.debug('Failed to load font: %s, drawing text as outlines...' % names) - return super(PdfEngine, self).drawTextItem(point, text_item) - indices, positions = [], [] - ignore_glyphs = metrics.ignore_glyphs - for glyph_id, gpos in zip(gi.indices, gi.positions): - if glyph_id not in ignore_glyphs: - indices.append(glyph_id), positions.append(gpos) - for glyph_id in indices: - try: - metrics.glyph_map[glyph_id] = metrics.full_glyph_map[glyph_id] - except (KeyError, ValueError): - pass - glyphs = [] - last_x = last_y = 0 - for glyph_index, (x, y) in zip(indices, positions): - glyphs.append((x-last_x, last_y - y, glyph_index)) - last_x, last_y = x, y - - if not self.content_written_to_current_page: - dy = self.graphics.current_state.transform.dy() - ypositions = [y + dy for x, y in positions] - miny = min(ypositions or (0,)) - maxy = max(ypositions or (self.pixel_height,)) - page_top = self.header_height if self.has_headers else 0 - page_bottom = self.pixel_height - (self.footer_height if self.has_footers else 0) - if page_top <= miny <= page_bottom or page_top <= maxy <= page_bottom: - self.content_written_to_current_page = 'drawTextItem' - else: - self.debug('Text in header/footer: miny=%s maxy=%s page_top=%s page_bottom=%s'% ( - miny, maxy, page_top, page_bottom)) - self.pdf.draw_glyph_run([gi.stretch, 0, 0, -1, 0, 0], gi.size, metrics, - glyphs) - - @store_error - def drawPolygon(self, points, mode): - self.content_written_to_current_page = 'drawPolygon' - self.apply_graphics_state() - if not points: - return - p = Path() - p.move_to(points[0].x(), points[0].y()) - for point in points[1:]: - p.line_to(point.x(), point.y()) - p.close() - fill_rule = {self.OddEvenMode:'evenodd', - self.WindingMode:'winding'}.get(mode, 'evenodd') - self.pdf.draw_path(p, stroke=True, fill_rule=fill_rule, - fill=(mode in (self.OddEvenMode, self.WindingMode, self.ConvexMode))) - - def set_metadata(self, *args, **kwargs): - self.pdf.set_metadata(*args, **kwargs) - - def add_outline(self, toc): - self.pdf.links.add_outline(toc) - - def add_links(self, current_item, start_page, links, anchors): - for pos in itervalues(anchors): - pos['left'], pos['top'] = self.pdf_system.map(pos['left'], pos['top']) - for link in links: - pos = link[1] - llx = pos['left'] - lly = pos['top'] + pos['height'] - urx = pos['left'] + pos['width'] - ury = pos['top'] - llx, lly = self.pdf_system.map(llx, lly) - urx, ury = self.pdf_system.map(urx, ury) - link[1] = pos['column'] + start_page - link.append((llx, lly, urx, ury)) - self.pdf.links.add(current_item, start_page, links, anchors) - - -class PdfDevice(QPaintDevice): # {{{ - - def __init__(self, file_object, page_size=A4, left_margin=inch, - top_margin=inch, right_margin=inch, bottom_margin=inch, - xdpi=1200, ydpi=1200, errors=print, debug=print, - compress=True, mark_links=False, opts=None, page_margins=(0, 0, 0, 0)): - QPaintDevice.__init__(self) - self.xdpi, self.ydpi = xdpi, ydpi - self.page_width, self.page_height = page_size - self.body_width = self.page_width - left_margin - right_margin - self.body_height = self.page_height - top_margin - bottom_margin - self.left_margin, self.right_margin = left_margin, right_margin - self.top_margin, self.bottom_margin = top_margin, bottom_margin - self.engine = PdfEngine(file_object, self.page_width, self.page_height, - left_margin, top_margin, right_margin, - bottom_margin, self.width(), self.height(), - errors=errors, debug=debug, compress=compress, - mark_links=mark_links, opts=opts, page_margins=page_margins) - self.add_outline = self.engine.add_outline - self.add_links = self.engine.add_links - - def paintEngine(self): - return self.engine - - def metric(self, m): - if m in (self.PdmDpiX, self.PdmPhysicalDpiX): - return self.xdpi - if m in (self.PdmDpiY, self.PdmPhysicalDpiY): - return self.ydpi - if m == self.PdmDepth: - return 32 - if m == self.PdmNumColors: - return sys.maxsize - if m == self.PdmWidthMM: - return int(round(self.body_width * 0.35277777777778)) - if m == self.PdmHeightMM: - return int(round(self.body_height * 0.35277777777778)) - if m == self.PdmWidth: - return int(round(self.body_width * self.xdpi / 72.0)) - if m == self.PdmHeight: - return int(round(self.body_height * self.ydpi / 72.0)) - return 0 - - def end_page(self, *args, **kwargs): - self.engine.end_page(*args, **kwargs) - - def init_page(self, custom_margins=None): - self.engine.init_page(custom_margins=custom_margins) - - @property - def full_page_rect(self): - page_width = int(math.ceil(self.page_width * self.xdpi / 72.0)) - lm = int(math.ceil(self.left_margin * self.xdpi / 72.0)) - page_height = int(math.ceil(self.page_height * self.ydpi / 72.0)) - tm = int(math.ceil(self.top_margin * self.ydpi / 72.0)) - return (-lm, -tm, page_width+1, page_height+1) - - @property - def current_page_num(self): - return self.engine.current_page_num - - @property - def errors_occurred(self): - return self.engine.errors_occurred - - 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) - -# }}} diff --git a/src/calibre/ebooks/pdf/render/from_html.py b/src/calibre/ebooks/pdf/render/from_html.py deleted file mode 100644 index 556c44219d..0000000000 --- a/src/calibre/ebooks/pdf/render/from_html.py +++ /dev/null @@ -1,565 +0,0 @@ -#!/usr/bin/env python2 -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai -from __future__ import absolute_import, division, print_function, unicode_literals - -__license__ = 'GPL v3' -__copyright__ = '2012, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - -import json, os, numbers -from math import floor -from collections import defaultdict - -from PyQt5.Qt import ( - QObject, QPainter, Qt, QSize, QTimer, QEventLoop, QPixmap, QRect, pyqtSlot) -from PyQt5.QtWebKit import QWebSettings -from PyQt5.QtWebKitWidgets import QWebView, QWebPage - -from calibre import fit_image -from calibre.constants import iswindows -from calibre.ebooks.oeb.display.webview import load_html -from calibre.ebooks.pdf.render.common import (inch, cm, mm, pica, cicero, - didot, PAPER_SIZES, current_log) -from calibre.ebooks.pdf.render.engine import PdfDevice -from calibre.ptempfile import PersistentTemporaryFile -from calibre.utils.resources import load_hyphenator_dicts -from calibre.utils.monotonic import monotonic -from polyglot.builtins import iteritems, itervalues, map, unicode_type - - -def get_page_size(opts, for_comic=False): # {{{ - use_profile = opts.use_profile_size and opts.output_profile.short_name != 'default' and opts.output_profile.width <= 9999 - if use_profile: - w = (opts.output_profile.comic_screen_size[0] if for_comic else - opts.output_profile.width) - h = (opts.output_profile.comic_screen_size[1] if for_comic else - opts.output_profile.height) - dpi = opts.output_profile.dpi - factor = 72.0 / dpi - page_size = (factor * w, factor * h) - else: - page_size = None - if opts.custom_size is not None: - width, sep, height = opts.custom_size.partition('x') - if height: - try: - width = float(width.replace(',', '.')) - height = float(height.replace(',', '.')) - except: - pass - else: - if opts.unit == 'devicepixel': - factor = 72.0 / opts.output_profile.dpi - else: - factor = {'point':1.0, 'inch':inch, 'cicero':cicero, - 'didot':didot, 'pica':pica, 'millimeter':mm, - 'centimeter':cm}[opts.unit] - page_size = (factor*width, factor*height) - if page_size is None: - page_size = PAPER_SIZES[opts.paper_size] - return page_size -# }}} - - -class Page(QWebPage): # {{{ - - def __init__(self, opts, log): - from calibre.gui2 import secure_web_page - self.log = log - QWebPage.__init__(self) - settings = self.settings() - settings.setFontSize(QWebSettings.DefaultFontSize, - opts.pdf_default_font_size) - settings.setFontSize(QWebSettings.DefaultFixedFontSize, - opts.pdf_mono_font_size) - settings.setFontSize(QWebSettings.MinimumLogicalFontSize, 8) - settings.setFontSize(QWebSettings.MinimumFontSize, 8) - secure_web_page(settings) - - std = {'serif':opts.pdf_serif_family, 'sans':opts.pdf_sans_family, - 'mono':opts.pdf_mono_family}.get(opts.pdf_standard_font, - opts.pdf_serif_family) - if std: - settings.setFontFamily(QWebSettings.StandardFont, std) - if opts.pdf_serif_family: - settings.setFontFamily(QWebSettings.SerifFont, opts.pdf_serif_family) - if opts.pdf_sans_family: - settings.setFontFamily(QWebSettings.SansSerifFont, - opts.pdf_sans_family) - if opts.pdf_mono_family: - settings.setFontFamily(QWebSettings.FixedFont, opts.pdf_mono_family) - self.longjs_counter = 0 - - def javaScriptConsoleMessage(self, msg, lineno, msgid): - self.log.debug(u'JS:', unicode_type(msg)) - - def javaScriptAlert(self, frame, msg): - self.log(unicode_type(msg)) - - @pyqtSlot(result=bool) - def shouldInterruptJavaScript(self): - if self.longjs_counter < 10: - self.log('Long running javascript, letting it proceed') - self.longjs_counter += 1 - return False - self.log.warn('Long running javascript, aborting it') - return True - -# }}} - - -def draw_image_page(page_rect, painter, p, preserve_aspect_ratio=True): - if preserve_aspect_ratio: - aspect_ratio = float(p.width())/p.height() - 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 - nw = page_rect.height()*aspect_ratio - __, nnw, nnh = fit_image(nw, nh, page_rect.width(), - page_rect.height()) - dx = int((page_rect.width() - nnw)/2.) - dy = int((page_rect.height() - nnh)/2.) - page_rect.translate(dx, dy) - page_rect.setHeight(nnh) - page_rect.setWidth(nnw) - painter.drawPixmap(page_rect, p, p.rect()) - - -class PDFWriter(QObject): - - @pyqtSlot(result=unicode_type) - def title(self): - return self.doc_title - - @pyqtSlot(result=unicode_type) - def author(self): - return self.doc_author - - @pyqtSlot(result=unicode_type) - def section(self): - return self.current_section - - @pyqtSlot(result=unicode_type) - def tl_section(self): - return self.current_tl_section - - def __init__(self, opts, log, cover_data=None, toc=None): - from calibre.gui2 import must_use_qt - must_use_qt() - QObject.__init__(self) - - self.logger = self.log = log - self.mathjax_dir = P('mathjax', allow_user_override=False) - current_log(log) - self.opts = opts - self.cover_data = cover_data - self.paged_js = None - self.toc = toc - - self.loop = QEventLoop() - self.view = QWebView() - self.page = Page(opts, self.log) - self.view.setPage(self.page) - self.view.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform) - self.view.loadFinished.connect(self.render_html, - type=Qt.QueuedConnection) - self.view.loadProgress.connect(self.load_progress) - self.ignore_failure = None - self.hang_check_timer = t = QTimer(self) - t.timeout.connect(self.hang_check) - t.setInterval(1000) - - for x in (Qt.Horizontal, Qt.Vertical): - self.view.page().mainFrame().setScrollBarPolicy(x, - Qt.ScrollBarAlwaysOff) - self.report_progress = lambda x, y: x - self.current_section = '' - self.current_tl_section = '' - - def dump(self, items, out_stream, pdf_metadata): - opts = self.opts - page_size = get_page_size(self.opts) - xdpi, ydpi = self.view.logicalDpiX(), self.view.logicalDpiY() - - def margin(which): - val = getattr(opts, 'pdf_page_margin_' + which) - if val == 0.0: - val = getattr(opts, 'margin_' + which) - return val - ml, mr, mt, mb = map(margin, 'left right top bottom'.split()) - # We cannot set the side margins in the webview as there is no right - # margin for the last page (the margins are implemented with - # -webkit-column-gap) - self.doc = PdfDevice(out_stream, page_size=page_size, left_margin=ml, - top_margin=0, right_margin=mr, bottom_margin=0, - xdpi=xdpi, ydpi=ydpi, errors=self.log.error, - debug=self.log.debug, compress=not - opts.uncompressed_pdf, opts=opts, - mark_links=opts.pdf_mark_links, page_margins=(ml, mr, mt, mb)) - self.footer = opts.pdf_footer_template - if self.footer: - self.footer = self.footer.strip() - if not self.footer and opts.pdf_page_numbers: - self.footer = '

_PAGENUM_

' - self.header = opts.pdf_header_template - if self.header: - self.header = self.header.strip() - min_margin = 1.5 * opts._final_base_font_size - if self.footer and mb < min_margin: - self.log.warn('Bottom margin is too small for footer, increasing it to %.1fpts' % min_margin) - mb = min_margin - if self.header and mt < min_margin: - self.log.warn('Top margin is too small for header, increasing it to %.1fpts' % min_margin) - mt = min_margin - - self.page.setViewportSize(QSize(self.doc.width(), self.doc.height())) - self.render_queue = items - self.total_items = len(items) - - mt, mb = map(self.doc.to_px, (mt, mb)) - self.margin_top, self.margin_bottom = map(lambda x:int(floor(x)), (mt, mb)) - - self.painter = QPainter(self.doc) - try: - self.book_language = pdf_metadata.mi.languages[0] - except Exception: - self.book_language = 'eng' - self.doc.set_metadata(title=pdf_metadata.title, - author=pdf_metadata.author, - tags=pdf_metadata.tags, mi=pdf_metadata.mi) - self.doc_title = pdf_metadata.title - self.doc_author = pdf_metadata.author - self.painter.save() - try: - if self.cover_data is not None: - p = QPixmap() - try: - p.loadFromData(self.cover_data) - except TypeError: - self.log.warn('This ebook does not have a raster cover, cannot generate cover for PDF' - '. Cover type: %s' % type(self.cover_data)) - if not p.isNull(): - self.doc.init_page() - draw_image_page(QRect(*self.doc.full_page_rect), - self.painter, p, - preserve_aspect_ratio=self.opts.preserve_cover_aspect_ratio) - self.doc.end_page() - finally: - self.painter.restore() - - QTimer.singleShot(0, self.render_book) - if self.loop.exec_() == 1: - raise Exception('PDF Output failed, see log for details') - - if self.toc is not None and len(self.toc) > 0: - self.doc.add_outline(self.toc) - - self.painter.end() - - if self.doc.errors_occurred: - raise Exception('PDF Output failed, see log for details') - - def render_inline_toc(self): - evaljs = self.view.page().mainFrame().evaluateJavaScript - 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, evaljs) - 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.opts.pdf_add_toc and 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() - except: - self.logger.exception('Rendering failed') - self.loop.exit(1) - - def render_next(self): - item = unicode_type(self.render_queue.pop(0)) - - self.logger.debug('Processing %s...' % item) - self.current_item = item - load_html(item, self.view) - self.last_load_progress_at = monotonic() - self.hang_check_timer.start() - - def load_progress(self, progress): - self.last_load_progress_at = monotonic() - - def hang_check(self): - if monotonic() - self.last_load_progress_at > 60: - self.log.warn('Timed out waiting for %s to render' % self.current_item) - self.ignore_failure = self.current_item - self.view.stop() - - def render_html(self, ok): - self.hang_check_timer.stop() - if self.ignore_failure == self.current_item: - ok = True - self.ignore_failure = None - if ok: - try: - self.do_paged_render() - except: - self.log.exception('Rendering failed') - self.loop.exit(1) - return - else: - # The document is so corrupt that we can't render the page. - self.logger.error('Document %s cannot be rendered.' % self.current_item) - self.loop.exit(1) - return - done = self.total_items - len(self.render_queue) - self.report_progress(done/self.total_items, - _('Rendered %s'%os.path.basename(self.current_item))) - self.render_book() - - @property - def current_page_num(self): - return self.doc.current_page_num - - def load_mathjax(self): - evaljs = self.view.page().mainFrame().evaluateJavaScript - mjpath = self.mathjax_dir.replace(os.sep, '/') - if iswindows: - mjpath = u'/' + mjpath - if bool(evaljs(''' - window.mathjax.base = %s; - mathjax.check_for_math(); mathjax.math_present - '''%(json.dumps(mjpath, ensure_ascii=False)))): - self.log.debug('Math present, loading MathJax') - while not bool(evaljs('mathjax.math_loaded')): - self.loop.processEvents(self.loop.ExcludeUserInputEvents) - # give the MathJax fonts time to load - for i in range(5): - self.loop.processEvents(self.loop.ExcludeUserInputEvents) - evaljs('document.getElementById("MathJax_Message").style.display="none";') - - def load_header_footer_images(self): - from calibre.utils.monotonic import monotonic - evaljs = self.view.page().mainFrame().evaluateJavaScript - st = monotonic() - while not evaljs('paged_display.header_footer_images_loaded()'): - self.loop.processEvents(self.loop.ExcludeUserInputEvents) - if monotonic() - st > 5: - self.log.warn('Header and footer images have not loaded in 5 seconds, ignoring') - break - - def get_sections(self, anchor_map, only_top_level=False): - sections = defaultdict(list) - ci = os.path.abspath(os.path.normcase(self.current_item)) - if self.toc is not None: - tocentries = self.toc.top_level_items() if only_top_level else self.toc.flat() - for toc in tocentries: - path = toc.abspath or None - frag = toc.fragment or None - if path is None: - continue - path = os.path.abspath(os.path.normcase(path)) - if path == ci: - col = 0 - if frag and frag in anchor_map: - col = anchor_map[frag]['column'] - sections[col].append(toc.text or _('Untitled')) - - return sections - - def hyphenate(self, evaljs): - evaljs(u'''\ - Hyphenator.config( - { - 'minwordlength' : 6, - // 'hyphenchar' : '|', - 'displaytogglebox' : false, - 'remoteloading' : false, - 'doframes' : true, - 'defaultlanguage' : 'en', - 'storagetype' : 'session', - 'onerrorhandler' : function (e) { - console.log(e); - } - }); - Hyphenator.hyphenate(document.body, "%s"); - ''' % 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 - from calibre.utils.resources import compiled_coffeescript as cc - self.paged_js = cc('ebooks.oeb.display.utils').decode('utf-8') - self.paged_js += cc('ebooks.oeb.display.indexing').decode('utf-8') - self.paged_js += cc('ebooks.oeb.display.paged').decode('utf-8') - self.paged_js += cc('ebooks.oeb.display.mathjax').decode('utf-8') - if self.opts.pdf_hyphenate: - self.paged_js += P('viewer/hyphenate/Hyphenator.js', data=True).decode('utf-8') - hjs, self.hyphenate_lang = load_hyphenator_dicts({}, self.book_language) - self.paged_js += hjs - self.hf_uuid = unicode_type(uuid.uuid4()).replace('-', '') - - self.view.page().mainFrame().addToJavaScriptWindowObject("py_bridge", self) - self.view.page().longjs_counter = 0 - evaljs = self.view.page().mainFrame().evaluateJavaScript - evaljs(self.paged_js) - self.load_mathjax() - 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 iteritems(doc_margins) if isinstance(v, numbers.Number) 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"; - // Qt WebKit cannot handle opacity with the Pdf backend - s = document.createElement('style'); - s.textContent = '* {opacity: 1 !important}'; - document.documentElement.appendChild(s); - paged_display.set_geometry(1, %d, %d, %d); - paged_display.layout(); - paged_display.fit_images(); - 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); - '''%(margin_top, 0, margin_bottom))) - - if not isinstance(amap, dict): - amap = {'links':[], 'anchors':{}} # Some javascript error occurred - for val in itervalues(amap['anchors']): - if isinstance(val, dict) and 'column' in val: - val['column'] = int(val['column']) - for href, val in amap['links']: - if isinstance(val, dict) and 'column' in val: - val['column'] = int(val['column']) - sections = self.get_sections(amap['anchors']) - tl_sections = self.get_sections(amap['anchors'], True) - col = 0 - - if self.header: - evaljs('paged_display.header_template = ' + json.dumps(self.header)) - if self.footer: - evaljs('paged_display.footer_template = ' + json.dumps(self.footer)) - if self.header or self.footer: - evaljs('paged_display.create_header_footer("%s");'%self.hf_uuid) - - start_page = self.current_page_num - - mf = self.view.page().mainFrame() - - def set_section(col, sections, attr): - # If this page has no section, use the section from the previous page - idx = col if col in sections else col - 1 if col - 1 in sections else None - if idx is not None: - setattr(self, attr, sections[idx][0]) - - from calibre.ebooks.pdf.render.toc import calculate_page_number - - while True: - set_section(col, sections, 'current_section') - set_section(col, tl_sections, 'current_tl_section') - self.doc.init_page(page_margins) - num = calculate_page_number(self.current_page_num, self.opts.pdf_page_number_map, evaljs) - if self.header or self.footer: - if evaljs('paged_display.update_header_footer(%d)'%num) is True: - self.load_header_footer_images() - - self.painter.save() - mf.render(self.painter, mf.ContentsLayer) - self.painter.restore() - try: - nsl = int(evaljs('paged_display.next_screen_location()')) - except (TypeError, ValueError): - break - self.doc.end_page(nsl <= 0) - if nsl <= 0: - break - evaljs('window.scrollTo(%d, 0); paged_display.position_header_footer();'%nsl) - if self.doc.errors_occurred: - break - col += 1 - - if not self.doc.errors_occurred and self.doc.current_page_num > 1: - self.doc.add_links(self.current_item, start_page, amap['links'], - amap['anchors']) - - -class ImagePDFWriter(object): - - def __init__(self, opts, log, cover_data=None, toc=None): - from calibre.gui2 import must_use_qt - must_use_qt() - - self.logger = self.log = log - self.opts = opts - self.cover_data = cover_data - self.toc = toc - - def dump(self, items, out_stream, pdf_metadata): - opts = self.opts - page_size = get_page_size(self.opts) - ml, mr = opts.margin_left, opts.margin_right - self.doc = PdfDevice( - out_stream, page_size=page_size, left_margin=ml, - top_margin=opts.margin_top, right_margin=mr, - bottom_margin=opts.margin_bottom, - errors=self.log.error, debug=self.log.debug, compress=not - opts.uncompressed_pdf, opts=opts, mark_links=opts.pdf_mark_links) - self.painter = QPainter(self.doc) - self.doc.set_metadata(title=pdf_metadata.title, - author=pdf_metadata.author, - tags=pdf_metadata.tags, mi=pdf_metadata.mi) - self.doc_title = pdf_metadata.title - self.doc_author = pdf_metadata.author - - for imgpath in items: - self.log.debug('Processing %s...' % imgpath) - self.doc.init_page() - p = QPixmap() - with lopen(imgpath, 'rb') as f: - if not p.loadFromData(f.read()): - raise ValueError('Could not read image from: {}'.format(imgpath)) - draw_image_page(QRect(*self.doc.full_page_rect), - self.painter, p, - preserve_aspect_ratio=True) - self.doc.end_page() - if self.toc is not None and len(self.toc) > 0: - self.doc.add_outline(self.toc) - - self.painter.end() - - if self.doc.errors_occurred: - raise Exception('PDF Output failed, see log for details') diff --git a/src/calibre/ebooks/pdf/render/qt_hack.cpp b/src/calibre/ebooks/pdf/render/qt_hack.cpp deleted file mode 100644 index 5974feb878..0000000000 --- a/src/calibre/ebooks/pdf/render/qt_hack.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * qt_hack.cpp - * Copyright (C) 2012 Kovid Goyal - * - * Distributed under terms of the GPL3 license. - */ - -#include "qt_hack.h" - -#include - -#include "private/qtextengine_p.h" -#include "private/qfontengine_p.h" - -#if PY_MAJOR_VERSION > 2 -#define BYTES_FMT "y#" -#else -#define BYTES_FMT "s#" -#endif - -PyObject* get_glyphs(const QPointF &p, const QTextItem &text_item) { - const quint32 *tag = reinterpret_cast("name"); - QTextItemInt ti = static_cast(text_item); - QFontEngine *fe = ti.fontEngine; - qreal size = ti.fontEngine->fontDef.pixelSize; -#ifdef Q_WS_WIN - if (false && ti.fontEngine->type() == QFontEngine::Win) { - // This is used in the Qt sourcecode, but it gives incorrect results, - // so I have disabled it. I dont understand how it works in qpdf.cpp - QFontEngineWin *fe = static_cast(ti.fontEngine); - // I think this should be tmHeight - tmInternalLeading, but pixelSize - // seems to work on windows as well, so leave it as pixelSize - size = fe->tm.tmHeight; - } -#endif - int synthesized = ti.fontEngine->synthesized(); - qreal stretch = synthesized & QFontEngine::SynthesizedStretch ? ti.fontEngine->fontDef.stretch/100. : 1.; - - QVarLengthArray glyphs; - QVarLengthArray positions; - QTransform m = QTransform::fromTranslate(p.x(), p.y()); - fe->getGlyphPositions(ti.glyphs, m, ti.flags, glyphs, positions); - - PyObject *points = NULL, *indices = NULL, *temp = NULL; - - points = PyTuple_New(positions.count()); - if (points == NULL) return PyErr_NoMemory(); - for (int i = 0; i < positions.count(); i++) { - temp = Py_BuildValue("dd", positions[i].x.toReal()/stretch, positions[i].y.toReal()); - if (temp == NULL) { Py_DECREF(points); return NULL; } - PyTuple_SET_ITEM(points, i, temp); temp = NULL; - } - - indices = PyTuple_New(glyphs.count()); - if (indices == NULL) { Py_DECREF(points); return PyErr_NoMemory(); } - for (int i = 0; i < glyphs.count(); i++) { -#if PY_MAJOR_VERSION >= 3 - temp = PyLong_FromLong((long)glyphs[i]); -#else - temp = PyInt_FromLong((long)glyphs[i]); -#endif - if (temp == NULL) { Py_DECREF(indices); Py_DECREF(points); return PyErr_NoMemory(); } - PyTuple_SET_ITEM(indices, i, temp); temp = NULL; - } - const QByteArray table(fe->getSfntTable(qToBigEndian(*tag))); - return Py_BuildValue(BYTES_FMT "ffOO", table.constData(), table.size(), size, stretch, points, indices); -} - -PyObject* get_sfnt_table(const QTextItem &text_item, const char* tag_name) { - QTextItemInt ti = static_cast(text_item); - const quint32 *tag = reinterpret_cast(tag_name); - const QByteArray table(ti.fontEngine->getSfntTable(qToBigEndian(*tag))); - return Py_BuildValue(BYTES_FMT, table.constData(), table.size()); -} - -PyObject* get_glyph_map(const QTextItem &text_item) { - QTextItemInt ti = static_cast(text_item); - QGlyphLayoutArray<10> glyphs; - int nglyphs = 10; - PyObject *t = NULL, *ans = PyTuple_New(0x10000); - - if (ans == NULL) return PyErr_NoMemory(); - - for (uint uc = 0; uc < 0x10000; ++uc) { - QChar ch(uc); - ti.fontEngine->stringToCMap(&ch, 1, &glyphs, &nglyphs, QFontEngine::GlyphIndicesOnly); -#if PY_MAJOR_VERSION >= 3 - t = PyLong_FromLong(glyphs.glyphs[0]); -#else - t = PyInt_FromLong(glyphs.glyphs[0]); -#endif - if (t == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); } - PyTuple_SET_ITEM(ans, uc, t); t = NULL; - } - return ans; -} diff --git a/src/calibre/ebooks/pdf/render/qt_hack.h b/src/calibre/ebooks/pdf/render/qt_hack.h deleted file mode 100644 index c7c06614fc..0000000000 --- a/src/calibre/ebooks/pdf/render/qt_hack.h +++ /dev/null @@ -1,20 +0,0 @@ -/* - * qt_hack.h - * Copyright (C) 2012 Kovid Goyal - * - * Distributed under terms of the GPL3 license. - */ - -#pragma once - -// Per python C-API docs, Python.h must always be the first header -#include -#include -#include -#include - -PyObject* get_glyphs(const QPointF &p, const QTextItem &text_item); - -PyObject* get_sfnt_table(const QTextItem &text_item, const char* tag_name); - -PyObject* get_glyph_map(const QTextItem &text_item); diff --git a/src/calibre/ebooks/pdf/render/qt_hack.sip b/src/calibre/ebooks/pdf/render/qt_hack.sip deleted file mode 100644 index fd31a928e4..0000000000 --- a/src/calibre/ebooks/pdf/render/qt_hack.sip +++ /dev/null @@ -1,16 +0,0 @@ -//Define the SIP wrapper to the qt_hack code -//Author - Kovid Goyal - -%Module(name=qt_hack) - -%Import QtCore/QtCoremod.sip -%Import QtGui/QtGuimod.sip -%ModuleCode -#include -%End - -PyObject* get_glyphs(const QPointF &p, const QTextItem &text_item); - -PyObject* get_sfnt_table(const QTextItem &text_item, const char* tag_name); - -PyObject* get_glyph_map(const QTextItem &text_item); diff --git a/src/calibre/ebooks/pdf/render/serialize.py b/src/calibre/ebooks/pdf/render/serialize.py index 492edaacb6..62dbc2a11d 100644 --- a/src/calibre/ebooks/pdf/render/serialize.py +++ b/src/calibre/ebooks/pdf/render/serialize.py @@ -296,6 +296,7 @@ class PDFStream(object): self.image_cache = {} self.pattern_cache, self.shader_cache = {}, {} self.debug = debug + self.page_size = page_size self.links = Links(self, mark_links, page_size) i = QImage(1, 1, QImage.Format_ARGB32) i.fill(qRgba(0, 0, 0, 255)) @@ -483,9 +484,11 @@ class PDFStream(object): return self.shader_cache[shader.cache_key] def draw_image(self, x, y, width, height, imgref): + self.draw_image_with_transform(imgref, scaling=(width, -height), translation=(x, y + height)) + + def draw_image_with_transform(self, imgref, translation=(0, 0), scaling=(1, 1)): name = self.current_page.add_image(imgref) - self.current_page.write('q %s 0 0 %s %s %s cm '%(fmtnum(width), - fmtnum(-height), fmtnum(x), fmtnum(y+height))) + self.current_page.write('q {} 0 0 {} {} {} cm '.format(*(tuple(scaling) + tuple(translation)))) serialize(Name(name), self.current_page) self.current_page.write_line(' Do Q') diff --git a/src/calibre/ebooks/pdf/render/test.py b/src/calibre/ebooks/pdf/render/test.py deleted file mode 100644 index 30b113099d..0000000000 --- a/src/calibre/ebooks/pdf/render/test.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python2 -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai -from __future__ import absolute_import, division, print_function, unicode_literals - -__license__ = 'GPL v3' -__copyright__ = '2012, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - -import os - -from PyQt5.Qt import (QBrush, QColor, QPoint, QPixmap, QPainterPath, QRectF, - QApplication, QPainter, Qt, QImage, QLinearGradient, - QPointF, QPen) -QBrush, QColor, QPoint, QPixmap, QPainterPath, QRectF, Qt, QPointF - -from calibre.ebooks.pdf.render.engine import PdfDevice -from polyglot.builtins import range - - -def full(p, xmax, ymax): - p.drawRect(0, 0, xmax, ymax) - p.drawPolyline(QPoint(0, 0), QPoint(xmax, 0), QPoint(xmax, ymax), - QPoint(0, ymax), QPoint(0, 0)) - pp = QPainterPath() - pp.addRect(0, 0, xmax, ymax) - p.drawPath(pp) - p.save() - for i in range(3): - col = [0, 0, 0, 200] - col[i] = 255 - p.setOpacity(0.3) - p.fillRect(0, 0, xmax/10, xmax/10, QBrush(QColor(*col))) - p.setOpacity(1) - p.drawRect(0, 0, xmax/10, xmax/10) - p.translate(xmax/10, xmax/10) - p.scale(1, 1.5) - p.restore() - - # p.scale(2, 2) - # p.rotate(45) - p.drawPixmap(0, 0, xmax/4, xmax/4, QPixmap(I('library.png'))) - p.drawRect(0, 0, xmax/4, xmax/4) - - f = p.font() - f.setPointSize(20) - # f.setLetterSpacing(f.PercentageSpacing, 200) - f.setUnderline(True) - # f.setOverline(True) - # f.setStrikeOut(True) - f.setFamily('Calibri') - p.setFont(f) - # p.setPen(QColor(0, 0, 255)) - # p.scale(2, 2) - # p.rotate(45) - p.drawText(QPoint(xmax/3.9, 30), 'Some—text not By’s ū --- Д AV ff ff') - - b = QBrush(Qt.HorPattern) - b.setColor(QColor(Qt.blue)) - pix = QPixmap(I('lt.png')) - w = xmax/4 - p.fillRect(0, ymax/3, w, w, b) - p.fillRect(xmax/3, ymax/3, w, w, QBrush(pix)) - x, y = 2*xmax/3, ymax/3 - p.drawTiledPixmap(QRectF(x, y, w, w), pix, QPointF(10, 10)) - - x, y = 1, ymax/1.9 - g = QLinearGradient(QPointF(x, y), QPointF(x+w, y+w)) - g.setColorAt(0, QColor('#00f')) - g.setColorAt(1, QColor('#fff')) - p.fillRect(x, y, w, w, QBrush(g)) - - -def run(dev, func): - p = QPainter(dev) - if isinstance(dev, PdfDevice): - dev.init_page() - xmax, ymax = p.viewport().width(), p.viewport().height() - try: - func(p, xmax, ymax) - finally: - p.end() - if isinstance(dev, PdfDevice): - if dev.engine.errors_occurred: - raise SystemExit(1) - - -def brush(p, xmax, ymax): - x = 0 - y = 0 - w = xmax/2 - g = QLinearGradient(QPointF(x, y+w/3), QPointF(x, y+(2*w/3))) - g.setColorAt(0, QColor('#f00')) - g.setColorAt(0.5, QColor('#fff')) - g.setColorAt(1, QColor('#00f')) - g.setSpread(g.ReflectSpread) - p.fillRect(x, y, w, w, QBrush(g)) - p.drawRect(x, y, w, w) - - -def pen(p, xmax, ymax): - pix = QPixmap(I('lt.png')) - pen = QPen(QBrush(pix), 60) - p.setPen(pen) - p.drawRect(0, xmax/3, xmax/3, xmax/2) - - -def text(p, xmax, ymax): - f = p.font() - f.setPixelSize(24) - f.setFamily('Candara') - p.setFont(f) - p.drawText(QPoint(0, 100), - 'Test intra glyph spacing ffagain imceo') - - -def main(): - app = QApplication([]) - app - tdir = os.path.abspath('.') - pdf = os.path.join(tdir, 'painter.pdf') - func = full - dpi = 100 - with open(pdf, 'wb') as f: - dev = PdfDevice(f, xdpi=dpi, ydpi=dpi, compress=False) - img = QImage(dev.width(), dev.height(), - QImage.Format_ARGB32_Premultiplied) - img.setDotsPerMeterX(dpi*39.37) - img.setDotsPerMeterY(dpi*39.37) - img.fill(Qt.white) - run(dev, func) - run(img, func) - path = os.path.join(tdir, 'painter.png') - img.save(path) - print('PDF written to:', pdf) - print('Image written to:', path) - - -if __name__ == '__main__': - main()