Start work on porting the PDF output plugin to use web engine

This commit is contained in:
Kovid Goyal 2019-07-03 17:00:18 +05:30
parent b7e57597a3
commit b5b0891421
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
12 changed files with 135 additions and 1350 deletions

View File

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

View File

@ -176,7 +176,6 @@ class Plugins(collections.Mapping):
'html',
'freetype',
'imageops',
'qt_hack',
'hunspell',
'_patiencediff_c',
'bzzdec',

View File

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

View File

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

View File

@ -0,0 +1,96 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
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()

View File

@ -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 <kovid at kovidgoyal.net>'
__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)
# }}}

View File

@ -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 <kovid at kovidgoyal.net>'
__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 = '<p style="text-align:center; text-indent: 0">_PAGENUM_</p>'
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')

View File

@ -1,96 +0,0 @@
/*
* qt_hack.cpp
* Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "qt_hack.h"
#include <QtEndian>
#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<const quint32 *>("name");
QTextItemInt ti = static_cast<const QTextItemInt &>(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<QFontEngineWin *>(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<glyph_t> glyphs;
QVarLengthArray<QFixedPoint> 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<const QTextItemInt &>(text_item);
const quint32 *tag = reinterpret_cast<const quint32 *>(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<const QTextItemInt &>(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;
}

View File

@ -1,20 +0,0 @@
/*
* qt_hack.h
* Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
// Per python C-API docs, Python.h must always be the first header
#include <Python.h>
#include <QGlyphRun>
#include <QTextItem>
#include <QPointF>
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);

View File

@ -1,16 +0,0 @@
//Define the SIP wrapper to the qt_hack code
//Author - Kovid Goyal <kovid@kovidgoyal.net>
%Module(name=qt_hack)
%Import QtCore/QtCoremod.sip
%Import QtGui/QtGuimod.sip
%ModuleCode
#include <qt_hack.h>
%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);

View File

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

View File

@ -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 <kovid at kovidgoyal.net>'
__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 Bys ū --- Д 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()