mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
New PDF engine, still incomplete (outlines and links have to be added, lots of testing needed)
This commit is contained in:
parent
955ace2687
commit
ccc6449df3
@ -14,50 +14,32 @@ import os
|
||||
from calibre.customize.conversion import OutputFormatPlugin, \
|
||||
OptionRecommendation
|
||||
from calibre.ptempfile import TemporaryDirectory
|
||||
from calibre.constants import iswindows
|
||||
|
||||
UNITS = [
|
||||
'millimeter',
|
||||
'point',
|
||||
'inch' ,
|
||||
'pica' ,
|
||||
'didot',
|
||||
'cicero',
|
||||
'devicepixel',
|
||||
]
|
||||
UNITS = ['millimeter', 'centimeter', 'point', 'inch' , 'pica' , 'didot',
|
||||
'cicero', 'devicepixel']
|
||||
|
||||
PAPER_SIZES = ['b2',
|
||||
'a9',
|
||||
'executive',
|
||||
'tabloid',
|
||||
'b4',
|
||||
'b5',
|
||||
'b6',
|
||||
'b7',
|
||||
'b0',
|
||||
'b1',
|
||||
'letter',
|
||||
'b3',
|
||||
'a7',
|
||||
'a8',
|
||||
'b8',
|
||||
'b9',
|
||||
'a3',
|
||||
'a1',
|
||||
'folio',
|
||||
'c5e',
|
||||
'dle',
|
||||
'a0',
|
||||
'ledger',
|
||||
'legal',
|
||||
'a6',
|
||||
'a2',
|
||||
'b10',
|
||||
'a5',
|
||||
'comm10e',
|
||||
'a4']
|
||||
PAPER_SIZES = ['b2', 'b4', 'b5', 'b6', 'b0', 'b1', 'letter', 'b3', 'a3', 'a1',
|
||||
'a0', 'legal', 'a6', 'a2', 'a5', 'a4']
|
||||
|
||||
ORIENTATIONS = ['portrait', 'landscape']
|
||||
class PDFMetadata(object): # {{{
|
||||
def __init__(self, oeb_metadata=None):
|
||||
from calibre import force_unicode
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
self.title = _(u'Unknown')
|
||||
self.author = _(u'Unknown')
|
||||
self.tags = u''
|
||||
|
||||
if oeb_metadata != None:
|
||||
if len(oeb_metadata.title) >= 1:
|
||||
self.title = oeb_metadata.title[0].value
|
||||
if len(oeb_metadata.creator) >= 1:
|
||||
self.author = authors_to_string([x.value for x in oeb_metadata.creator])
|
||||
if oeb_metadata.subject:
|
||||
self.tags = u', '.join(map(unicode, oeb_metadata.subject))
|
||||
|
||||
self.title = force_unicode(self.title)
|
||||
self.author = force_unicode(self.author)
|
||||
# }}}
|
||||
|
||||
class PDFOutput(OutputFormatPlugin):
|
||||
|
||||
@ -66,9 +48,14 @@ class PDFOutput(OutputFormatPlugin):
|
||||
file_type = 'pdf'
|
||||
|
||||
options = set([
|
||||
OptionRecommendation(name='override_profile_size', recommended_value=False,
|
||||
help=_('Normally, the PDF page size is set by the output profile'
|
||||
' chosen under page options. This option will cause the '
|
||||
' page size settings under PDF Output to override the '
|
||||
' size specified by the output profile.')),
|
||||
OptionRecommendation(name='unit', recommended_value='inch',
|
||||
level=OptionRecommendation.LOW, short_switch='u', choices=UNITS,
|
||||
help=_('The unit of measure. Default is inch. Choices '
|
||||
help=_('The unit of measure for page sizes. Default is inch. Choices '
|
||||
'are %s '
|
||||
'Note: This does not override the unit for margins!') % UNITS),
|
||||
OptionRecommendation(name='paper_size', recommended_value='letter',
|
||||
@ -80,10 +67,6 @@ class PDFOutput(OutputFormatPlugin):
|
||||
help=_('Custom size of the document. Use the form widthxheight '
|
||||
'EG. `123x321` to specify the width and height. '
|
||||
'This overrides any specified paper-size.')),
|
||||
OptionRecommendation(name='orientation', recommended_value='portrait',
|
||||
level=OptionRecommendation.LOW, choices=ORIENTATIONS,
|
||||
help=_('The orientation of the page. Default is portrait. Choices '
|
||||
'are %s') % ORIENTATIONS),
|
||||
OptionRecommendation(name='preserve_cover_aspect_ratio',
|
||||
recommended_value=False,
|
||||
help=_('Preserve the aspect ratio of the cover, instead'
|
||||
@ -108,6 +91,11 @@ class PDFOutput(OutputFormatPlugin):
|
||||
OptionRecommendation(name='pdf_mono_font_size',
|
||||
recommended_value=16, help=_(
|
||||
'The default font size for monospaced text')),
|
||||
OptionRecommendation(name='uncompressed_pdf',
|
||||
recommended_value=False, help=_(
|
||||
'Generate an uncompressed PDF (useful for debugging)')),
|
||||
OptionRecommendation(name='old_pdf_engine', recommended_value=False,
|
||||
help=_('Use the old, less capable engine to generate the PDF')),
|
||||
])
|
||||
|
||||
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
||||
@ -200,32 +188,17 @@ class PDFOutput(OutputFormatPlugin):
|
||||
if k in family_map:
|
||||
val[i].value = family_map[k]
|
||||
|
||||
def remove_font_specification(self):
|
||||
# Qt produces image based pdfs on windows when non-generic fonts are specified
|
||||
# This might change in Qt WebKit 2.3+ you will have to test.
|
||||
for item in self.oeb.manifest:
|
||||
if not hasattr(item.data, 'cssRules'): continue
|
||||
for i, rule in enumerate(item.data.cssRules):
|
||||
if rule.type != rule.STYLE_RULE: continue
|
||||
ff = rule.style.getProperty('font-family')
|
||||
if ff is None: continue
|
||||
val = ff.propertyValue
|
||||
for i in xrange(val.length):
|
||||
k = icu_lower(val[i].value)
|
||||
if k not in {'serif', 'sans', 'sans-serif', 'sansserif',
|
||||
'monospace', 'cursive', 'fantasy'}:
|
||||
val[i].value = ''
|
||||
|
||||
def convert_text(self, oeb_book):
|
||||
if self.opts.old_pdf_engine:
|
||||
from calibre.ebooks.pdf.writer import PDFWriter
|
||||
PDFWriter
|
||||
else:
|
||||
from calibre.ebooks.pdf.render.from_html import PDFWriter
|
||||
from calibre.ebooks.metadata.opf2 import OPF
|
||||
|
||||
self.log.debug('Serializing oeb input to disk for processing...')
|
||||
self.get_cover_data()
|
||||
|
||||
if iswindows:
|
||||
self.remove_font_specification()
|
||||
else:
|
||||
self.handle_embedded_fonts()
|
||||
|
||||
with TemporaryDirectory('_pdf_out') as oeb_dir:
|
||||
@ -240,7 +213,6 @@ class PDFOutput(OutputFormatPlugin):
|
||||
'toc', None))
|
||||
|
||||
def write(self, Writer, items, toc):
|
||||
from calibre.ebooks.pdf.writer import PDFMetadata
|
||||
writer = Writer(self.opts, self.log, cover_data=self.cover_data,
|
||||
toc=toc)
|
||||
|
||||
|
@ -18,6 +18,8 @@ inch = 72.0
|
||||
cm = inch / 2.54
|
||||
mm = cm * 0.1
|
||||
pica = 12.0
|
||||
didot = 0.375 * mm
|
||||
cicero = 12 * didot
|
||||
|
||||
_W, _H = (21*cm, 29.7*cm)
|
||||
|
||||
@ -41,6 +43,10 @@ B3 = (_BH*2, _BW)
|
||||
B2 = (_BW*2, _BH*2)
|
||||
B1 = (_BH*4, _BW*2)
|
||||
B0 = (_BW*4, _BH*4)
|
||||
|
||||
PAPER_SIZES = {k:globals()[k.upper()] for k in ('a0 a1 a2 a3 a4 a5 a6 b0 b1 b2'
|
||||
' b3 b4 b5 b6 letter legal').split()}
|
||||
|
||||
# }}}
|
||||
|
||||
# Basic PDF datatypes {{{
|
||||
@ -79,19 +85,12 @@ class String(unicode):
|
||||
raw = codecs.BOM_UTF16_BE + s.encode('utf-16-be')
|
||||
stream.write(b'('+raw+b')')
|
||||
|
||||
class GlyphIndex(object):
|
||||
|
||||
def __init__(self, code, compress):
|
||||
self.code = code
|
||||
self.compress = compress
|
||||
class GlyphIndex(int):
|
||||
|
||||
def pdf_serialize(self, stream):
|
||||
if self.compress:
|
||||
stream.write(pack(b'>sHs', b'(', self.code, b')'))
|
||||
else:
|
||||
byts = bytearray(pack(b'>H', self.code))
|
||||
byts = bytearray(pack(b'>H', self))
|
||||
stream.write('<%s>'%''.join(map(
|
||||
lambda x: bytes(hex(int(x))[2:]).rjust(2, b'0'), byts)))
|
||||
lambda x: bytes(hex(x)[2:]).rjust(2, b'0'), byts)))
|
||||
|
||||
class Dictionary(dict):
|
||||
|
||||
|
@ -14,17 +14,13 @@ from functools import wraps
|
||||
|
||||
from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter,
|
||||
QTransform, QPainterPath, QTextOption, QTextLayout,
|
||||
QImage, QByteArray, QBuffer, qRgba)
|
||||
QImage, QByteArray, QBuffer, qRgba, QRectF)
|
||||
|
||||
from calibre.constants import DEBUG
|
||||
from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path)
|
||||
from calibre.ebooks.pdf.render.common import inch, A4
|
||||
from calibre.utils.fonts.sfnt.container import Sfnt
|
||||
from calibre.utils.fonts.sfnt.metrics import FontMetrics
|
||||
|
||||
XDPI = 1200
|
||||
YDPI = 1200
|
||||
|
||||
Point = namedtuple('Point', 'x y')
|
||||
ColorState = namedtuple('ColorState', 'color opacity do')
|
||||
|
||||
@ -35,7 +31,8 @@ def store_error(func):
|
||||
try:
|
||||
func(self, *args, **kwargs)
|
||||
except:
|
||||
self.errors.append(traceback.format_exc())
|
||||
self.errors_occurred = True
|
||||
self.errors(traceback.format_exc())
|
||||
|
||||
return errh
|
||||
|
||||
@ -115,7 +112,7 @@ class GraphicsState(object): # {{{
|
||||
elif flags & QPaintEngine.DirtyClipRegion:
|
||||
path = QPainterPath()
|
||||
for rect in state.clipRegion().rects():
|
||||
path.addRect(rect)
|
||||
path.addRect(QRectF(rect))
|
||||
self.ops['clip'] = (state.clipOperation(), path)
|
||||
|
||||
def __call__(self, engine):
|
||||
@ -215,9 +212,11 @@ class Font(FontMetrics):
|
||||
class PdfEngine(QPaintEngine):
|
||||
|
||||
def __init__(self, file_object, page_width, page_height, left_margin,
|
||||
top_margin, right_margin, bottom_margin, width, height):
|
||||
top_margin, right_margin, bottom_margin, width, height,
|
||||
errors=print, debug=print, compress=True):
|
||||
QPaintEngine.__init__(self, self.features)
|
||||
self.file_object = file_object
|
||||
self.compress = compress
|
||||
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
|
||||
@ -242,18 +241,20 @@ class PdfEngine(QPaintEngine):
|
||||
self.scale = sqrt(sy**2 + sx**2)
|
||||
self.xscale, self.yscale = sx, sy
|
||||
self.graphics_state = GraphicsState()
|
||||
self.errors, self.debug = [], []
|
||||
self.errors_occurred = False
|
||||
self.errors, self.debug = errors, debug
|
||||
self.text_option = QTextOption()
|
||||
self.text_option.setWrapMode(QTextOption.NoWrap)
|
||||
self.fonts = {}
|
||||
i = QImage(1, 1, QImage.Format_ARGB32)
|
||||
i.fill(qRgba(0, 0, 0, 255))
|
||||
self.alpha_bit = i.constBits().asstring(4).find(b'\xff')
|
||||
self.current_page_num = 1
|
||||
|
||||
def init_page(self):
|
||||
self.pdf.transform(self.pdf_system)
|
||||
self.pdf.set_rgb_colorspace()
|
||||
width = self.painter.pen().widthF() if self.isActive() else 0
|
||||
width = self.painter().pen().widthF() if self.isActive() else 0
|
||||
self.pdf.set_line_width(width)
|
||||
self.do_stroke = True
|
||||
self.do_fill = False
|
||||
@ -271,7 +272,7 @@ class PdfEngine(QPaintEngine):
|
||||
try:
|
||||
self.pdf = PDFStream(self.file_object, (self.page_width,
|
||||
self.page_height),
|
||||
compress=not DEBUG)
|
||||
compress=self.compress)
|
||||
self.init_page()
|
||||
except:
|
||||
self.errors.append(traceback.format_exc())
|
||||
@ -281,6 +282,7 @@ class PdfEngine(QPaintEngine):
|
||||
def end_page(self, start_new=True):
|
||||
self.pdf.restore_stack()
|
||||
self.pdf.end_page()
|
||||
self.current_page_num += 1
|
||||
if start_new:
|
||||
self.init_page()
|
||||
|
||||
@ -488,7 +490,7 @@ class PdfEngine(QPaintEngine):
|
||||
glyph_map[g[0]] = string
|
||||
break
|
||||
if not found:
|
||||
self.debug.append(
|
||||
self.debug(
|
||||
'Failed to find glyph->unicode mapping for text: %s'%text)
|
||||
break
|
||||
ipos += 1
|
||||
@ -546,6 +548,9 @@ class PdfEngine(QPaintEngine):
|
||||
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 __enter__(self):
|
||||
self.pdf.save_stack()
|
||||
self.saved_ps = (self.do_stroke, self.do_fill)
|
||||
@ -558,23 +563,26 @@ class PdfDevice(QPaintDevice): # {{{
|
||||
|
||||
|
||||
def __init__(self, file_object, page_size=A4, left_margin=inch,
|
||||
top_margin=inch, right_margin=inch, bottom_margin=inch):
|
||||
top_margin=inch, right_margin=inch, bottom_margin=inch,
|
||||
xdpi=1200, ydpi=1200, errors=print, debug=print, compress=True):
|
||||
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.engine = PdfEngine(file_object, self.page_width, self.page_height,
|
||||
left_margin, top_margin, right_margin,
|
||||
bottom_margin, self.width(), self.height())
|
||||
bottom_margin, self.width(), self.height(),
|
||||
errors=errors, debug=debug, compress=compress)
|
||||
|
||||
def paintEngine(self):
|
||||
return self.engine
|
||||
|
||||
def metric(self, m):
|
||||
if m in (self.PdmDpiX, self.PdmPhysicalDpiX):
|
||||
return XDPI
|
||||
return self.xdpi
|
||||
if m in (self.PdmDpiY, self.PdmPhysicalDpiY):
|
||||
return YDPI
|
||||
return self.ydpi
|
||||
if m == self.PdmDepth:
|
||||
return 32
|
||||
if m == self.PdmNumColors:
|
||||
@ -584,10 +592,32 @@ class PdfDevice(QPaintDevice): # {{{
|
||||
if m == self.PdmHeightMM:
|
||||
return int(round(self.body_height * 0.35277777777778))
|
||||
if m == self.PdmWidth:
|
||||
return int(round(self.body_width * XDPI / 72.0))
|
||||
return int(round(self.body_width * self.xdpi / 72.0))
|
||||
if m == self.PdmHeight:
|
||||
return int(round(self.body_height * YDPI / 72.0))
|
||||
return int(round(self.body_height * self.ydpi / 72.0))
|
||||
return 0
|
||||
|
||||
def end_page(self, start_new=True):
|
||||
self.engine.end_page(start_new=start_new)
|
||||
|
||||
def init_page(self):
|
||||
self.engine.init_page()
|
||||
|
||||
@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 set_metadata(self, *args, **kwargs):
|
||||
self.engine.set_metadata(*args, **kwargs)
|
||||
|
||||
# }}}
|
||||
|
||||
if __name__ == '__main__':
|
||||
@ -596,7 +626,7 @@ if __name__ == '__main__':
|
||||
app = QApplication([])
|
||||
p = QPainter()
|
||||
with open('/tmp/painter.pdf', 'wb') as f:
|
||||
dev = PdfDevice(f)
|
||||
dev = PdfDevice(f, compress=False)
|
||||
p.begin(dev)
|
||||
xmax, ymax = p.viewport().width(), p.viewport().height()
|
||||
try:
|
||||
@ -642,9 +672,6 @@ if __name__ == '__main__':
|
||||
# p.drawText(QPoint(100, 300), 'Some text ū --- Д AV ff ff')
|
||||
finally:
|
||||
p.end()
|
||||
for line in dev.engine.debug:
|
||||
print (line)
|
||||
if dev.engine.errors:
|
||||
for err in dev.engine.errors: print (err)
|
||||
if dev.engine.errors_occurred:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
284
src/calibre/ebooks/pdf/render/from_html.py
Normal file
284
src/calibre/ebooks/pdf/render/from_html.py
Normal file
@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import json
|
||||
from future_builtins import map
|
||||
from math import floor
|
||||
|
||||
from PyQt4.Qt import (QObject, QPainter, Qt, QSize, QString, QTimer,
|
||||
pyqtProperty, QEventLoop, QPixmap, QRect)
|
||||
from PyQt4.QtWebKit import QWebView, QWebPage, QWebSettings
|
||||
|
||||
from calibre import fit_image
|
||||
from calibre.ebooks.oeb.display.webview import load_html
|
||||
from calibre.ebooks.pdf.render.engine import PdfDevice
|
||||
from calibre.ebooks.pdf.render.common import (inch, cm, mm, pica, cicero,
|
||||
didot, PAPER_SIZES)
|
||||
from calibre.ebooks.pdf.outline_writer import Outline
|
||||
|
||||
def get_page_size(opts, for_comic=False): # {{{
|
||||
use_profile = not (opts.override_profile_size or
|
||||
opts.output_profile.short_name == 'default')
|
||||
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 != None:
|
||||
width, sep, height = opts.custom_size.partition('x')
|
||||
if height:
|
||||
try:
|
||||
width = float(width)
|
||||
height = float(height)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
if opts.unit == 'devicepixel':
|
||||
factor = 72.0 / opts.output_profile.dpi
|
||||
else:
|
||||
{'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):
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def javaScriptConsoleMessage(self, msg, lineno, msgid):
|
||||
self.log.debug(u'JS:', unicode(msg))
|
||||
|
||||
def javaScriptAlert(self, frame, msg):
|
||||
self.log(unicode(msg))
|
||||
# }}}
|
||||
|
||||
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.moveTo(dx, dy)
|
||||
page_rect.setHeight(nnh)
|
||||
page_rect.setWidth(nnw)
|
||||
painter.drawPixmap(page_rect, p, p.rect())
|
||||
|
||||
class PDFWriter(QObject):
|
||||
|
||||
def _pass_json_value_getter(self):
|
||||
val = json.dumps(self.bridge_value)
|
||||
return QString(val)
|
||||
|
||||
def _pass_json_value_setter(self, value):
|
||||
self.bridge_value = json.loads(unicode(value))
|
||||
|
||||
_pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter,
|
||||
fset=_pass_json_value_setter)
|
||||
|
||||
def __init__(self, opts, log, cover_data=None, toc=None):
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
if not is_ok_to_use_qt():
|
||||
raise Exception('Not OK to use Qt')
|
||||
QObject.__init__(self)
|
||||
|
||||
self.logger = self.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)
|
||||
for x in (Qt.Horizontal, Qt.Vertical):
|
||||
self.view.page().mainFrame().setScrollBarPolicy(x,
|
||||
Qt.ScrollBarAlwaysOff)
|
||||
|
||||
def dump(self, items, out_stream, pdf_metadata):
|
||||
opts = self.opts
|
||||
self.outline = Outline(self.toc, items)
|
||||
page_size = get_page_size(self.opts)
|
||||
dpi = min(self.opts.input_profile.dpi, 150)
|
||||
ml, mr = opts.margin_left, opts.margin_right
|
||||
margin_side = min(ml, mr)
|
||||
ml, mr = ml - margin_side, mr - margin_side
|
||||
self.doc = PdfDevice(out_stream, page_size=page_size, left_margin=ml,
|
||||
top_margin=0, right_margin=mr, bottom_margin=0,
|
||||
xdpi=dpi, ydpi=dpi, errors=self.log.error,
|
||||
debug=self.log.debug, compress=not
|
||||
opts.uncompressed_pdf)
|
||||
|
||||
self.page.setViewportSize(QSize(self.doc.width(), self.doc.height()))
|
||||
self.render_queue = items
|
||||
self.first_page = True
|
||||
|
||||
# TODO: Test margins
|
||||
mt, mb = map(self.doc.to_px, (opts.margin_top, opts.margin_bottom))
|
||||
ms = self.doc.to_px(margin_side, vertical=False)
|
||||
self.margin_top, self.margin_size, self.margin_bottom = map(
|
||||
lambda x:int(floor(x)), (mt, ms, mb))
|
||||
|
||||
self.painter = QPainter(self.doc)
|
||||
self.doc.set_metadata(title=pdf_metadata.title,
|
||||
author=pdf_metadata.author,
|
||||
tags=pdf_metadata.tags)
|
||||
self.painter.save()
|
||||
try:
|
||||
if self.cover_data is not None:
|
||||
p = QPixmap()
|
||||
p.loadFromData(self.cover_data)
|
||||
if not p.isNull():
|
||||
draw_image_page(QRect(0, 0, self.doc.width(), self.doc.height()),
|
||||
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)
|
||||
self.loop.exec_()
|
||||
|
||||
# TODO: Outline and links
|
||||
self.painter.end()
|
||||
|
||||
if self.doc.errors_occurred:
|
||||
raise Exception('PDF Output failed, see log for details')
|
||||
|
||||
def render_book(self):
|
||||
if self.doc.errors_occurred:
|
||||
return self.loop.exit(1)
|
||||
try:
|
||||
if not self.render_queue:
|
||||
self.loop.exit()
|
||||
else:
|
||||
self.render_next()
|
||||
except:
|
||||
self.logger.exception('Rendering failed')
|
||||
self.loop.exit(1)
|
||||
|
||||
def render_next(self):
|
||||
item = unicode(self.render_queue.pop(0))
|
||||
|
||||
self.logger.debug('Processing %s...' % item)
|
||||
self.current_item = item
|
||||
load_html(item, self.view)
|
||||
|
||||
def render_html(self, ok):
|
||||
if ok:
|
||||
try:
|
||||
self.do_paged_render()
|
||||
except:
|
||||
self.log.exception('Rendering failed')
|
||||
self.loop.exit(1)
|
||||
else:
|
||||
# The document is so corrupt that we can't render the page.
|
||||
self.logger.error('Document cannot be rendered.')
|
||||
self.loop.exit(1)
|
||||
return
|
||||
self.render_book()
|
||||
|
||||
@property
|
||||
def current_page_num(self):
|
||||
return self.doc.current_page_num
|
||||
|
||||
def do_paged_render(self):
|
||||
if self.paged_js is None:
|
||||
from calibre.utils.resources import compiled_coffeescript
|
||||
self.paged_js = compiled_coffeescript('ebooks.oeb.display.utils')
|
||||
self.paged_js += compiled_coffeescript('ebooks.oeb.display.indexing')
|
||||
self.paged_js += compiled_coffeescript('ebooks.oeb.display.paged')
|
||||
|
||||
self.view.page().mainFrame().addToJavaScriptWindowObject("py_bridge", self)
|
||||
evaljs = self.view.page().mainFrame().evaluateJavaScript
|
||||
evaljs(self.paged_js)
|
||||
evaljs('''
|
||||
py_bridge.__defineGetter__('value', function() {
|
||||
return JSON.parse(this._pass_json_value);
|
||||
});
|
||||
py_bridge.__defineSetter__('value', function(val) {
|
||||
this._pass_json_value = JSON.stringify(val);
|
||||
});
|
||||
|
||||
document.body.style.backgroundColor = "white";
|
||||
paged_display.set_geometry(1, %d, %d, %d);
|
||||
paged_display.layout();
|
||||
paged_display.fit_images();
|
||||
'''%(self.margin_top, self.margin_size, self.margin_bottom))
|
||||
|
||||
mf = self.view.page().mainFrame()
|
||||
start_page = self.current_page_num
|
||||
while True:
|
||||
if not self.first_page:
|
||||
self.doc.init_page()
|
||||
self.first_page = False
|
||||
self.painter.save()
|
||||
try:
|
||||
mf.render(self.painter)
|
||||
nsl = evaljs('paged_display.next_screen_location()').toInt()
|
||||
if not nsl[1] or nsl[0] <= 0:
|
||||
break
|
||||
evaljs('window.scrollTo(%d, 0)'%nsl[0])
|
||||
self.doc.end_page()
|
||||
finally:
|
||||
self.painter.restore()
|
||||
if self.doc.errors_occurred:
|
||||
break
|
||||
|
||||
self.bridge_value = tuple(self.outline.anchor_map[self.current_item])
|
||||
evaljs('py_bridge.value = book_indexing.anchor_positions(py_bridge.value)')
|
||||
amap = self.bridge_value
|
||||
if not isinstance(amap, dict):
|
||||
amap = {} # Some javascript error occurred
|
||||
self.outline.set_pos(self.current_item, None, start_page, 0)
|
||||
for anchor, x in amap.iteritems():
|
||||
pagenum, ypos = x
|
||||
self.outline.set_pos(self.current_item, anchor, start_page + pagenum, ypos)
|
||||
|
@ -303,6 +303,14 @@ class PDFStream(object):
|
||||
def catalog(self):
|
||||
return self.objects[1]
|
||||
|
||||
def set_metadata(self, title=None, author=None, tags=None):
|
||||
if title:
|
||||
self.info['Title'] = String(title)
|
||||
if author:
|
||||
self.info['Author'] = String(author)
|
||||
if tags:
|
||||
self.info['Keywords'] = String(tags)
|
||||
|
||||
def write_line(self, byts=b''):
|
||||
byts = byts if isinstance(byts, bytes) else byts.encode('ascii')
|
||||
self.stream.write(byts + EOL)
|
||||
@ -409,7 +417,7 @@ class PDFStream(object):
|
||||
self.current_page.write('%s Tm '%' '.join(map(type(u''), transform)))
|
||||
for x, y, glyph_id in glyphs:
|
||||
self.current_page.write('%g %g Td '%(x, y))
|
||||
serialize(GlyphIndex(glyph_id, self.compress), self.current_page)
|
||||
serialize(GlyphIndex(glyph_id), self.current_page)
|
||||
self.current_page.write(' Tj ')
|
||||
self.current_page.write_line(b' ET')
|
||||
|
||||
|
@ -9,18 +9,16 @@ Write content to PDF.
|
||||
'''
|
||||
|
||||
import os, shutil, json
|
||||
from future_builtins import map
|
||||
|
||||
from PyQt4.Qt import (QEventLoop, QObject, QPrinter, QSizeF, Qt, QPainter,
|
||||
QPixmap, QTimer, pyqtProperty, QString, QSize)
|
||||
from PyQt4.QtWebKit import QWebView, QWebPage, QWebSettings
|
||||
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.ebooks.pdf.pageoptions import (unit, paper_size, orientation)
|
||||
from calibre.ebooks.pdf.pageoptions import (unit, paper_size)
|
||||
from calibre.ebooks.pdf.outline_writer import Outline
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre import (__appname__, __version__, fit_image, isosx, force_unicode)
|
||||
from calibre import (__appname__, __version__, fit_image, isosx)
|
||||
from calibre.ebooks.oeb.display.webview import load_html
|
||||
|
||||
def get_custom_size(opts):
|
||||
@ -72,7 +70,6 @@ def get_pdf_printer(opts, for_comic=False, output_file_name=None): # {{{
|
||||
else:
|
||||
printer.setPageMargins(opts.margin_left, opts.margin_top,
|
||||
opts.margin_right, opts.margin_bottom, QPrinter.Point)
|
||||
printer.setOrientation(orientation(opts.orientation))
|
||||
printer.setOutputFormat(QPrinter.PdfFormat)
|
||||
printer.setFullPage(for_comic)
|
||||
if output_file_name:
|
||||
@ -103,24 +100,6 @@ def draw_image_page(printer, painter, p, preserve_aspect_ratio=True):
|
||||
painter.drawPixmap(page_rect, p, p.rect())
|
||||
|
||||
|
||||
class PDFMetadata(object): # {{{
|
||||
def __init__(self, oeb_metadata=None):
|
||||
self.title = _(u'Unknown')
|
||||
self.author = _(u'Unknown')
|
||||
self.tags = u''
|
||||
|
||||
if oeb_metadata != None:
|
||||
if len(oeb_metadata.title) >= 1:
|
||||
self.title = oeb_metadata.title[0].value
|
||||
if len(oeb_metadata.creator) >= 1:
|
||||
self.author = authors_to_string([x.value for x in oeb_metadata.creator])
|
||||
if oeb_metadata.subject:
|
||||
self.tags = u', '.join(map(unicode, oeb_metadata.subject))
|
||||
|
||||
self.title = force_unicode(self.title)
|
||||
self.author = force_unicode(self.author)
|
||||
# }}}
|
||||
|
||||
class Page(QWebPage): # {{{
|
||||
|
||||
def __init__(self, opts, log):
|
||||
|
@ -18,16 +18,17 @@ class PluginWidget(Widget, Ui_Form):
|
||||
ICON = I('mimetypes/pdf.png')
|
||||
|
||||
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
||||
Widget.__init__(self, parent, ['paper_size', 'custom_size',
|
||||
'orientation', 'preserve_cover_aspect_ratio', 'pdf_serif_family',
|
||||
Widget.__init__(self, parent, [
|
||||
'override_profile_size', 'paper_size', 'custom_size',
|
||||
'preserve_cover_aspect_ratio', 'pdf_serif_family', 'unit',
|
||||
'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font',
|
||||
'pdf_default_font_size', 'pdf_mono_font_size'])
|
||||
self.db, self.book_id = db, book_id
|
||||
|
||||
for x in get_option('paper_size').option.choices:
|
||||
self.opt_paper_size.addItem(x)
|
||||
for x in get_option('orientation').option.choices:
|
||||
self.opt_orientation.addItem(x)
|
||||
for x in get_option('unit').option.choices:
|
||||
self.opt_unit.addItem(x)
|
||||
for x in get_option('pdf_standard_font').option.choices:
|
||||
self.opt_pdf_standard_font.addItem(x)
|
||||
|
||||
|
@ -14,7 +14,27 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="1" column="0">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::ExpandingFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="text">
|
||||
<string><b>Note:</b> The paper size settings below only take effect if you enable the "Override" checkbox below. Otherwise the size from the output profile will be used.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_override_profile_size">
|
||||
<property name="text">
|
||||
<string>&Override paper size set in output profile</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>&Paper Size:</string>
|
||||
@ -24,21 +44,8 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="opt_paper_size"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>&Orientation:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_orientation</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="opt_orientation"/>
|
||||
<widget class="QComboBox" name="opt_paper_size"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
@ -51,8 +58,25 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="opt_custom_size"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_11">
|
||||
<property name="text">
|
||||
<string>&Unit:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_unit</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="opt_unit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_preserve_cover_aspect_ratio">
|
||||
<property name="text">
|
||||
@ -60,19 +84,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>213</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
@ -159,15 +170,18 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="text">
|
||||
<string><b>Note:</b> The paper size settings below only take effect if you have set the output profile to the default output profile. Otherwise the output profile will override these settings.</string>
|
||||
<item row="11" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>213</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
|
Loading…
x
Reference in New Issue
Block a user