From 04a03c716a0e211e0ef4b28833914c84f66243c8 Mon Sep 17 00:00:00 2001 From: David Hughes Date: Tue, 6 Sep 2016 12:48:30 +0100 Subject: [PATCH] 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. --- wx/lib/pdfviewer/bezier.py | 71 ----- wx/lib/pdfviewer/dcgraphics.py | 474 --------------------------------- wx/lib/pdfviewer/vec2d.py | 352 ------------------------ wx/lib/pdfviewer/viewer.py | 182 +++++-------- 4 files changed, 64 insertions(+), 1015 deletions(-) delete mode 100644 wx/lib/pdfviewer/bezier.py delete mode 100644 wx/lib/pdfviewer/dcgraphics.py delete mode 100644 wx/lib/pdfviewer/vec2d.py diff --git a/wx/lib/pdfviewer/bezier.py b/wx/lib/pdfviewer/bezier.py deleted file mode 100644 index 40ad3f39..00000000 --- a/wx/lib/pdfviewer/bezier.py +++ /dev/null @@ -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 - diff --git a/wx/lib/pdfviewer/dcgraphics.py b/wx/lib/pdfviewer/dcgraphics.py deleted file mode 100644 index 9ad0dfdd..00000000 --- a/wx/lib/pdfviewer/dcgraphics.py +++ /dev/null @@ -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) - - diff --git a/wx/lib/pdfviewer/vec2d.py b/wx/lib/pdfviewer/vec2d.py deleted file mode 100644 index dce2c57c..00000000 --- a/wx/lib/pdfviewer/vec2d.py +++ /dev/null @@ -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 diff --git a/wx/lib/pdfviewer/viewer.py b/wx/lib/pdfviewer/viewer.py index 215389f6..7a1e112e 100644 --- a/wx/lib/pdfviewer/viewer.py +++ b/wx/lib/pdfviewer/viewer.py @@ -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