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 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 @@ def __init__(self):
'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 @@ def update_color_state(self, which, color=None, opacity=None,
self.ops[which] = n
def read(self, state):
self.ops = {}
flags = state.state()
if flags & QPaintEngine.DirtyTransform:
@ -107,15 +107,12 @@ def read(self, state):
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 @@ def __call__(self, engine):
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 @@ def __init__(self, file_object, page_width, page_height, left_margin,
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 @@ def __init__(self, file_object, page_width, page_height, left_margin,
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 @@ def init_page(self):
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 @@ def features(self):
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 @@ def type(self):
@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 @@ def drawPixmap(self, rect, pixmap, source_rect):
@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 @@ def add_image(self, img, cache_key):
@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 @@ def convert_path(self, path):
@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 @@ def add_clip(self, path):
@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 @@ def drawPoints(self, points):
@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 @@ def get_glyphs(string):
@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 @@ def drawTextItem(self, point, text_item):
@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 @@ def metric(self, m):
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 @@ def set_metadata(self, *args, **kwargs):
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 @@ def set_metadata(self, *args, **kwargs):
# 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 @@ def set_metadata(self, *args, **kwargs):
# 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 Bys ū --- Д AV ff ff')
finally:
p.end()
if dev.engine.errors_occurred:

View file

@ -160,7 +160,6 @@ def dump(self, items, out_stream, pdf_metadata):
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 @@ def do_paged_render(self):
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