From ccc6449df3a81db4d721d127b446d9dac6e32e3c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 23 Dec 2012 15:59:02 +0530 Subject: [PATCH 1/7] New PDF engine, still incomplete (outlines and links have to be added, lots of testing needed) --- .../ebooks/conversion/plugins/pdf_output.py | 108 +++---- src/calibre/ebooks/pdf/render/common.py | 21 +- src/calibre/ebooks/pdf/render/engine.py | 73 +++-- src/calibre/ebooks/pdf/render/from_html.py | 284 ++++++++++++++++++ src/calibre/ebooks/pdf/render/serialize.py | 10 +- src/calibre/ebooks/pdf/writer.py | 25 +- src/calibre/gui2/convert/pdf_output.py | 9 +- src/calibre/gui2/convert/pdf_output.ui | 86 +++--- 8 files changed, 450 insertions(+), 166 deletions(-) create mode 100644 src/calibre/ebooks/pdf/render/from_html.py diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index da66a9be0d..2e4b370d50 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -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,33 +188,18 @@ 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): - from calibre.ebooks.pdf.writer import PDFWriter + 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() + self.handle_embedded_fonts() with TemporaryDirectory('_pdf_out') as oeb_dir: from calibre.customize.ui import plugin_for_output_format @@ -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) diff --git a/src/calibre/ebooks/pdf/render/common.py b/src/calibre/ebooks/pdf/render/common.py index 297e614560..554d170656 100644 --- a/src/calibre/ebooks/pdf/render/common.py +++ b/src/calibre/ebooks/pdf/render/common.py @@ -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)) - stream.write('<%s>'%''.join(map( - lambda x: bytes(hex(int(x))[2:]).rjust(2, b'0'), byts))) + byts = bytearray(pack(b'>H', self)) + stream.write('<%s>'%''.join(map( + lambda x: bytes(hex(x)[2:]).rjust(2, b'0'), byts))) class Dictionary(dict): diff --git a/src/calibre/ebooks/pdf/render/engine.py b/src/calibre/ebooks/pdf/render/engine.py index 47ed68f60b..ea8a42dc8f 100644 --- a/src/calibre/ebooks/pdf/render/engine.py +++ b/src/calibre/ebooks/pdf/render/engine.py @@ -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) diff --git a/src/calibre/ebooks/pdf/render/from_html.py b/src/calibre/ebooks/pdf/render/from_html.py new file mode 100644 index 0000000000..544eafe8a4 --- /dev/null +++ b/src/calibre/ebooks/pdf/render/from_html.py @@ -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 ' +__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) + diff --git a/src/calibre/ebooks/pdf/render/serialize.py b/src/calibre/ebooks/pdf/render/serialize.py index 9fe89cfafb..51d81f1b91 100644 --- a/src/calibre/ebooks/pdf/render/serialize.py +++ b/src/calibre/ebooks/pdf/render/serialize.py @@ -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') diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py index 76ab6b9096..46a3e92821 100644 --- a/src/calibre/ebooks/pdf/writer.py +++ b/src/calibre/ebooks/pdf/writer.py @@ -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): diff --git a/src/calibre/gui2/convert/pdf_output.py b/src/calibre/gui2/convert/pdf_output.py index a2bfcc667f..e0674d066c 100644 --- a/src/calibre/gui2/convert/pdf_output.py +++ b/src/calibre/gui2/convert/pdf_output.py @@ -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) diff --git a/src/calibre/gui2/convert/pdf_output.ui b/src/calibre/gui2/convert/pdf_output.ui index 5f77f526d0..5e3c4c9137 100644 --- a/src/calibre/gui2/convert/pdf_output.ui +++ b/src/calibre/gui2/convert/pdf_output.ui @@ -14,7 +14,27 @@ Form - + + QFormLayout::ExpandingFieldsGrow + + + + + <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. + + + true + + + + + + + &Override paper size set in output profile + + + + &Paper Size: @@ -24,21 +44,8 @@ - - - - - - - &Orientation: - - - opt_orientation - - - - + @@ -51,7 +58,24 @@ - + + + + + + + + &Unit: + + + opt_unit + + + + + + + @@ -60,19 +84,6 @@ - - - - Qt::Vertical - - - - 20 - 213 - - - - @@ -159,15 +170,18 @@ - - - - <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. + + + + Qt::Vertical - - true + + + 20 + 213 + - + From 52faba3c239a072d362d7723fa1d9fec20ab9757 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 24 Dec 2012 08:03:48 +0530 Subject: [PATCH 2/7] Fix #1093286 (Updated recipe for NSFW corp) --- recipes/nsfw_corp.recipe | 49 +++++++++------------------------------- 1 file changed, 11 insertions(+), 38 deletions(-) diff --git a/recipes/nsfw_corp.recipe b/recipes/nsfw_corp.recipe index c88bdd705e..0ed40ade3a 100644 --- a/recipes/nsfw_corp.recipe +++ b/recipes/nsfw_corp.recipe @@ -6,7 +6,6 @@ www.nsfwcorp.com ''' import urllib -from calibre import strftime from calibre.web.feeds.news import BasicNewsRecipe class NotSafeForWork(BasicNewsRecipe): @@ -21,8 +20,9 @@ class NotSafeForWork(BasicNewsRecipe): needs_subscription = True auto_cleanup = False INDEX = 'https://www.nsfwcorp.com' - LOGIN = INDEX + '/login' - use_embedded_content = False + LOGIN = INDEX + '/login/target/' + SETTINGS = INDEX + '/settings/' + use_embedded_content = True language = 'en' publication_type = 'magazine' masthead_url = 'http://assets.nsfwcorp.com/media/headers/nsfw_banner.jpg' @@ -46,15 +46,6 @@ class NotSafeForWork(BasicNewsRecipe): , 'language' : language } - remove_tags_before = dict(attrs={'id':'fromToLine'}) - remove_tags_after = dict(attrs={'id':'unlockButtonDiv'}) - remove_tags=[ - dict(name=['meta', 'link', 'iframe', 'embed', 'object']) - ,dict(name='a', attrs={'class':'switchToDeskNotes'}) - ,dict(attrs={'id':'unlockButtonDiv'}) - ] - remove_attributes = ['lang'] - def get_browser(self): br = BasicNewsRecipe.get_browser() br.open(self.LOGIN) @@ -65,30 +56,12 @@ class NotSafeForWork(BasicNewsRecipe): br.open(self.LOGIN, data) return br - def parse_index(self): - articles = [] - soup = self.index_to_soup(self.INDEX) - dispatches = soup.find(attrs={'id':'dispatches'}) - if dispatches: - for item in dispatches.findAll('h3'): - description = u'' - title_link = item.find('span', attrs={'class':'dispatchTitle'}) - description_link = item.find('span', attrs={'class':'dispatchSubtitle'}) - feed_link = item.find('a', href=True) - if feed_link: - url = self.INDEX + feed_link['href'] - title = self.tag_to_string(title_link) - description = self.tag_to_string(description_link) - date = strftime(self.timefmt) - articles.append({ - 'title' :title - ,'date' :date - ,'url' :url - ,'description':description - }) - return [('Dispatches', articles)] + def get_feeds(self): + self.feeds = [] + soup = self.index_to_soup(self.SETTINGS) + for item in soup.findAll('input', attrs={'type':'text'}): + if item.has_key('value') and item['value'].startswith('http://www.nsfwcorp.com/feed/'): + self.feeds.append(item['value']) + return self.feeds + return self.feeds - def preprocess_html(self, soup): - for item in soup.findAll(style=True): - del item['style'] - return soup From 4edf62a2699157ea4554936a00d26a1d523ef335 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 24 Dec 2012 08:28:36 +0530 Subject: [PATCH 3/7] Report progress in pdf output engine --- src/calibre/ebooks/conversion/plugins/pdf_output.py | 1 + src/calibre/ebooks/pdf/render/from_html.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index 2e4b370d50..972631b5bd 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -215,6 +215,7 @@ class PDFOutput(OutputFormatPlugin): def write(self, Writer, items, toc): writer = Writer(self.opts, self.log, cover_data=self.cover_data, toc=toc) + writer.report_progress = self.report_progress close = False if not hasattr(self.output_path, 'write'): diff --git a/src/calibre/ebooks/pdf/render/from_html.py b/src/calibre/ebooks/pdf/render/from_html.py index 544eafe8a4..a2f452f58c 100644 --- a/src/calibre/ebooks/pdf/render/from_html.py +++ b/src/calibre/ebooks/pdf/render/from_html.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import json +import json, os from future_builtins import map from math import floor @@ -141,23 +141,25 @@ class PDFWriter(QObject): for x in (Qt.Horizontal, Qt.Vertical): self.view.page().mainFrame().setScrollBarPolicy(x, Qt.ScrollBarAlwaysOff) + self.report_progress = lambda x, y: x 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) + xdpi, ydpi = self.view.logicalDpiX(), self.view.logicalDpiY() 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, + xdpi=xdpi, ydpi=ydpi, 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.total_items = len(items) self.first_page = True # TODO: Test margins @@ -223,6 +225,9 @@ class PDFWriter(QObject): self.logger.error('Document cannot be rendered.') 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 From 4892defbec800d17100908743d33f084ed1570c8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 24 Dec 2012 09:44:47 +0530 Subject: [PATCH 4/7] When adding empty books, allow setting of the series for the new boks. Also select the newly added book records after adding. --- src/calibre/gui2/actions/add.py | 17 +++++++++--- src/calibre/gui2/dialogs/add_empty_book.py | 30 +++++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 4f3e9fc066..e18f5fe77c 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -151,7 +151,7 @@ class AddAction(InterfaceAction): Add an empty book item to the library. This does not import any formats from a book file. ''' - author = None + author = series = None index = self.gui.library_view.currentIndex() if index.isValid(): raw = index.model().db.authors(index.row()) @@ -159,16 +159,27 @@ class AddAction(InterfaceAction): authors = [a.strip().replace('|', ',') for a in raw.split(',')] if authors: author = authors[0] - dlg = AddEmptyBookDialog(self.gui, self.gui.library_view.model().db, author) + series = index.model().db.series(index.row()) + dlg = AddEmptyBookDialog(self.gui, self.gui.library_view.model().db, + author, series) if dlg.exec_() == dlg.Accepted: num = dlg.qty_to_add + series = dlg.selected_series + db = self.gui.library_view.model().db + ids = [] for x in xrange(num): mi = MetaInformation(_('Unknown'), dlg.selected_authors) - self.gui.library_view.model().db.import_book(mi, []) + if series: + mi.series = series + mi.series_index = db.get_next_series_num_for(series) + ids.append(db.import_book(mi, [])) self.gui.library_view.model().books_added(num) if hasattr(self.gui, 'db_images'): self.gui.db_images.reset() self.gui.tags_view.recount() + if ids: + ids.reverse() + self.gui.library_view.select_rows(ids) def add_isbns(self, books, add_tags=[]): self.isbn_books = list(books) diff --git a/src/calibre/gui2/dialogs/add_empty_book.py b/src/calibre/gui2/dialogs/add_empty_book.py index 98992f85bb..185a3699e7 100644 --- a/src/calibre/gui2/dialogs/add_empty_book.py +++ b/src/calibre/gui2/dialogs/add_empty_book.py @@ -12,7 +12,7 @@ from calibre.utils.config import tweaks class AddEmptyBookDialog(QDialog): - def __init__(self, parent, db, author): + def __init__(self, parent, db, author, series=None): QDialog.__init__(self, parent) self.db = db @@ -45,6 +45,22 @@ class AddEmptyBookDialog(QDialog): self.clear_button.clicked.connect(self.reset_author) self._layout.addWidget(self.clear_button, 3, 1, 1, 1) + self.series_label = QLabel(_('Set the series of the new books to:')) + self._layout.addWidget(self.series_label, 4, 0, 1, 2) + + self.series_combo = EditWithComplete(self) + self.authors_combo.setSizeAdjustPolicy( + self.authors_combo.AdjustToMinimumContentsLengthWithIcon) + self.series_combo.setEditable(True) + self._layout.addWidget(self.series_combo, 5, 0, 1, 1) + self.initialize_series(db, series) + + self.sclear_button = QToolButton(self) + self.sclear_button.setIcon(QIcon(I('trash.png'))) + self.sclear_button.setToolTip(_('Reset series')) + self.sclear_button.clicked.connect(self.reset_series) + self._layout.addWidget(self.sclear_button, 5, 1, 1, 1) + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) @@ -54,6 +70,9 @@ class AddEmptyBookDialog(QDialog): def reset_author(self, *args): self.authors_combo.setEditText(_('Unknown')) + def reset_series(self): + self.series_combo.setEditText('') + def initialize_authors(self, db, author): au = author if not au: @@ -65,6 +84,11 @@ class AddEmptyBookDialog(QDialog): self.authors_combo.set_add_separator(tweaks['authors_completer_append_separator']) self.authors_combo.update_items_cache(db.all_author_names()) + def initialize_series(self, db, series): + self.series_combo.show_initial_value(series or '') + self.series_combo.update_items_cache(db.all_series_names()) + self.series_combo.set_separator(None) + @property def qty_to_add(self): return self.qty_spinbox.value() @@ -73,6 +97,10 @@ class AddEmptyBookDialog(QDialog): def selected_authors(self): return string_to_authors(unicode(self.authors_combo.text())) + @property + def selected_series(self): + return unicode(self.series_combo.text()) + if __name__ == '__main__': app = QApplication([]) d = AddEmptyBookDialog() From 8b7eda245e4f9e1fa0996a9338fc72f8efef63ac Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 24 Dec 2012 16:10:54 +0530 Subject: [PATCH 5/7] Update Pajamas Media --- recipes/pajama.recipe | 52 +++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/recipes/pajama.recipe b/recipes/pajama.recipe index 8c5ba74317..9b474b6e65 100644 --- a/recipes/pajama.recipe +++ b/recipes/pajama.recipe @@ -1,27 +1,27 @@ from calibre.web.feeds.news import BasicNewsRecipe -from calibre.ebooks.BeautifulSoup import BeautifulSoup class PajamasMedia(BasicNewsRecipe): title = u'Pajamas Media' description = u'Provides exclusive news and opinion for forty countries.' language = 'en' __author__ = 'Krittika Goyal' - oldest_article = 1 #days + oldest_article = 2 #days max_articles_per_feed = 25 recursions = 1 match_regexps = [r'http://pajamasmedia.com/blog/.*/2/$'] #encoding = 'latin1' remove_stylesheets = True - #remove_tags_before = dict(name='h1', attrs={'class':'heading'}) - remove_tags_after = dict(name='div', attrs={'class':'paged-nav'}) - remove_tags = [ - dict(name='iframe'), - dict(name='div', attrs={'class':['pages']}), - #dict(name='div', attrs={'id':['bookmark']}), - #dict(name='span', attrs={'class':['related_link', 'slideshowcontrols']}), - #dict(name='ul', attrs={'class':'articleTools'}), - ] + auto_cleanup = True + ##remove_tags_before = dict(name='h1', attrs={'class':'heading'}) + #remove_tags_after = dict(name='div', attrs={'class':'paged-nav'}) + #remove_tags = [ + #dict(name='iframe'), + #dict(name='div', attrs={'class':['pages']}), + ##dict(name='div', attrs={'id':['bookmark']}), + ##dict(name='span', attrs={'class':['related_link', 'slideshowcontrols']}), + ##dict(name='ul', attrs={'class':'articleTools'}), + #] feeds = [ ('pajamas Media', @@ -29,20 +29,20 @@ class PajamasMedia(BasicNewsRecipe): ] - def preprocess_html(self, soup): - story = soup.find(name='div', attrs={'id':'innerpage-content'}) - #td = heading.findParent(name='td') - #td.extract() + #def preprocess_html(self, soup): + #story = soup.find(name='div', attrs={'id':'innerpage-content'}) + ##td = heading.findParent(name='td') + ##td.extract() - soup = BeautifulSoup('t') - body = soup.find(name='body') - body.insert(0, story) - return soup + #soup = BeautifulSoup('t') + #body = soup.find(name='body') + #body.insert(0, story) + #return soup - def postprocess_html(self, soup, first): - if not first: - h = soup.find(attrs={'class':'innerpage-header'}) - if h: h.extract() - auth = soup.find(attrs={'class':'author'}) - if auth: auth.extract() - return soup + #def postprocess_html(self, soup, first): + #if not first: + #h = soup.find(attrs={'class':'innerpage-header'}) + #if h: h.extract() + #auth = soup.find(attrs={'class':'author'}) + #if auth: auth.extract() + #return soup From a34799a2847e2b13d1572bb89f63391bf60f931e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 24 Dec 2012 21:18:47 +0530 Subject: [PATCH 6/7] Fix graphics state handling --- src/calibre/ebooks/pdf/render/engine.py | 157 +++++++++------------ src/calibre/ebooks/pdf/render/from_html.py | 23 ++- 2 files changed, 79 insertions(+), 101 deletions(-) diff --git a/src/calibre/ebooks/pdf/render/engine.py b/src/calibre/ebooks/pdf/render/engine.py index ea8a42dc8f..723cff7a89 100644 --- a/src/calibre/ebooks/pdf/render/engine.py +++ b/src/calibre/ebooks/pdf/render/engine.py @@ -14,7 +14,7 @@ from functools import wraps from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter, QTransform, QPainterPath, QTextOption, QTextLayout, - QImage, QByteArray, QBuffer, qRgba, QRectF) + QImage, QByteArray, QBuffer, qRgba) from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path) from calibre.ebooks.pdf.render.common import inch, A4 @@ -40,7 +40,7 @@ class GraphicsState(object): # {{{ def __init__(self): self.ops = {} - self.current_state = self.initial_state = { + self.initial_state = { 'fill': ColorState(Color(0., 0., 0., 1.), 1.0, False), 'transform': QTransform(), 'dash': [], @@ -50,9 +50,10 @@ class GraphicsState(object): # {{{ 'line_join': 'miter', 'clip': (Qt.NoClip, QPainterPath()), } + self.current_state = self.initial_state.copy() def reset(self): - self.current_state = self.initial_state + self.current_state = self.initial_state.copy() def update_color_state(self, which, color=None, opacity=None, brush_style=None, pen_style=None): @@ -75,7 +76,6 @@ class GraphicsState(object): # {{{ self.ops[which] = n def read(self, state): - self.ops = {} flags = state.state() if flags & QPaintEngine.DirtyTransform: @@ -107,15 +107,12 @@ class GraphicsState(object): # {{{ self.update_color_state('fill', opacity=state.opacity()) self.update_color_state('stroke', opacity=state.opacity()) - if flags & QPaintEngine.DirtyClipPath: - self.ops['clip'] = (state.clipOperation(), state.clipPath()) - elif flags & QPaintEngine.DirtyClipRegion: - path = QPainterPath() - for rect in state.clipRegion().rects(): - path.addRect(QRectF(rect)) - self.ops['clip'] = (state.clipOperation(), path) + if flags & QPaintEngine.DirtyClipPath or flags & QPaintEngine.DirtyClipRegion: + self.ops['clip'] = True def __call__(self, engine): + if not self.ops: + return pdf = engine.pdf ops = self.ops current_transform = self.current_state['transform'] @@ -125,58 +122,34 @@ class GraphicsState(object): # {{{ if reset_stack: pdf.restore_stack() pdf.save_stack() - - # We apply clip before transform as the clip may have to be merged with - # the previous clip path so it is easiest to work with clips that are - # pre-transformed - prev_op, prev_clip_path = self.current_state['clip'] - if 'clip' in ops: - op, path = ops['clip'] - self.current_state['clip'] = (op, path) - transform = ops.get('transform', QTransform()) - if not transform.isIdentity() and path is not None: - # Pre transform the clip path - path = current_transform.map(path) - self.current_state['clip'] = (op, path) - - if op == Qt.ReplaceClip: - pass - elif op == Qt.IntersectClip: - if prev_op != Qt.NoClip: - self.current_state['clip'] = (op, path.intersected(prev_clip_path)) - elif op == Qt.UniteClip: - if prev_clip_path is not None: - path.addPath(prev_clip_path) - else: - self.current_state['clip'] = (Qt.NoClip, QPainterPath()) - op, path = self.current_state['clip'] - if op != Qt.NoClip: - engine.add_clip(path) - elif reset_stack and prev_op != Qt.NoClip: - # Re-apply the previous clip path since no clipping operation was - # specified - engine.add_clip(prev_clip_path) - - if reset_stack: # Since we have reset the stack we need to re-apply all previous # operations, that are different from the default value (clip is # handled separately). - for op in set(self.current_state) - (set(ops)|{'clip'}): - if self.current_state[op] != self.initial_state[op]: + for op in set(self.initial_state) - {'clip'}: + if op in ops: # These will be applied below + self.current_state[op] = self.initial_state[op] + elif self.current_state[op] != self.initial_state[op]: self.apply(op, self.current_state[op], engine, pdf) # Now apply the new operations for op, val in ops.iteritems(): - if op != 'clip': + if op != 'clip' and self.current_state[op] != val: self.apply(op, val, engine, pdf) self.current_state[op] = val + if 'clip' in ops: + # Get the current clip + path = engine.painter().clipPath() + if not path.isEmpty(): + engine.add_clip(path) + self.ops = {} + def apply(self, op, val, engine, pdf): getattr(self, 'apply_'+op)(val, engine, pdf) def apply_transform(self, val, engine, pdf): - engine.qt_system = val - pdf.transform(val) + if not val.isIdentity(): + pdf.transform(val) def apply_stroke(self, val, engine, pdf): self.apply_color_state('stroke', val, engine, pdf) @@ -235,7 +208,6 @@ class PdfEngine(QPaintEngine): self.bottom_margin) / self.pixel_height self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy) - self.qt_system = QTransform() self.do_stroke = True self.do_fill = False self.scale = sqrt(sy**2 + sx**2) @@ -250,6 +222,7 @@ class PdfEngine(QPaintEngine): i.fill(qRgba(0, 0, 0, 255)) self.alpha_bit = i.constBits().asstring(4).find(b'\xff') self.current_page_num = 1 + self.current_page_inited = False def init_page(self): self.pdf.transform(self.pdf_system) @@ -260,6 +233,7 @@ class PdfEngine(QPaintEngine): self.do_fill = False self.graphics_state.reset() self.pdf.save_stack() + self.current_page_inited = True @property def features(self): @@ -269,26 +243,26 @@ class PdfEngine(QPaintEngine): QPaintEngine.PrimitiveTransform) def begin(self, device): - try: - self.pdf = PDFStream(self.file_object, (self.page_width, - self.page_height), - compress=self.compress) - self.init_page() - except: - self.errors.append(traceback.format_exc()) - return False + if not hasattr(self, 'pdf'): + try: + self.pdf = PDFStream(self.file_object, (self.page_width, + self.page_height), + compress=self.compress) + except: + self.errors.append(traceback.format_exc()) + return False return True - 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() + def end_page(self): + if self.current_page_inited: + self.pdf.restore_stack() + self.pdf.end_page() + self.current_page_inited = False + self.current_page_num += 1 def end(self): try: - self.end_page(start_new=False) + self.end_page() self.pdf.end() except: self.errors.append(traceback.format_exc()) @@ -302,6 +276,7 @@ class PdfEngine(QPaintEngine): @store_error def drawPixmap(self, rect, pixmap, source_rect): + self.graphics_state(self) source_rect = source_rect.toRect() pixmap = (pixmap if source_rect == pixmap.rect() else pixmap.copy(source_rect)) @@ -313,6 +288,7 @@ class PdfEngine(QPaintEngine): @store_error def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor): + self.graphics_state(self) source_rect = source_rect.toRect() image = (image if source_rect == image.rect() else image.copy(source_rect)) @@ -388,7 +364,6 @@ class PdfEngine(QPaintEngine): @store_error def updateState(self, state): self.graphics_state.read(state) - self.graphics_state(self) def convert_path(self, path): p = Path() @@ -416,6 +391,7 @@ class PdfEngine(QPaintEngine): @store_error def drawPath(self, path): + self.graphics_state(self) p = self.convert_path(path) fill_rule = {Qt.OddEvenFill:'evenodd', Qt.WindingFill:'winding'}[path.fillRule()] @@ -430,6 +406,7 @@ class PdfEngine(QPaintEngine): @store_error def drawPoints(self, points): + self.graphics_state(self) p = Path() for point in points: p.move_to(point.x(), point.y()) @@ -438,6 +415,7 @@ class PdfEngine(QPaintEngine): @store_error def drawRects(self, rects): + self.graphics_state(self) for rect in rects: bl = rect.topLeft() self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(), @@ -500,7 +478,8 @@ class PdfEngine(QPaintEngine): @store_error def drawTextItem(self, point, text_item): - # super(PdfEngine, self).drawTextItem(point+QPoint(0, 0), text_item) + # super(PdfEngine, self).drawTextItem(point, text_item) + self.graphics_state(self) text = type(u'')(text_item.text()).replace('\n', ' ') text = unicodedata.normalize('NFKC', text) tl = self.get_text_layout(text_item, text) @@ -537,6 +516,7 @@ class PdfEngine(QPaintEngine): @store_error def drawPolygon(self, points, mode): + self.graphics_state(self) if not points: return p = Path() p.move_to(points[0].x(), points[0].y()) @@ -597,8 +577,8 @@ class PdfDevice(QPaintDevice): # {{{ 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 end_page(self): + self.engine.end_page() def init_page(self): self.engine.init_page() @@ -628,6 +608,7 @@ if __name__ == '__main__': with open('/tmp/painter.pdf', 'wb') as f: dev = PdfDevice(f, compress=False) p.begin(dev) + dev.init_page() xmax, ymax = p.viewport().width(), p.viewport().height() try: p.drawRect(0, 0, xmax, ymax) @@ -636,21 +617,21 @@ if __name__ == '__main__': # pp = QPainterPath() # pp.addRect(0, 0, xmax, ymax) # p.drawPath(pp) - p.save() - for i in xrange(3): - col = [0, 0, 0, 200] - col[i] = 255 - p.setOpacity(0.3) - p.setBrush(QBrush(QColor(*col))) - p.drawRect(0, 0, xmax/10, xmax/10) - p.translate(xmax/10, xmax/10) - p.scale(1, 1.5) - p.restore() + # p.save() + # for i in xrange(3): + # col = [0, 0, 0, 200] + # col[i] = 255 + # p.setOpacity(0.3) + # p.setBrush(QBrush(QColor(*col))) + # 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, 2048, 2048, QPixmap(I('library.png'))) - p.drawRect(0, 0, 2048, 2048) + # # p.scale(2, 2) + # # p.rotate(45) + # p.drawPixmap(0, 0, 2048, 2048, QPixmap(I('library.png'))) + # p.drawRect(0, 0, 2048, 2048) # p.save() # p.drawLine(0, 0, 5000, 0) @@ -658,18 +639,18 @@ if __name__ == '__main__': # p.drawLine(0, 0, 5000, 0) # p.restore() - # f = p.font() - # f.setPointSize(24) + 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) + f.setFamily('DejaVu Sans') + p.setFont(f) # p.setPen(QColor(0, 0, 255)) # p.scale(2, 2) # p.rotate(45) - # p.drawText(QPoint(100, 300), 'Some text ū --- Д AV ff ff') + p.drawText(QPoint(0, 300), 'Some—text not By’s ū --- Д AV ff ff') finally: p.end() if dev.engine.errors_occurred: diff --git a/src/calibre/ebooks/pdf/render/from_html.py b/src/calibre/ebooks/pdf/render/from_html.py index a2f452f58c..cdc5a96c61 100644 --- a/src/calibre/ebooks/pdf/render/from_html.py +++ b/src/calibre/ebooks/pdf/render/from_html.py @@ -160,7 +160,6 @@ class PDFWriter(QObject): self.page.setViewportSize(QSize(self.doc.width(), self.doc.height())) self.render_queue = items self.total_items = len(items) - self.first_page = True # TODO: Test margins mt, mb = map(self.doc.to_px, (opts.margin_top, opts.margin_bottom)) @@ -260,20 +259,18 @@ class PDFWriter(QObject): mf = self.view.page().mainFrame() start_page = self.current_page_num + dx = 0 while True: - if not self.first_page: - self.doc.init_page() - self.first_page = False + self.doc.init_page() 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() + mf.render(self.painter) + self.painter.restore() + nsl = evaljs('paged_display.next_screen_location()').toInt() + self.doc.end_page() + if not nsl[1] or nsl[0] <= 0: + break + dx = nsl[0] + evaljs('window.scrollTo(%d, 0)'%dx) if self.doc.errors_occurred: break From c1fa9da1cf8888c1a7b20e3b2667cf74bf1a3e63 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 24 Dec 2012 21:26:08 +0530 Subject: [PATCH 7/7] Fix multiple 'All column' coloring rules not being applied --- src/calibre/gui2/library/models.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 063149c62e..8cd84bdafc 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -52,10 +52,9 @@ class ColumnColor(object): self.mi = None def __call__(self, id_, key, fmt, db, formatter, color_cache, colors): - if id_ in color_cache: - if key in color_cache[id_]: - self.mi = None - return color_cache[id_][key] + if id_ in color_cache and key in color_cache[id_]: + self.mi = None + return color_cache[id_][key] try: if self.mi is None: self.mi = db.get_metadata(id_, index_is_id=True) @@ -763,9 +762,8 @@ class BooksModel(QAbstractTableModel): # {{{ self.column_color.mi = None if self.color_row_fmt_cache is None: - d = dict(self.db.prefs['column_color_rules']) - self.color_row_fmt_cache = d.get(color_row_key, '') - + self.color_row_fmt_cache = tuple(fmt for key, fmt in + self.db.prefs['column_color_rules'] if key == color_row_key) for k, fmt in self.db.prefs['column_color_rules']: if k == key: @@ -789,10 +787,9 @@ class BooksModel(QAbstractTableModel): # {{{ except: pass - if self.color_row_fmt_cache: - key = color_row_key - ccol = self.column_color(id_, key, self.color_row_fmt_cache, - self.db, self.formatter, self.color_cache, self.colors) + for fmt in self.color_row_fmt_cache: + ccol = self.column_color(id_, color_row_key, fmt, self.db, + self.formatter, self.color_cache, self.colors) if ccol is not None: return ccol