diff --git a/src/calibre/ebooks/pdf/render/engine.py b/src/calibre/ebooks/pdf/render/engine.py index 4bdf38e123..aa1fa17cc3 100644 --- a/src/calibre/ebooks/pdf/render/engine.py +++ b/src/calibre/ebooks/pdf/render/engine.py @@ -14,12 +14,13 @@ from future_builtins import map import sip from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter, - QTransform, QPainterPath, QImage, QByteArray, QBuffer, + QTransform, QImage, QByteArray, QBuffer, qRgba) from calibre.constants import plugins -from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path) +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 from calibre.utils.fonts.sfnt.metrics import FontMetrics @@ -42,146 +43,6 @@ def store_error(func): return errh -class GraphicsState(object): # {{{ - - def __init__(self): - self.ops = {} - self.initial_state = { - 'fill': ColorState(Color(0., 0., 0., 1.), 1.0, False), - 'transform': QTransform(), - 'dash': [], - 'line_width': 0, - 'stroke': ColorState(Color(0., 0., 0., 1.), 1.0, True), - 'line_cap': 'flat', - 'line_join': 'miter', - 'clip': (Qt.NoClip, QPainterPath()), - } - self.current_state = self.initial_state.copy() - - def reset(self): - self.current_state = self.initial_state.copy() - - def update_color_state(self, which, color=None, opacity=None, - brush_style=None, pen_style=None): - current = self.ops.get(which, self.current_state[which]) - n = ColorState(*current) - if color is not None: - n = n._replace(color=Color(*color.getRgbF())) - if opacity is not None: - n = n._replace(opacity=opacity) - if opacity is not None: - opacity *= n.color.opacity - if brush_style is not None: - if which == 'fill': - do = (False if opacity == 0.0 or brush_style == Qt.NoBrush else - True) - else: - do = (False if opacity == 0.0 or brush_style == Qt.NoBrush or - pen_style == Qt.NoPen else True) - n = n._replace(do=do) - self.ops[which] = n - - def read(self, state): - flags = state.state() - - if flags & QPaintEngine.DirtyTransform: - self.ops['transform'] = state.transform() - - # TODO: Add support for brush patterns - if flags & QPaintEngine.DirtyBrush: - brush = state.brush() - color = brush.color() - self.update_color_state('fill', color=color, - brush_style=brush.style()) - - if flags & QPaintEngine.DirtyPen: - pen = state.pen() - brush = pen.brush() - color = pen.color() - self.update_color_state('stroke', color, brush_style=brush.style(), - pen_style=pen.style()) - ps = {Qt.DashLine:[3], Qt.DotLine:[1,2], Qt.DashDotLine:[3,2,1,2], - Qt.DashDotDotLine:[3, 2, 1, 2, 1, 2]}.get(pen.style(), []) - self.ops['dash'] = ps - self.ops['line_width'] = pen.widthF() - self.ops['line_cap'] = {Qt.FlatCap:'flat', Qt.RoundCap:'round', - Qt.SquareCap:'square'}.get(pen.capStyle(), 'flat') - self.ops['line_join'] = {Qt.MiterJoin:'miter', Qt.RoundJoin:'round', - Qt.BevelJoin:'bevel'}.get(pen.joinStyle(), 'miter') - - if flags & QPaintEngine.DirtyOpacity: - self.update_color_state('fill', opacity=state.opacity()) - self.update_color_state('stroke', opacity=state.opacity()) - - 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'] - transform_changed = 'transform' in ops and ops['transform'] != current_transform - reset_stack = transform_changed or 'clip' in ops - - if reset_stack: - pdf.restore_stack() - pdf.save_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.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' 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): - if not val.isIdentity(): - pdf.transform(val) - - def apply_stroke(self, val, engine, pdf): - self.apply_color_state('stroke', val, engine, pdf) - - def apply_fill(self, val, engine, pdf): - self.apply_color_state('fill', val, engine, pdf) - - def apply_color_state(self, which, val, engine, pdf): - color = val.color._replace(opacity=val.opacity*val.color.opacity) - getattr(pdf, 'set_%s_color'%which)(color) - setattr(engine, 'do_%s'%which, val.do) - - def apply_dash(self, val, engine, pdf): - pdf.set_dash(val) - - def apply_line_width(self, val, engine, pdf): - pdf.set_line_width(val) - - def apply_line_cap(self, val, engine, pdf): - pdf.set_line_cap(val) - - def apply_line_join(self, val, engine, pdf): - pdf.set_line_join(val) - -# }}} - class Font(FontMetrics): def __init__(self, sfnt): @@ -215,9 +76,7 @@ class PdfEngine(QPaintEngine): self.bottom_margin) / self.pixel_height self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy) - self.do_stroke = True - self.do_fill = False - self.graphics_state = GraphicsState() + self.graphics = Graphics() self.errors_occurred = False self.errors, self.debug = errors, debug self.fonts = {} @@ -230,14 +89,21 @@ class PdfEngine(QPaintEngine): if err: raise RuntimeError('Failed to load qt_hack with err: %s'%err) + def apply_graphics_state(self): + self.graphics(self.pdf, self.pdf_system, self.painter()) + + @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): self.pdf.transform(self.pdf_system) self.pdf.set_rgb_colorspace() - width = self.painter().pen().widthF() if self.isActive() else 0 - self.pdf.set_line_width(width) - self.do_stroke = True - self.do_fill = False - self.graphics_state.reset() + self.graphics.reset() self.pdf.save_stack() self.current_page_inited = True @@ -287,7 +153,7 @@ class PdfEngine(QPaintEngine): @store_error def drawPixmap(self, rect, pixmap, source_rect): - self.graphics_state(self) + self.apply_graphics_state() source_rect = source_rect.toRect() pixmap = (pixmap if source_rect == pixmap.rect() else pixmap.copy(source_rect)) @@ -299,7 +165,7 @@ class PdfEngine(QPaintEngine): @store_error def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor): - self.graphics_state(self) + self.apply_graphics_state() source_rect = source_rect.toRect() image = (image if source_rect == image.rect() else image.copy(source_rect)) @@ -374,50 +240,20 @@ class PdfEngine(QPaintEngine): @store_error def updateState(self, state): - self.graphics_state.read(state) - - def convert_path(self, path): - p = Path() - i = 0 - while i < path.elementCount(): - elem = path.elementAt(i) - em = (elem.x, elem.y) - i += 1 - if elem.isMoveTo(): - p.move_to(*em) - elif elem.isLineTo(): - p.line_to(*em) - elif elem.isCurveTo(): - added = False - if path.elementCount() > i+1: - c1, c2 = path.elementAt(i), path.elementAt(i+1) - if (c1.type == path.CurveToDataElement and c2.type == - path.CurveToDataElement): - i += 2 - p.curve_to(em[0], em[1], c1.x, c1.y, c2.x, c2.y) - added = True - if not added: - raise ValueError('Invalid curve to operation') - return p + self.graphics.update_state(state, self.painter()) @store_error def drawPath(self, path): - self.graphics_state(self) - p = self.convert_path(path) + 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) - def add_clip(self, path): - p = self.convert_path(path) - fill_rule = {Qt.OddEvenFill:'evenodd', - Qt.WindingFill:'winding'}[path.fillRule()] - self.pdf.add_clip(p, fill_rule=fill_rule) - @store_error def drawPoints(self, points): - self.graphics_state(self) + self.apply_graphics_state() p = Path() for point in points: p.move_to(point.x(), point.y()) @@ -426,7 +262,7 @@ class PdfEngine(QPaintEngine): @store_error def drawRects(self, rects): - self.graphics_state(self) + self.apply_graphics_state() for rect in rects: bl = rect.topLeft() self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(), @@ -446,7 +282,7 @@ class PdfEngine(QPaintEngine): @store_error def drawTextItem(self, point, text_item): # super(PdfEngine, self).drawTextItem(point, text_item) - self.graphics_state(self) + self.apply_graphics_state() gi = self.qt_hack.get_glyphs(point, text_item) if not gi.indices: sip.delete(gi) @@ -477,7 +313,7 @@ class PdfEngine(QPaintEngine): @store_error def drawPolygon(self, points, mode): - self.graphics_state(self) + self.apply_graphics_state() if not points: return p = Path() p.move_to(points[0].x(), points[0].y()) @@ -510,14 +346,6 @@ class PdfEngine(QPaintEngine): link.append((llx, lly, urx, ury)) self.pdf.links.add(current_item, start_page, links, anchors) - def __enter__(self): - self.pdf.save_stack() - self.saved_ps = (self.do_stroke, self.do_fill) - - def __exit__(self, *args): - self.do_stroke, self.do_fill = self.saved_ps - self.pdf.restore_stack() - class PdfDevice(QPaintDevice): # {{{ @@ -584,8 +412,8 @@ class PdfDevice(QPaintDevice): # {{{ # }}} if __name__ == '__main__': - from PyQt4.Qt import (QBrush, QColor, QPoint, QPixmap) - QBrush, QColor, QPoint, QPixmap + from PyQt4.Qt import (QBrush, QColor, QPoint, QPixmap, QPainterPath) + QBrush, QColor, QPoint, QPixmap, QPainterPath app = QApplication([]) p = QPainter() with open('/t/painter.pdf', 'wb') as f: @@ -593,6 +421,7 @@ if __name__ == '__main__': p.begin(dev) dev.init_page() xmax, ymax = p.viewport().width(), p.viewport().height() + b = p.brush() try: p.drawRect(0, 0, xmax, ymax) # p.drawPolyline(QPoint(0, 0), QPoint(xmax, 0), QPoint(xmax, ymax), @@ -600,27 +429,22 @@ 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.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, 2048, 2048, QPixmap(I('library.png'))) - # p.drawRect(0, 0, 2048, 2048) - - # p.save() - # p.drawLine(0, 0, 5000, 0) + # p.scale(2, 2) # p.rotate(45) - # p.drawLine(0, 0, 5000, 0) - # p.restore() + p.drawPixmap(0, 0, 2048, 2048, QPixmap(I('library.png'))) + p.drawRect(0, 0, 2048, 2048) f = p.font() f.setPointSize(20) diff --git a/src/calibre/ebooks/pdf/render/graphics.py b/src/calibre/ebooks/pdf/render/graphics.py new file mode 100644 index 0000000000..68efb2514a --- /dev/null +++ b/src/calibre/ebooks/pdf/render/graphics.py @@ -0,0 +1,196 @@ +#!/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' + +from math import sqrt + +from PyQt4.Qt import (QBrush, QPen, Qt, QPointF, QTransform, QPainterPath, + QPaintEngine) + +from calibre.ebooks.pdf.render.common import Array +from calibre.ebooks.pdf.render.serialize import Path, Color + +def convert_path(path): + p = Path() + i = 0 + while i < path.elementCount(): + elem = path.elementAt(i) + em = (elem.x, elem.y) + i += 1 + if elem.isMoveTo(): + p.move_to(*em) + elif elem.isLineTo(): + p.line_to(*em) + elif elem.isCurveTo(): + added = False + if path.elementCount() > i+1: + c1, c2 = path.elementAt(i), path.elementAt(i+1) + if (c1.type == path.CurveToDataElement and c2.type == + path.CurveToDataElement): + i += 2 + p.curve_to(em[0], em[1], c1.x, c1.y, c2.x, c2.y) + added = True + if not added: + raise ValueError('Invalid curve to operation') + return p + + +class GraphicsState(object): + + FIELDS = ('fill', 'stroke', 'opacity', 'transform', 'brush_origin', + 'clip', 'do_fill', 'do_stroke') + + def __init__(self): + self.fill = QBrush() + self.stroke = QPen() + self.opacity = 1.0 + self.transform = QTransform() + self.brush_origin = QPointF() + self.clip = QPainterPath() + self.do_fill = False + self.do_stroke = True + + def __eq__(self, other): + for x in self.FIELDS: + if getattr(other, x) != getattr(self, x): + return False + return True + + def copy(self): + ans = GraphicsState() + ans.fill = QBrush(self.fill) + ans.stroke = QPen(self.stroke) + ans.opacity = self.opacity + ans.transform = self.transform * QTransform() + ans.brush_origin = QPointF(self.brush_origin) + ans.clip = self.clip + ans.do_fill, ans.do_stroke = self.do_fill, self.do_stroke + return ans + +class Graphics(object): + + def __init__(self): + self.base_state = GraphicsState() + self.current_state = GraphicsState() + self.pending_state = None + + def update_state(self, state, painter): + flags = state.state() + if self.pending_state is None: + self.pending_state = self.current_state.copy() + + s = self.pending_state + + if flags & QPaintEngine.DirtyTransform: + s.transform = state.transform() + + if flags & QPaintEngine.DirtyBrushOrigin: + s.brush_origin = state.brushOrigin() + + if flags & QPaintEngine.DirtyBrush: + s.fill = state.brush() + + if flags & QPaintEngine.DirtyPen: + s.stroke = state.pen() + + if flags & QPaintEngine.DirtyOpacity: + s.opacity = state.opacity() + + if flags & QPaintEngine.DirtyClipPath or flags & QPaintEngine.DirtyClipRegion: + s.clip = painter.clipPath() + + def reset(self): + self.current_state = GraphicsState() + self.pending_state = None + + def __call__(self, pdf, pdf_system, painter): + # Apply the currently pending state to the PDF + if self.pending_state is None: + return + + pdf_state = self.current_state + ps = self.pending_state + + if (ps.transform != pdf_state.transform or ps.clip != pdf_state.clip): + pdf.restore_stack() + pdf.save_stack() + pdf_state = self.base_state + + if (pdf_state.transform != ps.transform): + pdf.transform(ps.transform) + + if (pdf_state.opacity != ps.opacity or pdf_state.stroke != ps.stroke): + self.apply_stroke(ps, pdf, pdf_system, painter) + + if (pdf_state.opacity != ps.opacity or pdf_state.fill != ps.fill or + pdf_state.brush_origin != ps.brush_origin): + self.apply_fill(ps, pdf, pdf_system, painter) + + if (pdf_state.clip != ps.clip): + p = convert_path(ps.clip) + fill_rule = {Qt.OddEvenFill:'evenodd', + Qt.WindingFill:'winding'}[ps.clip.fillRule()] + pdf.add_clip(p, fill_rule=fill_rule) + + self.current_state = self.pending_state + self.pending_state = None + + def apply_stroke(self, state, pdf, pdf_system, painter): + # TODO: Handle pens with non solid brushes by setting the colorspace + # for stroking to a pattern + # TODO: Support miter limit by using QPainterPathStroker + pen = state.stroke + self.pending_state.do_stroke = True + if pen.style() == Qt.NoPen: + self.pending_state.do_stroke = False + + # Width + w = pen.widthF() + if pen.isCosmetic(): + t = painter.transform() + w /= sqrt(t.m11()**2 + t.m22()**2) + pdf.serialize(w) + pdf.current_page.write(' w ') + + # Line cap + cap = {Qt.FlatCap:0, Qt.RoundCap:1, Qt.SquareCap: + 2}.get(pen.capStyle(), 0) + pdf.current_page.write('%d J '%cap) + + # Line join + join = {Qt.MiterJoin:0, Qt.RoundJoin:1, + Qt.BevelJoin:2}.get(pen.joinStyle(), 0) + pdf.current_page.write('%d j '%join) + + # Dash pattern + ps = {Qt.DashLine:[3], Qt.DotLine:[1,2], Qt.DashDotLine:[3,2,1,2], + Qt.DashDotDotLine:[3, 2, 1, 2, 1, 2]}.get(pen.style(), []) + if ps: + pdf.serialize(Array(ps)) + pdf.current_page.write(' d ') + + # Stroke fill + b = pen.brush() + vals = list(b.color().getRgbF()) + vals[-1] *= state.opacity + color = Color(*vals) + pdf.set_stroke_color(color) + + if vals[-1] < 1e-5 or b.style() == Qt.NoBrush: + self.pending_state.do_stroke = False + + def apply_fill(self, state, pdf, pdf_system, painter): + self.pending_state.do_fill = True + b = state.fill + if b.style() == Qt.NoBrush: + self.pending_state.do_fill = False + vals = list(b.color().getRgbF()) + vals[-1] *= state.opacity + color = Color(*vals) + pdf.set_fill_color(color) + diff --git a/src/calibre/ebooks/pdf/render/serialize.py b/src/calibre/ebooks/pdf/render/serialize.py index 908c4ff919..be78ddda66 100644 --- a/src/calibre/ebooks/pdf/render/serialize.py +++ b/src/calibre/ebooks/pdf/render/serialize.py @@ -369,25 +369,8 @@ class PDFStream(object): op = 'W' if fill_rule == 'winding' else 'W*' self.current_page.write_line(op + ' ' + 'n') - def set_dash(self, array, phase=0): - array = Array(array) - serialize(array, self.current_page) - self.current_page.write(b' ') - serialize(phase, self.current_page) - self.current_page.write_line(' d') - - def set_line_width(self, width): - serialize(width, self.current_page) - self.current_page.write_line(' w') - - def set_line_cap(self, style): - serialize({'flat':0, 'round':1, 'square':2}.get(style), - self.current_page) - self.current_page.write_line(' J') - - def set_line_join(self, style): - serialize({'miter':0, 'round':1, 'bevel':2}[style], self.current_page) - self.current_page.write_line(' j') + def serialize(self, o): + serialize(o, self.current_page) def set_stroke_color(self, color): opacity = color.opacity