Fix graphics state handling

This commit is contained in:
Kovid Goyal 2012-12-24 21:18:47 +05:30
parent 8b7eda245e
commit a34799a284
2 changed files with 79 additions and 101 deletions

View File

@ -14,7 +14,7 @@ from functools import wraps
from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter, from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter,
QTransform, QPainterPath, QTextOption, QTextLayout, 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.serialize import (Color, PDFStream, Path)
from calibre.ebooks.pdf.render.common import inch, A4 from calibre.ebooks.pdf.render.common import inch, A4
@ -40,7 +40,7 @@ class GraphicsState(object): # {{{
def __init__(self): def __init__(self):
self.ops = {} self.ops = {}
self.current_state = self.initial_state = { self.initial_state = {
'fill': ColorState(Color(0., 0., 0., 1.), 1.0, False), 'fill': ColorState(Color(0., 0., 0., 1.), 1.0, False),
'transform': QTransform(), 'transform': QTransform(),
'dash': [], 'dash': [],
@ -50,9 +50,10 @@ class GraphicsState(object): # {{{
'line_join': 'miter', 'line_join': 'miter',
'clip': (Qt.NoClip, QPainterPath()), 'clip': (Qt.NoClip, QPainterPath()),
} }
self.current_state = self.initial_state.copy()
def reset(self): 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, def update_color_state(self, which, color=None, opacity=None,
brush_style=None, pen_style=None): brush_style=None, pen_style=None):
@ -75,7 +76,6 @@ class GraphicsState(object): # {{{
self.ops[which] = n self.ops[which] = n
def read(self, state): def read(self, state):
self.ops = {}
flags = state.state() flags = state.state()
if flags & QPaintEngine.DirtyTransform: if flags & QPaintEngine.DirtyTransform:
@ -107,15 +107,12 @@ class GraphicsState(object): # {{{
self.update_color_state('fill', opacity=state.opacity()) self.update_color_state('fill', opacity=state.opacity())
self.update_color_state('stroke', opacity=state.opacity()) self.update_color_state('stroke', opacity=state.opacity())
if flags & QPaintEngine.DirtyClipPath: if flags & QPaintEngine.DirtyClipPath or flags & QPaintEngine.DirtyClipRegion:
self.ops['clip'] = (state.clipOperation(), state.clipPath()) self.ops['clip'] = True
elif flags & QPaintEngine.DirtyClipRegion:
path = QPainterPath()
for rect in state.clipRegion().rects():
path.addRect(QRectF(rect))
self.ops['clip'] = (state.clipOperation(), path)
def __call__(self, engine): def __call__(self, engine):
if not self.ops:
return
pdf = engine.pdf pdf = engine.pdf
ops = self.ops ops = self.ops
current_transform = self.current_state['transform'] current_transform = self.current_state['transform']
@ -125,58 +122,34 @@ class GraphicsState(object): # {{{
if reset_stack: if reset_stack:
pdf.restore_stack() pdf.restore_stack()
pdf.save_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 # Since we have reset the stack we need to re-apply all previous
# operations, that are different from the default value (clip is # operations, that are different from the default value (clip is
# handled separately). # handled separately).
for op in set(self.current_state) - (set(ops)|{'clip'}): for op in set(self.initial_state) - {'clip'}:
if self.current_state[op] != self.initial_state[op]: 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) self.apply(op, self.current_state[op], engine, pdf)
# Now apply the new operations # Now apply the new operations
for op, val in ops.iteritems(): 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.apply(op, val, engine, pdf)
self.current_state[op] = val 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): def apply(self, op, val, engine, pdf):
getattr(self, 'apply_'+op)(val, engine, pdf) getattr(self, 'apply_'+op)(val, engine, pdf)
def apply_transform(self, val, engine, pdf): def apply_transform(self, val, engine, pdf):
engine.qt_system = val if not val.isIdentity():
pdf.transform(val) pdf.transform(val)
def apply_stroke(self, val, engine, pdf): def apply_stroke(self, val, engine, pdf):
self.apply_color_state('stroke', 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.bottom_margin) / self.pixel_height
self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy) self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy)
self.qt_system = QTransform()
self.do_stroke = True self.do_stroke = True
self.do_fill = False self.do_fill = False
self.scale = sqrt(sy**2 + sx**2) self.scale = sqrt(sy**2 + sx**2)
@ -250,6 +222,7 @@ class PdfEngine(QPaintEngine):
i.fill(qRgba(0, 0, 0, 255)) i.fill(qRgba(0, 0, 0, 255))
self.alpha_bit = i.constBits().asstring(4).find(b'\xff') self.alpha_bit = i.constBits().asstring(4).find(b'\xff')
self.current_page_num = 1 self.current_page_num = 1
self.current_page_inited = False
def init_page(self): def init_page(self):
self.pdf.transform(self.pdf_system) self.pdf.transform(self.pdf_system)
@ -260,6 +233,7 @@ class PdfEngine(QPaintEngine):
self.do_fill = False self.do_fill = False
self.graphics_state.reset() self.graphics_state.reset()
self.pdf.save_stack() self.pdf.save_stack()
self.current_page_inited = True
@property @property
def features(self): def features(self):
@ -269,26 +243,26 @@ class PdfEngine(QPaintEngine):
QPaintEngine.PrimitiveTransform) QPaintEngine.PrimitiveTransform)
def begin(self, device): def begin(self, device):
try: if not hasattr(self, 'pdf'):
self.pdf = PDFStream(self.file_object, (self.page_width, try:
self.page_height), self.pdf = PDFStream(self.file_object, (self.page_width,
compress=self.compress) self.page_height),
self.init_page() compress=self.compress)
except: except:
self.errors.append(traceback.format_exc()) self.errors.append(traceback.format_exc())
return False return False
return True return True
def end_page(self, start_new=True): def end_page(self):
self.pdf.restore_stack() if self.current_page_inited:
self.pdf.end_page() self.pdf.restore_stack()
self.current_page_num += 1 self.pdf.end_page()
if start_new: self.current_page_inited = False
self.init_page() self.current_page_num += 1
def end(self): def end(self):
try: try:
self.end_page(start_new=False) self.end_page()
self.pdf.end() self.pdf.end()
except: except:
self.errors.append(traceback.format_exc()) self.errors.append(traceback.format_exc())
@ -302,6 +276,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPixmap(self, rect, pixmap, source_rect): def drawPixmap(self, rect, pixmap, source_rect):
self.graphics_state(self)
source_rect = source_rect.toRect() source_rect = source_rect.toRect()
pixmap = (pixmap if source_rect == pixmap.rect() else pixmap = (pixmap if source_rect == pixmap.rect() else
pixmap.copy(source_rect)) pixmap.copy(source_rect))
@ -313,6 +288,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor): def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor):
self.graphics_state(self)
source_rect = source_rect.toRect() source_rect = source_rect.toRect()
image = (image if source_rect == image.rect() else image = (image if source_rect == image.rect() else
image.copy(source_rect)) image.copy(source_rect))
@ -388,7 +364,6 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def updateState(self, state): def updateState(self, state):
self.graphics_state.read(state) self.graphics_state.read(state)
self.graphics_state(self)
def convert_path(self, path): def convert_path(self, path):
p = Path() p = Path()
@ -416,6 +391,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPath(self, path): def drawPath(self, path):
self.graphics_state(self)
p = self.convert_path(path) p = self.convert_path(path)
fill_rule = {Qt.OddEvenFill:'evenodd', fill_rule = {Qt.OddEvenFill:'evenodd',
Qt.WindingFill:'winding'}[path.fillRule()] Qt.WindingFill:'winding'}[path.fillRule()]
@ -430,6 +406,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPoints(self, points): def drawPoints(self, points):
self.graphics_state(self)
p = Path() p = Path()
for point in points: for point in points:
p.move_to(point.x(), point.y()) p.move_to(point.x(), point.y())
@ -438,6 +415,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawRects(self, rects): def drawRects(self, rects):
self.graphics_state(self)
for rect in rects: for rect in rects:
bl = rect.topLeft() bl = rect.topLeft()
self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(), self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(),
@ -500,7 +478,8 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawTextItem(self, point, text_item): 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 = type(u'')(text_item.text()).replace('\n', ' ')
text = unicodedata.normalize('NFKC', text) text = unicodedata.normalize('NFKC', text)
tl = self.get_text_layout(text_item, text) tl = self.get_text_layout(text_item, text)
@ -537,6 +516,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPolygon(self, points, mode): def drawPolygon(self, points, mode):
self.graphics_state(self)
if not points: return if not points: return
p = Path() p = Path()
p.move_to(points[0].x(), points[0].y()) 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 int(round(self.body_height * self.ydpi / 72.0))
return 0 return 0
def end_page(self, start_new=True): def end_page(self):
self.engine.end_page(start_new=start_new) self.engine.end_page()
def init_page(self): def init_page(self):
self.engine.init_page() self.engine.init_page()
@ -628,6 +608,7 @@ if __name__ == '__main__':
with open('/tmp/painter.pdf', 'wb') as f: with open('/tmp/painter.pdf', 'wb') as f:
dev = PdfDevice(f, compress=False) dev = PdfDevice(f, compress=False)
p.begin(dev) p.begin(dev)
dev.init_page()
xmax, ymax = p.viewport().width(), p.viewport().height() xmax, ymax = p.viewport().width(), p.viewport().height()
try: try:
p.drawRect(0, 0, xmax, ymax) p.drawRect(0, 0, xmax, ymax)
@ -636,21 +617,21 @@ if __name__ == '__main__':
# pp = QPainterPath() # pp = QPainterPath()
# pp.addRect(0, 0, xmax, ymax) # pp.addRect(0, 0, xmax, ymax)
# p.drawPath(pp) # p.drawPath(pp)
p.save() # p.save()
for i in xrange(3): # for i in xrange(3):
col = [0, 0, 0, 200] # col = [0, 0, 0, 200]
col[i] = 255 # col[i] = 255
p.setOpacity(0.3) # p.setOpacity(0.3)
p.setBrush(QBrush(QColor(*col))) # p.setBrush(QBrush(QColor(*col)))
p.drawRect(0, 0, xmax/10, xmax/10) # p.drawRect(0, 0, xmax/10, xmax/10)
p.translate(xmax/10, xmax/10) # p.translate(xmax/10, xmax/10)
p.scale(1, 1.5) # p.scale(1, 1.5)
p.restore() # p.restore()
# p.scale(2, 2) # # p.scale(2, 2)
# p.rotate(45) # # p.rotate(45)
p.drawPixmap(0, 0, 2048, 2048, QPixmap(I('library.png'))) # p.drawPixmap(0, 0, 2048, 2048, QPixmap(I('library.png')))
p.drawRect(0, 0, 2048, 2048) # p.drawRect(0, 0, 2048, 2048)
# p.save() # p.save()
# p.drawLine(0, 0, 5000, 0) # p.drawLine(0, 0, 5000, 0)
@ -658,18 +639,18 @@ if __name__ == '__main__':
# p.drawLine(0, 0, 5000, 0) # p.drawLine(0, 0, 5000, 0)
# p.restore() # p.restore()
# f = p.font() f = p.font()
# f.setPointSize(24) f.setPointSize(20)
# f.setLetterSpacing(f.PercentageSpacing, 200) # f.setLetterSpacing(f.PercentageSpacing, 200)
# f.setUnderline(True) # f.setUnderline(True)
# f.setOverline(True) # f.setOverline(True)
# f.setStrikeOut(True) # f.setStrikeOut(True)
# f.setFamily('Calibri') f.setFamily('DejaVu Sans')
# p.setFont(f) p.setFont(f)
# p.setPen(QColor(0, 0, 255)) # p.setPen(QColor(0, 0, 255))
# p.scale(2, 2) # p.scale(2, 2)
# p.rotate(45) # p.rotate(45)
# p.drawText(QPoint(100, 300), 'Some text ū --- Д AV ff ff') p.drawText(QPoint(0, 300), 'Some—text not Bys ū --- Д AV ff ff')
finally: finally:
p.end() p.end()
if dev.engine.errors_occurred: if dev.engine.errors_occurred:

View File

@ -160,7 +160,6 @@ class PDFWriter(QObject):
self.page.setViewportSize(QSize(self.doc.width(), self.doc.height())) self.page.setViewportSize(QSize(self.doc.width(), self.doc.height()))
self.render_queue = items self.render_queue = items
self.total_items = len(items) self.total_items = len(items)
self.first_page = True
# TODO: Test margins # TODO: Test margins
mt, mb = map(self.doc.to_px, (opts.margin_top, opts.margin_bottom)) 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() mf = self.view.page().mainFrame()
start_page = self.current_page_num start_page = self.current_page_num
dx = 0
while True: while True:
if not self.first_page: self.doc.init_page()
self.doc.init_page()
self.first_page = False
self.painter.save() self.painter.save()
try: mf.render(self.painter)
mf.render(self.painter) self.painter.restore()
nsl = evaljs('paged_display.next_screen_location()').toInt() nsl = evaljs('paged_display.next_screen_location()').toInt()
if not nsl[1] or nsl[0] <= 0: self.doc.end_page()
break if not nsl[1] or nsl[0] <= 0:
evaljs('window.scrollTo(%d, 0)'%nsl[0]) break
self.doc.end_page() dx = nsl[0]
finally: evaljs('window.scrollTo(%d, 0)'%dx)
self.painter.restore()
if self.doc.errors_occurred: if self.doc.errors_occurred:
break break