dcgraphics.py, bezier.py and vec2d.py no longer needed for rendering to printer DC

wx.GraphicsContext, Cairo.GraphicsContext and MuPDF page-Pixmap can now be
created directly in a printer DC in pdfviewer.py (unlike originally in 2009).
Cacheing of page bitmaps now removed - jerky scrolling no longer observed.
SetZoom modified to stay on the same page after scale changed.
This commit is contained in:
David Hughes
2016-09-06 12:48:30 +01:00
parent 21ee7043d0
commit 04a03c716a
4 changed files with 64 additions and 1015 deletions

View File

@@ -1,71 +0,0 @@
# Name: bezier.py
# Package: wx.lib.pdfviewer
#
# Purpose: Compute Bezier curves for PDF rendered using wx.DC
# Adapted from the original source code, see below.
#
# Author: David Hughes dfh@forestfield.co.uk
# Copyright: Forestfield Software Ltd
# Licence: Public domain
# History: Created 17 Jun 2009
#
# Tags: phoenix-port, documented
#
#----------------------------------------------------------------------------
"""
This module is used to compute Bezier curves for PDF rendering.
"""
import wx
from .vec2d import *
def calculate_bezier(p, steps = 30):
"""
Calculate a bezier curve from 4 control points and return a
list of the resulting points.
Depends on the 2d vector class from http://www.pygame.org/wiki/2DVectorClass
2007 Victor Blomqvist
Released to the Public Domain
The function uses the forward differencing algorithm described at
http://www.niksula.cs.hut.fi/~hkankaan/Homepages/bezierfast.html
"""
t = 1.0 / steps
temp = t*t
f = p[0]
fd = 3 * (p[1] - p[0]) * t
fdd_per_2 = 3 * (p[0] - 2 * p[1] + p[2]) * temp
fddd_per_2 = 3 * (3 * (p[1] - p[2]) + p[3] - p[0]) * temp * t
fddd = fddd_per_2 + fddd_per_2
fdd = fdd_per_2 + fdd_per_2
fddd_per_6 = fddd_per_2 * (1.0 / 3)
points = []
for x in range(steps):
points.append(f)
f = f + fd + fdd_per_2 + fddd_per_6
fd = fd + fdd + fddd_per_2
fdd = fdd + fddd
fdd_per_2 = fdd_per_2 + fddd_per_2
points.append(f)
return points
def compute_points(controlpoints, nsteps=30):
"""
Input 4 control points as :class:`wx.RealPoint` and convert to vec2d instances.
compute the nsteps points on the resulting curve and return them
as a list of :class:`wx.Point`
"""
controlvectors = []
for p in controlpoints:
controlvectors.append(vec2d(p.x, p.y))
pointvectors = calculate_bezier(controlvectors, nsteps)
curvepoints = []
for v in pointvectors:
curvepoints.append(wx.Point(v[0], v[1]))
return curvepoints

View File

@@ -1,474 +0,0 @@
# Name: dcgraphics.py
# Package: wx.lib.pdfviewer
#
# Purpose: A wx.GraphicsContext-like API implemented using wx.DC
# based on wx.lib.graphics by Robin Dunn
#
# Author: David Hughes dfh@forestfield.co.uk
# Copyright: Forestfield Software Ltd
# Licence: Same as wxPython host
# History: 8 Aug 2009 - created
# 12 Dec 2011 - amended DrawText
#
# Tags: phoenix-port, unittest, documented
#
#----------------------------------------------------------------------------
"""
This module implements an API similar to :class:`wx.GraphicsContext` and the
related classes. The implementation is done using :class:`wx.DC`
Why do this? Neither :class:`wx.GraphicsContext` nor the Cairo-based
GraphicsContext API provided by wx.lib.graphics can be written
directly to a :class:`wx.PrinterDC`. It can be done via an intermediate bitmap in
a :class:`wx.MemoryDC` but transferring this to a :class:`wx.PrinterDC` is an order of
magnitude slower than writing directly.
Why not just use :class:`wx.PrinterDC` directly? There may be times when you do want
to use :class:`wx.GraphicsContext` for its displayed appearance and for its
clean(er) API, so being able to use the same code for printing as well is nice.
It started out with the intention of being a full implementation of the
GraphicsContext API but so far only contains the sub-set required to render PDF.
It also contains the co-ordinate twiddles for the PDF origin, which would need
to be separated out if this was ever developed to be more general purpose.
"""
import copy
from math import asin, pi
from . import bezier
import wx
class dcGraphicsState:
"""
Each instance holds the current graphics state. It can be
saved (pushed) and restored (popped) by the owning parent.
"""
def __init__ (self):
"""
Default constructor, creates an instance with default values.
"""
self.Yoffset = 0.0
self.Xtrans = 0.0
self.Ytrans = 0.0
self.Xscale = 1.0
self.Yscale = 1.0
self.sinA = 0.0
self.cosA = 1.0
self.tanAlpha = 0.0
self.tanBeta = 0.0
self.rotDegrees = 0
def Ytop(self, y):
"""
Return y co-ordinate wrt top of current page
:param integer `y`: y co-ordinate **TBW** (?)
"""
return self.Yoffset + y
def Translate(self, dx, dy):
"""
Move the origin from the current point to (dx, dy).
:param `dx`: x co-ordinate to move to **TBW** (?)
:param `dy`: y co-ordinate to move to **TBW** (?)
"""
self.Xtrans += (dx * self.Xscale)
self.Ytrans += (dy * self.Yscale)
def Scale(self, sx, sy):
"""
Scale the current co-ordinates.
:param `sx`: x co-ordinate to scale to **TBW** (?)
:param `sy`: y co-ordinate to scale to **TBW** (?)
"""
self.Xscale *= sx
self.Yscale *= sy
def Rotate(self, cosA, sinA):
"""
Compute the (text only) rotation angle
sinA is inverted to cancel the original inversion in
pdfviewer.drawfile that was introduced because of the difference
in y direction between pdf and GraphicsContext co-ordinates.
:param `cosA`: **TBW** (?)
:param `sinA`: **TBW** (?)
"""
self.cosA = cosA
self.sinA = sinA
self.rotDegrees += asin(-self.sinA) * 180 / pi
def Skew(self, tanAlpha, tanBeta):
"""
**TBW** (?)
:param `tanAlpha`: **TBW** (?)
:param `tanBeta`: **TBW** (?)
"""
self.tanAlpha = tanAlpha
self.tanBeta = tanBeta
def Get_x(self, x=0, y=0):
"""
Return x co-ordinate using graphic states and transforms
:param `x`: current x co-ordinats
:param `y`: current y co-ordinats
"""
return ((x*self.cosA*self.Xscale - y*self.sinA*self.Yscale) + self.Xtrans)
def Get_y(self, x=0, y=0):
"""
Return y co-ordinate using graphic states and transforms
:param `x`: current x co-ordinats
:param `y`: current y co-ordinats
"""
return self.Ytop((x*self.sinA*self.Xscale + y*self.cosA*self.Yscale) + self.Ytrans)
def Get_angle(self):
"""
Return rotation angle in degrees.
"""
return self.rotDegrees
#----------------------------------------------------------------------------
class dcGraphicsContext(object):
def __init__(self, context=None, yoffset=0, have_cairo=False):
"""
The incoming co-ordinates have a bottom left origin with increasing
y downwards (so y values are all negative). The DC origin is top left
also with increasing y down.
:class:`wx.DC` and :class:`wx.GraphicsContext` fonts are too big in the ratio
of pixels per inch to points per inch. If screen rendering used Cairo,
printed fonts need to be scaled but if :class:`wx.GCDC` was used, they are
already scaled.
:param `context`: **TBW** (?)
:param integer `yoffset`: informs us of the page height
:param boolean `have_cairo`: is Cairo used
"""
self._context = context
self.gstate = dcGraphicsState()
self.saved_state = []
self.gstate.Yoffset = yoffset
self.fontscale = 1.0
if have_cairo and wx.PlatformInfo[1] == 'wxMSW':
self.fontscale = 72.0 / 96.0
@staticmethod
def Create(dc, yoffset, have_cairo):
"""
The created dcGraphicsContext instance uses the dc itself.
"""
assert isinstance(dc, wx.DC)
return dcGraphicsContext(dc, yoffset, have_cairo)
def CreateMatrix(self, a=1.0, b=0, c=0, d=1.0, tx=0, ty=0):
"""
Create a new matrix object.
"""
m = dcGraphicsMatrix()
m.Set(a, b, c, d, tx, ty)
return m
def CreatePath(self):
"""
Create a new path obejct.
"""
return dcGraphicsPath(parent=self)
def PushState(self):
"""
Makes a copy of the current state of the context and saves it
on an internal stack of saved states. The saved state will be
restored when PopState is called.
"""
self.saved_state.append(copy.deepcopy(self.gstate))
def PopState(self):
"""
Restore the most recently saved state which was saved with PushState.
"""
self.gstate = self.saved_state.pop()
def Scale(self, xScale, yScale):
"""
Sets the dc userscale factor.
:param `xScale`: **TBW** (?)
:param `yScale`: **TBW** (?)
"""
self._context.SetUserScale(xScale, yScale)
def ConcatTransform(self, matrix):
"""
Modifies the current transformation matrix by applying matrix
as an additional transformation.
"""
g = self.gstate
a, b, c, d, e, f = map(float, matrix.Get())
g.Translate(e, f)
if d == a and c == -b and b != 0:
g.Rotate(a, b)
else:
g.Scale(a, d)
g.Skew(b,c)
def SetPen(self, pen):
"""
Set the :class:`wx.Pen` to be used for stroking lines in future drawing
operations.
:param `pen`: the :class:`wx.Pen` to be used from now on.
"""
self._context.SetPen(pen)
def SetBrush(self, brush):
"""
Set the :class:`wx.Brush` to be used for filling shapes in future drawing
operations.
:param `brush`: the :class:`wx.Brush` to be used from now on.
"""
self._context.SetBrush(brush)
def SetFont(self, font, colour=None):
"""
Sets the :class:`wx.Font` to be used for drawing text.
Don't set the dc font yet as it may need to be scaled
:param `font`: the :class:`wx.Font` for drawing text
:param `colour`: the colour to be used
"""
self._font = font
if colour is not None:
self._context.SetTextForeground(colour)
def StrokePath(self, path):
"""
Strokes the path (draws the lines) using the current pen.
:param `path`: path to draw line on
"""
raise NotImplementedError("TODO")
def FillPath(self, path, fillStyle=wx.ODDEVEN_RULE):
"""
Fills the path using the current brush.
:param `path`: path to draw line on
:param `fillStyle`: the fill style to use
"""
raise NotImplementedError("TODO")
def DrawPath(self, path, fillStyle=wx.ODDEVEN_RULE):
"""
Draws the path using current pen and brush.
:param `path`: path to draw line on
:param `fillStyle`: the fill style to use
"""
pathdict = {'SetPen': self._context.SetPen,
'DrawLine': self._context.DrawLine,
'DrawRectangle': self._context.DrawRectangle,
'DrawSpline': self._context.DrawSpline}
for pathcmd, args, kwargs in path.commands:
pathdict[pathcmd](*args, **kwargs)
if path.allpoints:
self._context.DrawPolygon(path.allpoints, 0, 0, fillStyle)
def DrawText(self, text, x, y, backgroundBrush=None):
"""
Set the dc font at the required size.
Ensure original font is not altered
Draw the text at (x, y) using the current font.
:param `text`: the text to draw
:param `x`: x co-ordinates for text
:param `y`: y co-ordinates for text
:param `backgroundBrush`: curently ignored
"""
g = self.gstate
orgsize = self._font.GetPointSize()
newsize = orgsize * (g.Xscale * self.fontscale)
self._font.SetPointSize(newsize)
self._context.SetFont(self._font)
self._context.DrawRotatedText(text, g.Get_x(x, y), g.Get_y(x, y), g.Get_angle())
self._font.SetPointSize(orgsize)
def DrawBitmap(self, bmp, x, y, w=-1, h=-1):
"""
Draw the bitmap
:param `bmp`: the bitmap to draw
:param `x`: the x co-ordinate for the bitmap
:param `y`: the y co-ordinate for the bitmap
:param `w`: currently ignored
:param `h`: currently ignored
"""
g = self.gstate
self._context.DrawBitmap(bmp, g.Get_x(x, y), g.Get_y(x, y))
#---------------------------------------------------------------------------
class dcGraphicsMatrix(object):
"""
A matrix holds an affine transformations, such as a scale,
rotation, shear, or a combination of these, and is used to convert
between different coordinante spaces.
"""
def __init__(self):
"""
The default constructor
"""
self._matrix = ()
def Set(self, a=1.0, b=0.0, c=0.0, d=1.0, tx=0.0, ty=0.0):
"""
Set the componenets of the matrix by value, default values
are the identity matrix.
:param `a`: **TBW** (?)
:param `b`: **TBW** (?)
:param `c`: **TBW** (?)
:param `d`: **TBW** (?)
:param `tx`: **TBW** (?)
:param `ty`: **TBW** (?)
"""
self._matrix = (a, b, c, d, tx, ty)
def Get(self):
"""
Return the component values of the matrix as a tuple.
"""
return tuple(self._matrix)
#---------------------------------------------------------------------------
class dcGraphicsPath(object):
"""
A GraphicsPath is a representaion of a geometric path, essentially
a collection of lines and curves. Paths can be used to define
areas to be stroked and filled on a GraphicsContext.
"""
def __init__(self, parent=None):
"""
A path is essentially an object that we use just for
collecting path moves, lines, and curves in order to apply
them to the real context using DrawPath.
"""
self.commands = []
self.allpoints = []
if parent:
self.gstate = parent.gstate
self.fillcolour = parent._context.GetBrush().GetColour()
self.isfilled = parent._context.GetBrush().GetStyle() != wx.BRUSHSTYLE_TRANSPARENT
def AddCurveToPoint(self, cx1, cy1, cx2, cy2, x, y):
"""
Adds a cubic Bezier curve from the current point, using two
control points and an end point.
:param `cx1`: **TBW** (?)
:param `cy1`: **TBW** (?)
:param `cx2`: **TBW** (?)
:param `cy2`: **TBW** (?)
:param `x`: **TBW** (?)
:param `y`: **TBW** (?)
"""
g = self.gstate
clist = []
clist.append(wx.RealPoint(self.xc, self.yc))
clist.append(wx.RealPoint(g.Get_x(cx1, cy1), g.Get_y(cx1, cy1)))
clist.append(wx.RealPoint(g.Get_x(cx2, cy2), g.Get_y(cx2, cy2)))
clist.append(wx.RealPoint(g.Get_x(x, y), g.Get_y(x, y)))
self.xc, self.yc = clist[-1]
plist = bezier.compute_points(clist, 64)
if self.isfilled:
self.allpoints.extend(plist)
else:
self.commands.append(['DrawSpline', (plist,), {}])
def AddLineToPoint(self, x, y):
"""
Adds a straight line from the current point to (x, y)
:param `x`: **TBW** (?)
:param `y`: **TBW** (?)
"""
x2 = self.gstate.Get_x(x, y)
y2 = self.gstate.Get_y(x, y)
if self.isfilled:
self.allpoints.extend([wx.Point(self.xc, self.yc), wx.Point(x2, y2)])
else:
self.commands.append(['DrawLine', (self.xc, self.yc, x2, y2), {}])
self.xc = x2
self.yc = y2
def AddRectangle(self, x, y, w, h):
"""
Adds a new rectangle as a closed sub-path.
:param `x`: **TBW** (?)
:param `y`: **TBW** (?)
:param `w`: **TBW** (?)
:param `h`: **TBW** (?)
"""
g = self.gstate
xr = g.Get_x(x, y)
yr = g.Get_y(x, y)
wr = w*g.Xscale*g.cosA - h*g.Yscale*g.sinA
hr = w*g.Xscale*g.sinA + h*g.Yscale*g.cosA
if round(wr) == 1 or round(hr) == 1: # draw thin rectangles as lines
self.commands.append(['SetPen', (wx.Pen(self.fillcolour, 1.0),), {}])
self.commands.append(['DrawLine', (xr, yr, xr+wr-1, yr+hr), {}])
else:
self.commands.append(['DrawRectangle', (xr, yr, wr, hr), {}])
def CloseSubpath(self):
"""
Adds a line segment to the path from the current point to the
beginning of the current sub-path, and closes this sub-path.
"""
if self.isfilled:
self.allpoints.extend([wx.Point(self.xc, self.yc), wx.Point(self.x0, self.y0)])
def MoveToPoint(self, x, y):
"""
Begins a new sub-path at (x,y) by moving the "current point" there.
"""
self.x0 = self.xc = self.gstate.Get_x(x, y)
self.y0 = self.yc = self.gstate.Get_y(x, y)

View File

@@ -1,352 +0,0 @@
# Name: vec2d.py
# Package: wx.lib.pdfviewer
#
# Purpose: 2D vector class. Used for computing Bezier curves
#
# Author:
# Copyright:
# Licence: LGPL - from http://www.pygame.org/wiki/2DVectorClass
# History: Created 17 Jun 2009
#
# Tags: phoenix-port, documented
#----------------------------------------------------------------------------
"""
This module is used to compute Bezier curves.
"""
import operator
import math
class vec2d(object):
"""
2d vector class, supports vector and scalar operators,
and also provides a bunch of high level functions.
"""
__slots__ = ['x', 'y']
def __init__(self, x_or_pair, y = None):
if y == None:
self.x = x_or_pair[0]
self.y = x_or_pair[1]
else:
self.x = x_or_pair
self.y = y
def __len__(self):
return 2
def __getitem__(self, key):
if key == 0:
return self.x
elif key == 1:
return self.y
else:
raise IndexError("Invalid subscript "+str(key)+" to vec2d")
def __setitem__(self, key, value):
if key == 0:
self.x = value
elif key == 1:
self.y = value
else:
raise IndexError("Invalid subscript "+str(key)+" to vec2d")
# String representaion (for debugging)
def __repr__(self):
return 'vec2d(%s, %s)' % (self.x, self.y)
# Comparison
def __eq__(self, other):
if hasattr(other, "__getitem__") and len(other) == 2:
return self.x == other[0] and self.y == other[1]
else:
return False
def __ne__(self, other):
if hasattr(other, "__getitem__") and len(other) == 2:
return self.x != other[0] or self.y != other[1]
else:
return True
def __nonzero__(self):
return bool(self.x or self.y)
# Generic operator handlers
def _o2(self, other, f):
"""
Any two-operator operation where the left operand is a vec2d.
"""
if isinstance(other, vec2d):
return vec2d(f(self.x, other.x),
f(self.y, other.y))
elif (hasattr(other, "__getitem__")):
return vec2d(f(self.x, other[0]),
f(self.y, other[1]))
else:
return vec2d(f(self.x, other),
f(self.y, other))
def _r_o2(self, other, f):
"""
Any two-operator operation where the right operand is a vec2d.
"""
if (hasattr(other, "__getitem__")):
return vec2d(f(other[0], self.x),
f(other[1], self.y))
else:
return vec2d(f(other, self.x),
f(other, self.y))
def _io(self, other, f):
"""
inplace operator
"""
if (hasattr(other, "__getitem__")):
self.x = f(self.x, other[0])
self.y = f(self.y, other[1])
else:
self.x = f(self.x, other)
self.y = f(self.y, other)
return self
# Addition
def __add__(self, other):
if isinstance(other, vec2d):
return vec2d(self.x + other.x, self.y + other.y)
elif hasattr(other, "__getitem__"):
return vec2d(self.x + other[0], self.y + other[1])
else:
return vec2d(self.x + other, self.y + other)
__radd__ = __add__
def __iadd__(self, other):
if isinstance(other, vec2d):
self.x += other.x
self.y += other.y
elif hasattr(other, "__getitem__"):
self.x += other[0]
self.y += other[1]
else:
self.x += other
self.y += other
return self
# Subtraction
def __sub__(self, other):
if isinstance(other, vec2d):
return vec2d(self.x - other.x, self.y - other.y)
elif (hasattr(other, "__getitem__")):
return vec2d(self.x - other[0], self.y - other[1])
else:
return vec2d(self.x - other, self.y - other)
def __rsub__(self, other):
if isinstance(other, vec2d):
return vec2d(other.x - self.x, other.y - self.y)
if (hasattr(other, "__getitem__")):
return vec2d(other[0] - self.x, other[1] - self.y)
else:
return vec2d(other - self.x, other - self.y)
def __isub__(self, other):
if isinstance(other, vec2d):
self.x -= other.x
self.y -= other.y
elif (hasattr(other, "__getitem__")):
self.x -= other[0]
self.y -= other[1]
else:
self.x -= other
self.y -= other
return self
# Multiplication
def __mul__(self, other):
if isinstance(other, vec2d):
return vec2d(self.x*other.x, self.y*other.y)
if (hasattr(other, "__getitem__")):
return vec2d(self.x*other[0], self.y*other[1])
else:
return vec2d(self.x*other, self.y*other)
__rmul__ = __mul__
def __imul__(self, other):
if isinstance(other, vec2d):
self.x *= other.x
self.y *= other.y
elif (hasattr(other, "__getitem__")):
self.x *= other[0]
self.y *= other[1]
else:
self.x *= other
self.y *= other
return self
# Division
def __div__(self, other):
return self._o2(other, operator.div)
def __rdiv__(self, other):
return self._r_o2(other, operator.div)
def __idiv__(self, other):
return self._io(other, operator.div)
def __floordiv__(self, other):
return self._o2(other, operator.floordiv)
def __rfloordiv__(self, other):
return self._r_o2(other, operator.floordiv)
def __ifloordiv__(self, other):
return self._io(other, operator.floordiv)
def __truediv__(self, other):
return self._o2(other, operator.truediv)
def __rtruediv__(self, other):
return self._r_o2(other, operator.truediv)
def __itruediv__(self, other):
return self._io(other, operator.floordiv)
# Modulo
def __mod__(self, other):
return self._o2(other, operator.mod)
def __rmod__(self, other):
return self._r_o2(other, operator.mod)
def __divmod__(self, other):
return self._o2(other, operator.divmod)
def __rdivmod__(self, other):
return self._r_o2(other, operator.divmod)
# Exponentation
def __pow__(self, other):
return self._o2(other, operator.pow)
def __rpow__(self, other):
return self._r_o2(other, operator.pow)
# Bitwise operators
def __lshift__(self, other):
return self._o2(other, operator.lshift)
def __rlshift__(self, other):
return self._r_o2(other, operator.lshift)
def __rshift__(self, other):
return self._o2(other, operator.rshift)
def __rrshift__(self, other):
return self._r_o2(other, operator.rshift)
def __and__(self, other):
return self._o2(other, operator.and_)
__rand__ = __and__
def __or__(self, other):
return self._o2(other, operator.or_)
__ror__ = __or__
def __xor__(self, other):
return self._o2(other, operator.xor)
__rxor__ = __xor__
# Unary operations
def __neg__(self):
return vec2d(operator.neg(self.x), operator.neg(self.y))
def __pos__(self):
return vec2d(operator.pos(self.x), operator.pos(self.y))
def __abs__(self):
return vec2d(abs(self.x), abs(self.y))
def __invert__(self):
return vec2d(-self.x, -self.y)
# vectory functions
def get_length_sqrd(self):
return self.x**2 + self.y**2
def get_length(self):
return math.sqrt(self.x**2 + self.y**2)
def __setlength(self, value):
length = self.get_length()
self.x *= value/length
self.y *= value/length
length = property(get_length, __setlength, None, "gets or sets the magnitude of the vector")
def rotate(self, angle_degrees):
radians = math.radians(angle_degrees)
cos = math.cos(radians)
sin = math.sin(radians)
x = self.x*cos - self.y*sin
y = self.x*sin + self.y*cos
self.x = x
self.y = y
def rotated(self, angle_degrees):
radians = math.radians(angle_degrees)
cos = math.cos(radians)
sin = math.sin(radians)
x = self.x*cos - self.y*sin
y = self.x*sin + self.y*cos
return vec2d(x, y)
def get_angle(self):
if (self.get_length_sqrd() == 0):
return 0
return math.degrees(math.atan2(self.y, self.x))
def __setangle(self, angle_degrees):
self.x = self.length
self.y = 0
self.rotate(angle_degrees)
angle = property(get_angle, __setangle, None, "gets or sets the angle of a vector")
def get_angle_between(self, other):
cross = self.x*other[1] - self.y*other[0]
dot = self.x*other[0] + self.y*other[1]
return math.degrees(math.atan2(cross, dot))
def normalized(self):
length = self.length
if length != 0:
return self/length
return vec2d(self)
def normalize_return_length(self):
length = self.length
if length != 0:
self.x /= length
self.y /= length
return length
def perpendicular(self):
return vec2d(-self.y, self.x)
def perpendicular_normal(self):
length = self.length
if length != 0:
return vec2d(-self.y/length, self.x/length)
return vec2d(self)
def dot(self, other):
return float(self.x*other[0] + self.y*other[1])
def get_distance(self, other):
return math.sqrt((self.x - other[0])**2 + (self.y - other[1])**2)
def get_dist_sqrd(self, other):
return (self.x - other[0])**2 + (self.y - other[1])**2
def projection(self, other):
other_length_sqrd = other[0]*other[0] + other[1]*other[1]
projected_length_times_other_length = self.dot(other)
return other*(projected_length_times_other_length/other_length_sqrd)
def cross(self, other):
return self.x*other[1] - self.y*other[0]
def interpolate_to(self, other, arange):
return vec2d(self.x + (other[0] - self.x)*arange, self.y + (other[1] - self.y)*arange)
def convert_to_basis(self, x_vector, y_vector):
return vec2d(self.dot(x_vector)/x_vector.get_length_sqrd(),
self.dot(y_vector)/y_vector.get_length_sqrd())
def __getstate__(self):
return [self.x, self.y]
def __setstate__(self, adict):
self.x, self.y = adict

View File

@@ -34,10 +34,7 @@ from six import BytesIO, string_types
import wx
CACHE_LATE_PAGES = False # TODO Cacheing of page bitmaps does not currently
# work when using cairocffi and Python 3.4 (MSW)
LATE_THRESHOLD = 200 # Time to render (ttr), milliseconds
VERBOSE = False
VERBOSE = True
try:
# see http://pythonhosted.org/PyMuPDF - documentation & installation
@@ -59,7 +56,7 @@ except ImportError:
GraphicsContext = wx.GraphicsContext
have_cairo = False
if not mupdf:
if not mupdf:
try:
import wx.lib.wxcairo as wxcairo
import cairo
@@ -69,8 +66,6 @@ if not mupdf:
except ImportError:
if VERBOSE: print('pdfviewer using wx.GraphicsContext')
from .dcgraphics import dcGraphicsContext
# New PageObject method added by Forestfield Software
def extractOperators(self):
"""
@@ -130,7 +125,6 @@ class pdfViewer(wx.ScrolledWindow):
self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) # recommended in wxWidgets docs
self.buttonpanel = None # reference to panel is set by their common parent
self._showLoadProgress = (not mupdf)
self._usePrintDirect = (not mupdf)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_SIZE, self.OnResize)
@@ -143,6 +137,7 @@ class pdfViewer(wx.ScrolledWindow):
self.nom_page_gap = 20 # nominal inter-page gap (points)
self.scrollrate = 20 # pixels per scrollbar increment
self.page_buffer_valid = False
self.page_after_zoom_change = None
self.ClearBackground()
def OnIdle(self, event):
@@ -159,8 +154,6 @@ class pdfViewer(wx.ScrolledWindow):
Buffer size change due to client area resize.
"""
self.resizing = True
if hasattr(self, 'cachedpages'):
self.cachedpages = {}
event.Skip()
def OnScroll(self, event):
@@ -185,8 +178,8 @@ class pdfViewer(wx.ScrolledWindow):
# This section defines the externally callable methods:
# LoadFile, Save, Print, SetZoom, and GoPage
# also the getters and setters for ShowLoadProgress and UsePrintDirect
# that are only applicable if using PyPDF2
# also the getter and setter for ShowLoadProgress
# that is only applicable if using PyPDF2
def LoadFile(self, pdf_file):
"""
@@ -211,10 +204,9 @@ class pdfViewer(wx.ScrolledWindow):
# remove comment from next line to test using a file-like object
# pdf_file = create_fileobject(pdf_file)
if mupdf:
self.pdfdoc = mupdfProcessor(self, pdf_file)
self.pdfdoc = mupdfProcessor(self, pdf_file)
else:
self.pdfdoc = pypdfProcessor(self, pdf_file, self.ShowLoadProgress)
self.cachedpages = {}
self.numpages = self.pdfdoc.numpages
self.pagewidth = self.pdfdoc.pagewidth
@@ -288,9 +280,11 @@ class pdfViewer(wx.ScrolledWindow):
"""
pagenow = self.frompage
self.zoomscale = zoomscale
if hasattr(self, 'cachedpages'):
self.cachedpages = {}
self.page_buffer_valid = False
# calling GoPage now will trigger rendering at the new size but the page location
# will be calculated based on the old zoom scale - so save the required page number
# and call GoPage again *after* rendering at the new size
self.page_after_zoom_change = pagenow
self.GoPage(pagenow)
def GoPage(self, pagenum):
@@ -317,19 +311,6 @@ class pdfViewer(wx.ScrolledWindow):
"""Setter for showLoadProgress."""
self._showLoadProgress = flag
@property
def UsePrintDirect(self):
"""
Property to control whether prining is done via a page buffer or
directly using dcGraphicsContext (PyPDF2 only)
"""
return self._usePrintDirect
@UsePrintDirect.setter
def UsePrintDirect(self, flag):
"""Setter for usePrintDirect."""
self._usePrintDirect = flag
#----------------------------------------------------------------------------
# This section is concerned with rendering a sub-set of drawing commands on demand
@@ -342,24 +323,23 @@ class pdfViewer(wx.ScrolledWindow):
"""
self.frompage = 0
self.topage = 0
self.clientdc = dc = wx.ClientDC(self) # dc for device scaling
self.device_scale = dc.GetPPI()[0]/72.0 # pixels per inch / points per inch
device_scale = wx.ClientDC(self).GetPPI()[0]/72.0 # pixels per inch/points per inch
self.font_scale_metrics = 1.0
self.font_scale_size = 1.0
# for Windows only with wx.GraphicsContext the rendered font size is too big
# in the ratio of screen pixels per inch to points per inch
# and font metrics are too big in the same ratio for both for Cairo and wx.GC
if wx.PlatformInfo[1] == 'wxMSW':
self.font_scale_metrics = 1.0 / self.device_scale
self.font_scale_metrics = 1.0 / device_scale
if not have_cairo:
self.font_scale_size = 1.0 / self.device_scale
self.font_scale_size = 1.0 / device_scale
self.winwidth, self.winheight = self.GetClientSize()
if self.winheight < 100:
return
self.Ypage = self.pageheight + self.nom_page_gap
if self.zoomscale > 0.0:
self.scale = self.zoomscale * self.device_scale
self.scale = self.zoomscale * device_scale
else:
if int(self.zoomscale) == -1: # fit width
self.scale = self.winwidth / self.pagewidth
@@ -395,7 +375,7 @@ class pdfViewer(wx.ScrolledWindow):
# Inform buttonpanel controls of any changes
if self.buttonpanel:
self.buttonpanel.Update(self.frompage, self.numpages,
self.scale/self.device_scale)
self.scale/device_scale)
self.page_y0 = self.frompage * self.Ypagepixels
self.page_x0 = 0
@@ -428,7 +408,9 @@ class pdfViewer(wx.ScrolledWindow):
# Initialize the buffer bitmap.
self.pagebuffer = wx.Bitmap(self.pagebufferwidth, self.pagebufferheight)
self.pdc = wx.MemoryDC(self.pagebuffer) # must persist
gc = GraphicsContext.Create(self.pdc) # Cairo/wx.GraphicsContext API
# white background
path = gc.CreatePath()
path.AddRectangle(0, 0,
@@ -439,34 +421,25 @@ class pdfViewer(wx.ScrolledWindow):
for pageno in range(self.frompage, self.topage+1):
self.xpageoffset = 0 - self.x0
self.ypageoffset = pageno*self.Ypagepixels - self.page_y0
if not mupdf and pageno in self.cachedpages:
self.pdc.Blit(self.xpageoffset, self.ypageoffset,
self.Xpagepixels, self.Ypagepixels,
self.cachedpages[pageno], 0, 0)
gc.PushState()
if mupdf:
gc.Translate(self.xpageoffset, self.ypageoffset)
# scaling is done inside RenderPage
else:
t1 = time.time()
gc.PushState()
if mupdf:
gc.Translate(self.xpageoffset, self.ypageoffset)
# scaling is done inside RenderPage
else:
gc.Translate(self.xpageoffset, self.ypageoffset +
self.pageheight*self.scale)
gc.Scale(self.scale, self.scale)
self.pdfdoc.RenderPage(gc, pageno, self.scale)
# Show inter-page gap
gc.SetBrush(wx.Brush(wx.Colour(180, 180, 180))) #mid grey
gc.SetPen(wx.TRANSPARENT_PEN)
if mupdf:
gc.DrawRectangle(0, self.pageheight*self.scale,
self.pagewidth*self.scale, self.page_gap*self.scale)
else:
gc.DrawRectangle(0, 0, self.pagewidth, self.page_gap)
gc.PopState()
ttr = time.time()-t1
if not mupdf and CACHE_LATE_PAGES and ttr * 1000 > LATE_THRESHOLD:
self.CachePage(pageno) # save page out of buffer
gc.Translate(self.xpageoffset, self.ypageoffset +
self.pageheight*self.scale)
gc.Scale(self.scale, self.scale)
self.pdfdoc.RenderPage(gc, pageno, scale=self.scale)
# Show inter-page gap
gc.SetBrush(wx.Brush(wx.Colour(180, 180, 180))) #mid grey
gc.SetPen(wx.TRANSPARENT_PEN)
if mupdf:
gc.DrawRectangle(0, self.pageheight*self.scale,
self.pagewidth*self.scale, self.page_gap*self.scale)
else:
gc.DrawRectangle(0, 0, self.pagewidth, self.page_gap)
gc.PopState()
gc.PushState()
gc.Translate(0-self.x0, 0-self.page_y0)
self.RenderPageBoundaries(gc)
@@ -475,6 +448,11 @@ class pdfViewer(wx.ScrolledWindow):
self.page_buffer_valid = True
self.Refresh(0) # Blit appropriate area of new or existing page buffer to screen
# ensure we stay on the same page after zoom scale is changed
if self.page_after_zoom_change:
self.GoPage(self.page_after_zoom_change)
self.page_after_zoom_change = None
def RenderPageBoundaries(self, gc):
"""
Show non-page areas in grey.
@@ -489,17 +467,6 @@ class pdfViewer(wx.ScrolledWindow):
if extraheight > 0:
gc.DrawRectangle(0, self.winheight-extraheight, self.maxwidth, extraheight)
def CachePage(self, pageno):
"""
When page takes a 'long' time to render, save its contents out of
self.pdc and re-use it to minimise jerky scrolling
"""
cachebuffer = wx.Bitmap(self.Xpagepixels, self.Ypagepixels)
cdc = wx.MemoryDC(cachebuffer)
cdc.Blit(0, 0, self.Xpagepixels, self.Ypagepixels,
self.pdc, self.xpageoffset, self.ypageoffset)
self.cachedpages[pageno] = cdc
#============================================================================
class mupdfProcessor(object):
@@ -641,6 +608,9 @@ class pypdfProcessor(object):
'CreatePath': gc.CreatePath,
'DrawPath': gc.DrawPath }
for drawcmd, args, kwargs in self.pagedrawings[pageno]:
# scale font if requested by printer DC
if drawcmd == 'SetFont' and hasattr(gc, 'font_scale'):
args[0].Scale(gc.font_scale)
if drawcmd == 'ConcatTransform':
cm = gc.CreateMatrix(*args, **kwargs)
args = (cm,)
@@ -651,6 +621,9 @@ class pypdfProcessor(object):
args = (gp, args[1])
if drawcmd in drawdict:
drawdict[drawcmd](*args, **kwargs)
# reset font scaling in case RenderPage call is repeated
if drawcmd == 'SetFont' and hasattr(gc, 'font_scale'):
args[0].Scale(1.0/gc.font_scale)
else:
pathdict = {'MoveToPoint': gp.MoveToPoint,
'AddLineToPoint': gp.AddLineToPoint,
@@ -824,8 +797,10 @@ class pypdfProcessor(object):
"""
dlist = []
g = self.gstate
f0 = self.SetFont(g.font, g.fontSize*self.parent.font_scale_metrics)
f1 = self.SetFont(g.font, g.fontSize*self.parent.font_scale_size)
f0 = self.SetFont(g.font, g.fontSize)
f0.Scale(self.parent.font_scale_metrics)
f1 = self.SetFont(g.font, g.fontSize)
f1.Scale(self.parent.font_scale_size)
dlist.append(['SetFont', (f1, g.fillRGB), {}])
if g.wordSpacing > 0:
textlist = text.split(' ')
@@ -1078,54 +1053,25 @@ class pdfPrintout(wx.Printout):
def OnPrintPage(self, page):
"""
Provide the data for page by rendering the drawing commands
to the printer DC either via a page buffer or directly using a
dcGraphicsContext depending on self.view.usePrintDirect
"""
if not mupdf and self.view.UsePrintDirect:
self.PrintDirect(page)
else:
self.PrintViaBuffer(page)
return True
def PrintDirect(self, page):
"""
Provide the data for page by rendering the drawing commands
to the printer DC using :class:`~wx.lib.pdfviewer.dcgraphics.dcGraphicsContext`.
to the printer DC, MuPDF returns the page content from an internally
generated bitmap and sfac sets it to a high enough resolution that
reduces anti-aliasing blur but keeps it small to minimise printing time
"""
sfac = 1.0
if mupdf:
sfac = 4.0
pageno = page - 1 # zero based
width = self.view.pagewidth
height = self.view.pageheight
self.FitThisSizeToPage(wx.Size(width, height))
self.FitThisSizeToPage(wx.Size(width*sfac, height*sfac))
dc = self.GetDC()
gc = dcGraphicsContext.Create(dc, height, have_cairo)
self.view.pdfdoc.RenderPage(gc, pageno)
def PrintViaBuffer(self, page):
"""
Provide the data for page by drawing it as a bitmap to the printer DC
sfac needs to provide a high enough resolution bitmap for printing that
reduces anti-aliasing blur but be kept small to minimise printing time
"""
sfac = 4.0
pageno = page - 1 # zero based
dc = self.GetDC()
width = self.view.pagewidth*sfac
height = self.view.pageheight*sfac
self.FitThisSizeToPage(wx.Size(width, height))
# Initialize the buffer bitmap.
abuffer = wx.Bitmap(width, height)
mdc = wx.MemoryDC(abuffer)
gc = GraphicsContext.Create(mdc)
# white background
path = gc.CreatePath()
path.AddRectangle(0, 0, width, height)
gc.SetBrush(wx.WHITE_BRUSH)
gc.FillPath(path)
if mupdf:
self.view.pdfdoc.RenderPage(gc, pageno, sfac)
else:
gc = wx.GraphicsContext.Create(dc)
if not mupdf:
gc.Translate(0, height)
gc.Scale(sfac, sfac)
self.view.pdfdoc.RenderPage(gc, pageno)
dc.DrawBitmap(abuffer, 0, 0)
if wx.PlatformInfo[1] == 'wxMSW' and have_cairo:
device_scale = wx.ClientDC(self.view).GetPPI()[0]/72.0 # pixels per inch/ppi
gc.font_scale = 1.0 / device_scale
self.view.pdfdoc.RenderPage(gc, pageno, sfac)
return True