Start work on replacement for Qt's pdf driver

This commit is contained in:
Kovid Goyal 2012-12-13 17:41:49 +05:30
parent a3d6b83046
commit 968e9de4ef

View File

@ -0,0 +1,394 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, traceback
from math import sqrt
from collections import namedtuple
from future_builtins import map
from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter,
QTransform, QPoint, QPainterPath)
from reportlab.lib.units import inch
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen.canvas import FILL_NON_ZERO, FILL_EVEN_ODD, Canvas
from reportlab.lib.colors import Color
from calibre.constants import DEBUG
XDPI = 1200
YDPI = 1200
Point = namedtuple('Point', 'x y')
def set_transform(transform, func):
func(transform.m11(), transform.m12(), transform.m21(), transform.m22(), transform.dx(), transform.dy())
class GraphicsState(object):
def __init__(self, state=None):
self.ops = {}
if state is not None:
self.read_state(state)
@property
def stack_reset_needed(self):
return 'transform' in self.ops
def read_state(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()
alpha = color.alphaF()
if alpha == 1.0: alpha = None
self.ops['do_fill'] = 0 if (alpha == 0.0 or brush.style() == Qt.NoBrush) else 1
self.ops['fill_color'] = Color(color.red(), color.green(), color.blue(),
alpha=alpha)
if flags & QPaintEngine.DirtyPen:
pen = state.pen()
brush = pen.brush()
color = pen.color()
alpha = color.alphaF()
if alpha == 1.0: alpha = None
self.ops['do_stroke'] = 0 if (pen.style() == Qt.NoPen or brush.style() ==
Qt.NoBrush or alpha == 0.0) else 1
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['stroke_color'] = Color(color.red(), color.green(),
color.blue(), alpha=alpha)
self.ops['line_cap'] = {Qt.FlatCap:0, Qt.RoundCap:1,
Qt.SquareCap:2}.get(pen.capStyle(), 0)
self.ops['line_join'] = {Qt.MiterJoin:0, Qt.RoundJoin:1,
Qt.BevelJoin:2}.get(pen.joinStyle(), 0)
# TODO: Handle clipping
def __call__(self, engine):
canvas = engine.canvas
ops = self.ops
if self.stack_reset_needed:
canvas.restoreState()
canvas.saveState()
# Since we have reset the stack we need to re-apply all previous
# operations
ops = engine.graphics_state.ops.copy()
ops.update(self.ops)
self.ops = ops
# Apply operations
if 'transform' in ops:
engine.qt_system = ops['transform']
set_transform(ops['transform'], canvas.transform)
if 'fill_color' in ops:
canvas.setFillColor(ops['fill_color'])
if 'stroke_color' in ops:
canvas.setStrokeColor(ops['stroke_color'])
for x in ('fill', 'stroke'):
x = 'do_'+x
if x in ops:
setattr(canvas, x, ops[x])
if 'dash' in ops:
canvas.setDash(ops['dash'])
if 'line_width' in ops:
canvas.setLineWidth(ops['line_width'])
if 'line_cap' in ops:
canvas.setLineCap(ops['line_cap'])
if 'line_join' in ops:
canvas.setLineJoin(ops['line_join'])
if not self.stack_reset_needed:
# Add the operations from the previous state object that were not
# updated in this state object. This is needed to allow stack
# resetting to work.
ops = canvas.graphics_state.ops.copy()
ops.update(self.ops)
self.ops = ops
return self
class PdfEngine(QPaintEngine):
def __init__(self, file_object, page_width, page_height, left_margin,
top_margin, right_margin, bottom_margin, width, height):
QPaintEngine.__init__(self, self.features)
self.file_object = file_object
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
self.pixel_width, self.pixel_height = width, height
# Setup a co-ordinate transform that allows us to use co-ords
# from Qt's pixel based co-ordinate system with its origin at the top
# left corner. PDF's co-ordinate system is based on pts and has its
# origin in the bottom left corner. We also have to implement the page
# margins. Therefore, we need to translate, scale and reflect about the
# x-axis.
dy = self.page_height - self.top_margin
dx = self.left_margin
sx = (self.page_width - self.left_margin -
self.right_margin) / self.pixel_width
sy = (self.page_height - self.top_margin -
self.bottom_margin) / self.pixel_height
self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy)
self.qt_system = QTransform()
self.do_stroke = 1
self.do_fill = 0
self.scale = sqrt(sy**2 + sx**2)
self.yscale = sy
self.graphics_state = GraphicsState()
def init_page(self):
set_transform(self.pdf_system, self.canvas.transform)
self.canvas.saveState()
@property
def features(self):
# TODO: Remove unsupported features from this
return QPaintEngine.AllFeatures
def begin(self, device):
try:
self.canvas = Canvas(self.file_object,
pageCompression=0 if DEBUG else 1,
pagesize=(self.page_width, self.page_height))
self.init_page()
except:
traceback.print_exc()
return False
return True
def end_page(self, start_new=True):
self.canvas.restoreState()
self.canvas.showPage()
if start_new:
self.init_page()
def end(self):
try:
self.end_page(start_new=False)
self.canvas.save()
except:
traceback.print_exc()
return False
finally:
self.canvas = self.file_object = None
return True
def type(self):
return QPaintEngine.User
def drawPixmap(self, rect, pixmap, source_rect):
pass # TODO: Implement me
def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor):
pass # TODO: Implement me
def updateState(self, state):
state = GraphicsState(state)
self.graphics_state = state(self)
def drawPath(self, path):
p = self.canvas.beginPath()
path = path.simplified()
i = 0
while i < path.elementCount():
elem = path.elementAt(i)
em = (elem.x, elem.y)
i += 1
if elem.isMoveTo():
p.moveTo(*em)
elif elem.isLineTo():
p.lineTo(*em)
elif elem.isCurveTo():
if path.elementCount() > i+1:
c1, c2 = map(lambda j:(
path.elementAt(j).x, path.elementAt(j)), (i, i+1))
i += 2
p.curveTo(*(c1 + c2 + em))
with self:
self.canvas._fillMode = {Qt.OddEvenFill:FILL_EVEN_ODD,
Qt.WindingFill:FILL_NON_ZERO}[path.fillRule()]
self.canvas.drawPath(p, stroke=self.do_stroke,
fill=self.do_fill)
def drawPoints(self, points):
for point in points:
point = self.current_transform.map(point)
self.canvas.circle(point.x(), point.y(), 0.1,
stroke=self.do_stroke, fill=self.do_fill)
def drawRects(self, rects):
for rect in rects:
bl = rect.topLeft()
self.canvas.rect(bl.x(), bl.y(), rect.width(), rect.height(),
stroke=self.do_stroke, fill=self.do_fill)
def drawTextItem(self, point, text_item):
# TODO: Add support for underline, overline, strike through and fonts
# super(PdfEngine, self).drawTextItem(point, text_item)
f = text_item.font()
px, pt = f.pixelSize(), f.pointSizeF()
if px == -1:
sz = pt/self.yscale
else:
sz = px
q = self.qt_system
if not q.isIdentity() and q.type() > q.TxShear:
# We cant map this transform to a PDF text transform operator
f, s = self.do_fill, self.do_stroke
self.do_fill, self.do_stroke = 1, 0
super(PdfEngine, self).drawTextItem(point, text_item)
self.do_fill, self.do_stroke = f, s
return
to = self.canvas.beginText()
set_transform(QTransform(1, 0, 0, -1, point.x(), point.y()), to.setTextTransform)
fontname = 'Times-Roman'
to.setFont(fontname, sz) # TODO: Embed font
stretch = f.stretch()
if stretch != 100:
to.setHorizontalScale(stretch)
ws = f.wordSpacing()
if ws != 0:
to.setWordSpacing(self.map_dx(ws))
spacing = f.letterSpacing()
st = f.letterSpacingType()
if st == f.AbsoluteSpacing and spacing != 0:
to.setCharSpace(spacing)
# TODO: Handle percentage letter spacing
text = type(u'')(text_item.text())
to.textOut(text)
# TODO: handle colors
self.canvas.drawText(to)
def draw_line(kind='underline'):
tw = self.canvas.stringWidth(text, fontname, sz)
p = self.canvas.beginPath()
if kind == 'underline':
dy = -text_item.descent()
elif kind == 'overline':
dy = text_item.ascent()
elif kind == 'strikeout':
dy = text_item.ascent()/2
p.moveTo(point.x, point.y+dy)
p.lineTo(point.x+tw, point.y+dy)
if f.underline():
draw_line()
if f.overline():
draw_line('overline')
if f.strikeOut():
draw_line('strikeout')
def drawPolygon(self, points, mode):
points = [Point(p.x(), p.y()) for p in points]
p = self.canvas.beginPath()
p.moveTo(*points[0])
for point in points[1:]:
p.lineTo(*point)
p.close()
with self:
self.canvas._fillMode = {self.OddEvenMode:FILL_EVEN_ODD,
self.WindingMode:FILL_NON_ZERO}.get(mode,
FILL_EVEN_ODD)
self.canvas.drawPath(p, fill=(mode in (self.OddEvenMode,
self.WindingMode, self.ConvexMode)))
def __enter__(self):
self.canvas.saveState()
def __exit__(self, *args):
self.canvas.restoreState()
class PdfDevice(QPaintDevice): # {{{
def __init__(self, file_object, page_size=A4, left_margin=inch,
top_margin=inch, right_margin=inch, bottom_margin=inch):
QPaintDevice.__init__(self)
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())
def paintEngine(self):
return self.engine
def metric(self, m):
if m in (self.PdmDpiX, self.PdmPhysicalDpiX):
return XDPI
if m in (self.PdmDpiY, self.PdmPhysicalDpiY):
return YDPI
if m == self.PdmDepth:
return 32
if m == self.PdmNumColors:
return sys.maxint
if m == self.PdmWidthMM:
return int(round(self.body_width * 0.35277777777778))
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))
if m == self.PdmHeight:
return int(round(self.body_height * YDPI / 72.0))
return 0
# }}}
if __name__ == '__main__':
QPainterPath, QPoint
app = QApplication([])
p = QPainter()
with open('/tmp/painter.pdf', 'wb') as f:
dev = PdfDevice(f)
p.begin(dev)
xmax, ymax = p.viewport().width(), p.viewport().height()
try:
p.drawRect(0, 0, xmax, ymax)
p.drawPolyline(QPoint(0, 0), QPoint(xmax, 0), QPoint(xmax, ymax),
QPoint(0, ymax), QPoint(0, 0))
pp = QPainterPath()
pp.addRect(0, 0, xmax, ymax)
p.drawPath(pp)
p.save()
for i in xrange(3):
p.drawRect(0, 0, xmax/10, xmax/10)
p.translate(xmax/10, xmax/10)
p.scale(1, 1.5)
p.restore()
p.save()
p.drawLine(0, 0, 5000, 0)
p.rotate(45)
p.drawLine(0, 0, 5000, 0)
p.restore()
f = p.font()
f.setPointSize(24)
f.setFamily('Times New Roman')
p.setFont(f)
# p.scale(2, 2)
p.rotate(45)
p.drawText(QPoint(100, 300), 'Some text')
finally:
p.end()