From 56cad152d9d3fc0fabb3f650c80ef37a77e56de0 Mon Sep 17 00:00:00 2001 From: David Hughes Date: Thu, 4 Aug 2016 11:26:08 +0100 Subject: [PATCH] 4 August 2016 Phoenix updates for wx.lib.pdfviewer and demo/PDFViewer demo/viewer.py and viewer_basics.py removed - was demo of obsolete vtk --- demo/PDFViewer.py | 35 +- demo/viewer.py | 100 ---- demo/viewer_basics.py | 75 --- wx/lib/pdfviewer/__init__.py | 38 +- wx/lib/pdfviewer/bezier.py | 2 +- wx/lib/pdfviewer/buttonpanel.py | 16 +- wx/lib/pdfviewer/dcgraphics.py | 18 +- wx/lib/pdfviewer/vec2d.py | 141 +---- wx/lib/pdfviewer/viewer.py | 938 ++++++++++++++++---------------- 9 files changed, 544 insertions(+), 819 deletions(-) delete mode 100644 demo/viewer.py delete mode 100644 demo/viewer_basics.py diff --git a/demo/PDFViewer.py b/demo/PDFViewer.py index 084dd50f..8cd8a9d0 100644 --- a/demo/PDFViewer.py +++ b/demo/PDFViewer.py @@ -3,7 +3,6 @@ import wx try: - import pyPdf from wx.lib.pdfviewer import pdfViewer, pdfButtonPanel havePyPdf = True except ImportError: @@ -51,10 +50,13 @@ def runTest(frame, nb, log): win = TestPanel(nb, log) return win else: - from Main import MessagePanel + from wx.lib.msgpanel import MessagePanel win = MessagePanel(nb, - 'This demo requires the pyPdf package to be installed.\n' - 'See: http://pybrary.net/pyPdf/', + 'This demo requires either the\n' + 'PyMuPDF see http://pythonhosted.org/PyMuPDF\n' + 'or\n' + 'PyPDF2 see http://pythonhosted.org/PyPDF2\n' + 'package installed.\n', 'Sorry', wx.ICON_WARNING) return win @@ -66,9 +68,22 @@ The wx.lib.pdfviewer.pdfViewer class is derived from wx.ScrolledWindow and can display and print PDF files. The whole file can be scrolled from end to end at whatever magnification (zoom-level) is specified. -

The viewer uses pyPdf to parse the pdf file so it is a requirement that -this must be installed. The pyPdf home page is http://pybrary.net/pyPdf/ -and the library can also be downloaded from http://pypi.python.org/pypi/pyPdf/1.12 +

The viewer checks for the PyMuPDF then the PyPDF2 package. +If neither are installed an import error exception will be raised. + +

PyMuPDF contains the Python bindings for the underlying MuPDF library, a cross platform, +complete PDF rendering library that is GPL licenced. PyMuPDF version 1.9.2 or later is required. + +

Further details on PyMuPDF can be found via http://pythonhosted.org/PyMuPDF + +

PyPDF2 provides a PdfFileReader class that is used to read the content stream of a PDF +file which is subsequently rendered by the viewer itself. +Please note that this is not a complete implementation of the pdf specification and +will probably fail to render any random PDF file you supply. However it does seem to +behave correctly with files that have been produced by ReportLab using Western languages. +The main limitation is that it doesn't currently support embedded fonts. + +

Additional details on PyPDF2 can be found via http://pythonhosted.org/PyPDF2

There is an optional pdfButtonPanel class, derived from wx.lib.agw.buttonpanel, that can be placed, for example, at the top of the scrolled viewer window, @@ -82,12 +97,6 @@ Externally callable methods are: LoadFile, Save, Print, SetZoom, and GoPage. otherwise wx.GraphicsContext is used. Printing is achieved by writing directly to a wx.PrintDC and using wx.Printer. -

Please note that pdfviewer is a far from complete implementation of the pdf -specification and will probably fail to display any random file you supply. -However it does seem to be OK with the sort of files produced by ReportLab that -use Western languages. The biggest limitation is probably that it doesn't (yet?) -support embedded fonts and will substitute one of the standard fonts instead. - """ diff --git a/demo/viewer.py b/demo/viewer.py deleted file mode 100644 index a0332d98..00000000 --- a/demo/viewer.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python - -""" - Run wxPython in a second thread. - - Overview: - Importing this module creates a second thread and starts - wxPython in that thread. Its single method, - add_cone(), sends an event to the second thread - telling it to create a VTK viewer window with a cone in - it. - - This module is meant to be imported into the standard - Python interpreter. It also works with Pythonwin. - It doesn't seem to work with IDLE (on NT anyways). - It should also work in a wxPython application. - - Applications already running a wxPython app do not - need to start a second thread. In these cases, - viewer creates the cone windows in the current - thread. You can test this by running shell.py - that comes with wxPython, importing viewer and - calling add_cone. - - Usage: - [user]$ python - Python 1.5.2 (#1, Sep 17 1999, 20:15:36) ... - Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam - >>> import viewer - >>> viewer.add_cone() # pop up a cone window - >>> a = 1 - 1 - >>> viewer.add_cone() # create another cone window - - Why would anyone do this?: - When using wxPython, the call to app.Mainloop() takes over - the thread from which it is called. This presents a - problem for applications that want to use the standard - Python command line user interface, while occasionally - creating a GUI window for viewing an image, plot, etc. - One solution is to manage the GUI in a second thread. - - wxPython does not behave well if windows are created in - a thread other than the one where wxPython was originally - imported. ( I assume importing wxPython initializes some - info in the thread). The current solution is to make the - original import of wxPython in the second thread and then - create all windows in that second thread. - - Methods in the main thread can create a new window by issuing - events to a "catcher" window in the second thread. This - catcher window has event handlers that actually create the - new window. -""" - -class viewer_thread: - def start(self): - """ start the GUI thread - """ - import time - import thread - thread.start_new_thread(self.run, ()) - - def run(self): - """ - Note that viewer_basices is first imported ***here***. - This is the second thread. viewer_basics imports - wxPython. if we imported it at - the module level instead of in this function, - the import would occur in the main thread and - wxPython wouldn't run correctly in the second thread. - """ - import viewer_basics - - try: - self.app = viewer_basics.SecondThreadApp(0) - self.app.MainLoop() - except TypeError: - self.app = None - - def add_cone(self): - """ - send an event to the catcher window in the - other thread and tell it to create a cone window. - """ - import viewer_basics - - if self.app: - evt = viewer_basics.AddCone() - viewer_basics.wxPostEvent(self.app.catcher, evt) - else: - viewer_basics.add_cone() - -viewer = viewer_thread() -viewer.start() - -def add_cone(): - viewer.add_cone() - - diff --git a/demo/viewer_basics.py b/demo/viewer_basics.py deleted file mode 100644 index 60564e15..00000000 --- a/demo/viewer_basics.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python - -# 11/15/2003 - Jeff Grimmett (grimmtooth@softhome.net) -# -# o Updated for wx namespace -# o No idea what this does. -# - -import wx -import wx.lib.vtk as vtk - -#--------------------------------------------------------------------------- -class VtkFrame(wx.Frame): - """ - Simple example VTK window that contains a cone. - """ - def __init__(self, parent, id, title): - wx.Frame.__init__(self, parent, id, title, size=(450, 300)) - win = vtk.VTKRenderWindow(self, -1) - - renWin = win.GetRenderWindow() - - ren = vtk.vtkRenderer() - renWin.AddRenderer(ren) - cone = vtk.vtkConeSource() - coneMapper = vtk.vtkPolyDataMapper() - coneMapper.SetInput(cone.GetOutput()) - coneActor = vtk.vtkActor() - coneActor.SetMapper(coneMapper) - ren.AddActor(coneActor) - -#--------------------------------------------------------------------------- -# Using new event binder -wx_EVT_ADD_CONE = wx.NewEventType() -EVT_ADD_CONE = wx.PyEventBinder(wx_EVT_ADD_CONE, 1) - -class AddCone(wx.PyEvent): - def __init__(self): - wx.PyEvent.__init__(self) - self.SetEventType(wx_EVT_ADD_CONE) - - -class HiddenCatcher(wx.Frame): - """ - The "catcher" frame in the second thread. - It is invisible. It's only job is to receive - Events from the main thread, and create - the appropriate windows. - """ - def __init__(self): - wx.Frame.__init__(self, None, -1, '') - self.Bind(EVT_ADD_CONE, self.AddCone) - - def AddCone(self,evt): - add_cone() - - -#--------------------------------------------------------------------------- - -class SecondThreadApp(wx.App): - """ - wxApp that lives in the second thread. - """ - def OnInit(self): - catcher = HiddenCatcher() - #self.SetTopWindow(catcher) - self.catcher = catcher - return True - -#--------------------------------------------------------------------------- - -def add_cone(): - frame = VtkFrame(None, -1, "Cone") - frame.Show(True) - diff --git a/wx/lib/pdfviewer/__init__.py b/wx/lib/pdfviewer/__init__.py index 8d9fc5e0..e8ee02dc 100644 --- a/wx/lib/pdfviewer/__init__.py +++ b/wx/lib/pdfviewer/__init__.py @@ -23,18 +23,22 @@ The :class:`~lib.pdfviewer.viewer.pdfViewer` class is derived from :class:`Scro and can display and print PDF files. The whole file can be scrolled from end to end at whatever magnification (zoom-level) is specified. -The viewer uses pyPDF2 or pyPdf, if neither of them are installed an -import error exception will be thrown. +The viewer uses PyMuPDF (version 1.9.2 or later) or PyPDF2. +If neither of them are installed an import error exception will be raised. -Additional details on pyPdf can be found: +PyMuPDF contains the Python bindings for the underlying MuPDF library, a cross platform, +complete PDF rendering library that is GPL licenced. -- home page: http://pybrary.net/pyPdf/ -- download: https://pypi.python.org/pypi/pyPdf +Further details on PyMuPDF can be found via http://pythonhosted.org/PyMuPDF -Additional details on pyPDF2 can be found: +PyPDF2 provides a PdfFileReader class that is used to read the content stream of a PDF +file which is subsequently rendered by :class:`~lib.pdfviewer.viewer.pdfViewer` itself. +Please note that this is not a complete implementation of the pdf specification and +will probably fail to display any random file you supply. However it does seem to +satisfactorily render files typically produced by ReportLab using Western languages. +The main limitation is that it doesn't currently support embedded fonts. -- home page: http://knowah.github.com/PyPDF2/ -- download: https://github.com/knowah/PyPDF2/ +Additional details on PyPDF2 can be found via http://pythonhosted.org/PyPDF2 There is an optional :class:`~lib.pdfviewer.buttonpanel.pdfButtonPanel` class, derived from :class:`~lib.agw.buttonpanel`, that can be placed, for example, at the top of the @@ -102,22 +106,10 @@ The viewer renders the pdf file content using Cairo if installed, otherwise :class:`GraphicsContext` is used. Printing is achieved by writing directly to a :class:`PrinterDC` and using :class:`Printer`. -Please note that :class:`~lib.pdfviewer.viewer.pdfViewer` is a far from complete -implementation of the pdf specification and will probably fail to display any -random file you supply. However it does seem to be OK with the sort of files -produced by ReportLab that use Western languages. The biggest limitation is -probably that it doesn't (yet?) support embedded fonts and will substitute one -of the standard fonts instead. - The icons used in :class:`~lib.pdfviewer.buttonpanel.pdfButtonPanel` are Free Icons -by Axialis Software: http://www.axialis.com - -You can freely use them in any project or website, commercially or not. - -TERMS OF USE: - -You must keep the credits of the authors: "Axialis Team", even if you modify them. -See ./bitmaps/ReadMe.txt for further details +by Axialis Software: http://www.axialis.com. You can freely use them in any project, +commercially or not, but you must keep the credits of the authors: +"Axialis Team", even if you modify them. See ./bitmaps/ReadMe.txt for further details. """ diff --git a/wx/lib/pdfviewer/bezier.py b/wx/lib/pdfviewer/bezier.py index 18fb7989..60e6df28 100644 --- a/wx/lib/pdfviewer/bezier.py +++ b/wx/lib/pdfviewer/bezier.py @@ -58,7 +58,7 @@ def compute_points(controlpoints, nsteps=30): """ Input 4 control points as :class:`RealPoint` and convert to vec2d instances. compute the nsteps points on the resulting curve and return them - as a list of :class:`wx.Point` + as a list of :class:`Point` """ controlvectors = [] for p in controlpoints: diff --git a/wx/lib/pdfviewer/buttonpanel.py b/wx/lib/pdfviewer/buttonpanel.py index 258719a8..36927a35 100644 --- a/wx/lib/pdfviewer/buttonpanel.py +++ b/wx/lib/pdfviewer/buttonpanel.py @@ -28,24 +28,24 @@ class pdfButtonPanel(bp.ButtonPanel): from wx.lib.agw.buttonpanel and provides buttons to manipulate the viewed PDF, e.g. zoom, save, print etc. """ - def __init__(self, parent, id, pos, size, style): + def __init__(self, parent, nid, pos, size, style): """ Default class constructor. - :param wx.Window `parent`: parent window. Must not be ``None``; - :param integer `id`: window identifier. A value of -1 indicates a default value; + :param Window `parent`: parent window. Must not be ``None``; + :param integer `nid`: window identifier. A value of -1 indicates a default value; :param `pos`: the control position. A value of (-1, -1) indicates a default position, chosen by either the windowing system or wxPython, depending on platform; - :type `pos`: tuple or :class:`wx.Point` + :type `pos`: tuple or :class:`Point` :param `size`: the control size. A value of (-1, -1) indicates a default size, chosen by either the windowing system or wxPython, depending on platform; - :type `size`: tuple or :class:`wx.Size` + :type `size`: tuple or :class:`Size` :param integer `style`: the button style (unused); """ self.viewer = None # reference to viewer is set by their common parent self.numpages = None - bp.ButtonPanel.__init__(self, parent, id, "", + bp.ButtonPanel.__init__(self, parent, nid, "", agwStyle=bp.BP_USE_GRADIENT, alignment=bp.BP_ALIGN_LEFT) self.SetProperties() self.CreateButtons() @@ -92,7 +92,7 @@ class pdfButtonPanel(bp.ButtonPanel): self.Freeze() for item in panelitems: if item[0].lower() == 'btn': - type, image, kind, popup, handler = item + x_type, image, kind, popup, handler = item btn = bp.ButtonInfo(self, wx.NewId(),image, kind=kind, shortHelp=popup, longHelp='') self.AddButton(btn) @@ -219,7 +219,7 @@ class pdfButtonPanel(bp.ButtonPanel): def OnZoomSet(self, event): """ - The zoom set handler, either a list selection of a value entered. + The zoom set handler, either a list selection or a value entered. """ MINZ = 0 MAXZ = 1000 diff --git a/wx/lib/pdfviewer/dcgraphics.py b/wx/lib/pdfviewer/dcgraphics.py index 7a7f2124..cb417fa3 100644 --- a/wx/lib/pdfviewer/dcgraphics.py +++ b/wx/lib/pdfviewer/dcgraphics.py @@ -16,7 +16,7 @@ #---------------------------------------------------------------------------- """ This module implements an API similar to :class:`GraphicsContext` and the -related classes. The implementation is done using :class:`wx.DC` +related classes. The implementation is done using :class:`DC` Why do this? Neither :class:`GraphicsContext` nor the Cairo-based GraphicsContext API provided by wx.lib.graphics can be written @@ -148,9 +148,9 @@ class dcGraphicsContext(object): 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:`GraphicsContext` fonts are too big in the ratio + :class:`DC` and :class:`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 + printed fonts need to be scaled but if :class:`GCDC` was used, they are already scaled. :param `context`: **TBW** (?) @@ -229,30 +229,30 @@ class dcGraphicsContext(object): def SetPen(self, pen): """ - Set the :class:`wx.Pen` to be used for stroking lines in future drawing + Set the :class:`Pen` to be used for stroking lines in future drawing operations. - :param `pen`: the :class:`wx.Pen` to be used from now on. + :param `pen`: the :class:`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 + Set the :class:`Brush` to be used for filling shapes in future drawing operations. - :param `brush`: the :class:`wx.Brush` to be used from now on. + :param `brush`: the :class:`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. + Sets the :class:`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 `font`: the :class:`Font` for drawing text :param `colour`: the colour to be used """ diff --git a/wx/lib/pdfviewer/vec2d.py b/wx/lib/pdfviewer/vec2d.py index e6fb17df..dce2c57c 100644 --- a/wx/lib/pdfviewer/vec2d.py +++ b/wx/lib/pdfviewer/vec2d.py @@ -9,8 +9,7 @@ # History: Created 17 Jun 2009 # -# Tags: phoenix-port, documented, unittest -# +# Tags: phoenix-port, documented #---------------------------------------------------------------------------- """ This module is used to compute Bezier curves. @@ -339,141 +338,15 @@ class vec2d(object): def cross(self, other): return self.x*other[1] - self.y*other[0] - def interpolate_to(self, other, range): - return vec2d(self.x + (other[0] - self.x)*range, self.y + (other[1] - self.y)*range) + 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()) + 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, dict): - self.x, self.y = dict - -######################################################################## -## Unit Testing ## -######################################################################## -if __name__ == "__main__": - - import unittest - import pickle - - #################################################################### - class UnitTestVec2D(unittest.TestCase): - - def setUp(self): - pass - - def testCreationAndAccess(self): - v = vec2d(111,222) - self.assert_(v.x == 111 and v.y == 222) - v.x = 333 - v[1] = 444 - self.assert_(v[0] == 333 and v[1] == 444) - - def testMath(self): - v = vec2d(111,222) - self.assertEqual(v + 1, vec2d(112,223)) - self.assert_(v - 2 == [109,220]) - self.assert_(v * 3 == (333,666)) - self.assert_(v / 2.0 == vec2d(55.5, 111)) - self.assert_(v / 2 == (55, 111)) - self.assert_(v ** vec2d(2,3) == [12321, 10941048]) - self.assert_(v + [-11, 78] == vec2d(100, 300)) - self.assert_(v / [11,2] == [10,111]) - - def testReverseMath(self): - v = vec2d(111,222) - self.assert_(1 + v == vec2d(112,223)) - self.assert_(2 - v == [-109,-220]) - self.assert_(3 * v == (333,666)) - self.assert_([222,999] / v == [2,4]) - self.assert_([111,222] ** vec2d(2,3) == [12321, 10941048]) - self.assert_([-11, 78] + v == vec2d(100, 300)) - - def testUnary(self): - v = vec2d(111,222) - v = -v - self.assert_(v == [-111,-222]) - v = abs(v) - self.assert_(v == [111,222]) - - def testLength(self): - v = vec2d(3,4) - self.assert_(v.length == 5) - self.assert_(v.get_length_sqrd() == 25) - self.assert_(v.normalize_return_length() == 5) - self.assert_(v.length == 1) - v.length = 5 - self.assert_(v == vec2d(3,4)) - v2 = vec2d(10, -2) - self.assert_(v.get_distance(v2) == (v - v2).get_length()) - - def testAngles(self): - v = vec2d(0, 3) - self.assertEquals(v.angle, 90) - v2 = vec2d(v) - v.rotate(-90) - self.assertEqual(v.get_angle_between(v2), 90) - v2.angle -= 90 - self.assertEqual(v.length, v2.length) - self.assertEquals(v2.angle, 0) - self.assertEqual(v2, [3, 0]) - self.assert_((v - v2).length < .00001) - self.assertEqual(v.length, v2.length) - v2.rotate(300) - self.assertAlmostEquals(v.get_angle_between(v2), -60) - v2.rotate(v2.get_angle_between(v)) - angle = v.get_angle_between(v2) - self.assertAlmostEquals(v.get_angle_between(v2), 0) - - def testHighLevel(self): - basis0 = vec2d(5.0, 0) - basis1 = vec2d(0, .5) - v = vec2d(10, 1) - self.assert_(v.convert_to_basis(basis0, basis1) == [2, 2]) - self.assert_(v.projection(basis0) == (10, 0)) - self.assert_(basis0.dot(basis1) == 0) - - def testCross(self): - lhs = vec2d(1, .5) - rhs = vec2d(4,6) - self.assert_(lhs.cross(rhs) == 4) - - def testComparison(self): - int_vec = vec2d(3, -2) - flt_vec = vec2d(3.0, -2.0) - zero_vec = vec2d(0, 0) - self.assert_(int_vec == flt_vec) - self.assert_(int_vec != zero_vec) - self.assert_((flt_vec == zero_vec) == False) - self.assert_((flt_vec != int_vec) == False) - self.assert_(int_vec == (3, -2)) - self.assert_(int_vec != [0, 0]) - self.assert_(int_vec != 5) - self.assert_(int_vec != [3, -2, -5]) - - def testInplace(self): - inplace_vec = vec2d(5, 13) - inplace_ref = inplace_vec - inplace_src = vec2d(inplace_vec) - inplace_vec *= .5 - inplace_vec += .5 - inplace_vec /= (3, 6) - inplace_vec += vec2d(-1, -1) - alternate = (inplace_src*.5 + .5)/vec2d(3,6) + [-1, -1] - self.assertEquals(inplace_vec, inplace_ref) - self.assertEquals(inplace_vec, alternate) - - def testPickle(self): - testvec = vec2d(5, .3) - testvec_str = pickle.dumps(testvec) - loaded_vec = pickle.loads(testvec_str) - self.assertEquals(testvec, loaded_vec) - - #################################################################### - unittest.main() - - ######################################################################## - + def __setstate__(self, adict): + self.x, self.y = adict diff --git a/wx/lib/pdfviewer/viewer.py b/wx/lib/pdfviewer/viewer.py index cd5406f0..13eb9a41 100644 --- a/wx/lib/pdfviewer/viewer.py +++ b/wx/lib/pdfviewer/viewer.py @@ -1,4 +1,4 @@ -# Name: viewer.py +# Name: viewer.py # Package: wx.lib.pdfviewer # # Purpose: A PDF report viewer class @@ -14,10 +14,12 @@ # pdfViewer.Print(). Added option to pdfViewer.LoadFile() to # accept a file-like object as well as a path string # -# Tags: phoenix-port, documented, unittest +# Tags: phoenix-port, documented, unittest # #---------------------------------------------------------------------------- + """ + This module provides the :class:`~lib.pdfviewer.viewer.pdfViewer` to view PDF files. """ @@ -28,126 +30,107 @@ import time import types import copy import shutil - -import six -from six import BytesIO - -USE_CAIRO = True -FONTSCALE = 1.0 -CACHE_LATE_PAGES = True -LATE_THRESHOLD = 200 # Time to render (ttr), milliseconds - -VERBOSE = False - -fpypdf = 0 -try: - import pyPdf - fpypdf = 1 -except: - pass - -try: - import PyPDF2 - fpypdf = 2 -except: - pass - -if not fpypdf: - msg = "You either need pyPdf or pyPDF2 to use this." - raise ImportError(msg) -elif fpypdf == 2: - from PyPDF2 import PdfFileReader - from PyPDF2.pdf import ContentStream, PageObject - from PyPDF2.filters import ASCII85Decode, FlateDecode -elif fpypdf == 1: - from pyPdf import PdfFileReader - from pyPdf.pdf import ContentStream, PageObject - from pyPdf.filters import ASCII85Decode, FlateDecode - -from .dcgraphics import dcGraphicsContext +from six import BytesIO, string_types import wx -have_cairo = False -if USE_CAIRO and wx.VERSION_STRING > '2.8.10.1': # Cairo DrawBitmap bug fixed + +CACHE_LATE_PAGES = True +LATE_THRESHOLD = 200 # Time to render (ttr), milliseconds +VERBOSE = False + +try: + # see http://pythonhosted.org/PyMuPDF - documentation & installation + import fitz + mupdf = True + if VERBOSE: print('pdfviewer using PyMuPDF (GPL)') +except ImportError: + mupdf = False try: + # see http://pythonhosted.org/PyPDF2 + import PyPDF2 + from PyPDF2 import PdfFileReader + from PyPDF2.pdf import ContentStream, PageObject + from PyPDF2.filters import ASCII85Decode, FlateDecode + if VERBOSE: print('pdfviewer using PyPDF2') + except ImportError: + msg = "PyMuPDF or PyPDF2 must be available to use pdfviewer" + raise ImportError(msg) + +GraphicsContext = wx.GraphicsContext +have_cairo = False +if not mupdf: + try: + import wx.lib.wxcairo as wxcairo import cairo from wx.lib.graphics import GraphicsContext - FONTSCALE = 1.0 have_cairo = True - if VERBOSE: print('Using Cairo') + if VERBOSE: print('pdfviewer using Cairo') except ImportError: - pass -if not have_cairo: - GraphicsContext = wx.GraphicsContext - if wx.PlatformInfo[1] == 'wxMSW': # for Windows only - FONTSCALE = 72.0 / 96.0 # wx.GraphicsContext fonts are too big in the ratio - # of screen pixels per inch to points per inch - if VERBOSE: print('Using wx.GraphicsContext') + if VERBOSE: print('pdfviewer using wx.GraphicsContext') -""" If reportlab is installed, use its stringWidth metric. For justifying text, - where widths are cumulative, dc.GetTextExtent consistently underestimates, - possibly because it returns integer rather than float. -""" -try: - from reportlab.pdfbase.pdfmetrics import stringWidth - have_rlwidth = True -except ImportError: - have_rlwidth = False + from .dcgraphics import dcGraphicsContext + + # New PageObject method added by Forestfield Software + def extractOperators(self): + """ + Locate and return all commands in the order they + occur in the content stream + """ + ops = [] + content = self["/Contents"].getObject() + if not isinstance(content, ContentStream): + content = ContentStream(content, self.pdf) + for op in content.operations: + if type(op[1] == bytes): + op = (op[0], op[1].decode()) + ops.append(op) + return ops + # Inject this method into the PageObject class + PageObject.extractOperators = extractOperators + + # If reportlab is installed, use its stringWidth metric. For justifying text, + # where widths are cumulative, dc.GetTextExtent consistently underestimates, + # possibly because it returns integer rather than float. + try: + from reportlab.pdfbase.pdfmetrics import stringWidth + have_rlwidth = True + if VERBOSE: print('pdfviewer using reportlab stringWidth function') + except ImportError: + have_rlwidth = False #---------------------------------------------------------------------------- -## New PageObject method added by Forestfield Software -def extractOperators(self): - """ - Locate and return all commands in the order they - occur in the content stream. Used by pdfviewer. - """ - ops = [] - content = self["/Contents"].getObject() - if not isinstance(content, ContentStream): - content = ContentStream(content, self.pdf) - for op in content.operations: - ops.append(op) - return ops - -# Inject this method into the PageObject class -PageObject.extractOperators = extractOperators - -#---------------------------------------------------------------------------- - class pdfViewer(wx.ScrolledWindow): - """ - View PDF report files in a scrolled window. Contents are read from PDF file - and rendered in a :class:`GraphicsContext`. Show visible window contents - as quickly as possible then read the whole file and build the set of drawing - commands for each page. This can take time for a big file or if there are complex - drawings eg. ReportLab's colour shading inside charts. Originally read in a thread - but navigation is limited until whole file is ready, so now done in main - thread with a progress bar, which isn't modal so can still do whatever navigation - is possible as the content availability increases. """ - def __init__(self, parent, id, pos, size, style): + View pdf file in a scrolled window. Contents are read from PDF file + and rendered in a GraphicsContext. Show visible window contents + as quickly as possible then, when using pyPDF, read the whole file and build + the set of drawing commands for each page. This can take time for a big file or if + there are complex drawings eg. ReportLab's colour shading inside charts and a + progress bar can be displayed by setting self.ShowLoadProgress = True (default) + """ + def __init__(self, parent, nid, pos, size, style): """ Default class constructor. - :param wx.Window `parent`: parent window. Must not be ``None``; - :param integer `id`: window identifier. A value of -1 indicates a default value; + :param Window `parent`: parent window. Must not be ``None``; + :param integer `nid`: window identifier. A value of -1 indicates a default value; :param `pos`: the control position. A value of (-1, -1) indicates a default position, chosen by either the windowing system or wxPython, depending on platform; - :type `pos`: tuple or :class:`wx.Point` + :type `pos`: tuple or :class:`Point` :param `size`: the control size. A value of (-1, -1) indicates a default size, chosen by either the windowing system or wxPython, depending on platform; - :type `size`: tuple or :class:`wx.Size` + :type `size`: tuple or :class:`Size` :param integer `style`: the button style (unused); """ - wx.ScrolledWindow.__init__(self, parent, id, pos, size, + wx.ScrolledWindow.__init__(self, parent, nid, pos, size, style | wx.NO_FULL_REPAINT_ON_RESIZE) self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) # recommended in wxWidgets docs self.buttonpanel = None # reference to panel is set by their common parent - self._showLoadProgress = True - self._usePrintDirect = True - + self._showLoadProgress = (not mupdf) + self._usePrintDirect = (not mupdf) + self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_SIZE, self.OnResize) self.Bind(wx.EVT_SCROLLWIN, self.OnScroll) @@ -156,8 +139,9 @@ class pdfViewer(wx.ScrolledWindow): self.resizing = False self.numpages = None self.zoomscale = -1 # fit page to screen width - self.nom_page_gap = 20 # nominal inter-page gap (points) + self.nom_page_gap = 20 # nominal inter-page gap (points) self.scrollrate = 20 # pixels per scrollbar increment + self.page_buffer_valid = False self.ClearBackground() def OnIdle(self, event): @@ -174,15 +158,16 @@ class pdfViewer(wx.ScrolledWindow): Buffer size change due to client area resize. """ self.resizing = True - self.cachedpages = {} + if hasattr(self, 'cachedpages'): + self.cachedpages = {} event.Skip() - def OnScroll(self,event): + def OnScroll(self, event): """ Recalculate and redraw visible area. CallAfter is *essential* for coordination. """ - wx.CallAfter(self.Render, force=False) + wx.CallAfter(self.Render) event.Skip() def OnPaint(self, event): @@ -190,74 +175,80 @@ class pdfViewer(wx.ScrolledWindow): Refresh visible window with bitmap contents. """ paintDC = wx.PaintDC(self) + paintDC.Clear() # in case buffer now smaller than visible window if hasattr(self, 'pdc'): paintDC.Blit(0, 0, self.winwidth, self.winheight, self.pdc, self.xshift, self.yshift) - else: - paintDC.Clear() #---------------------------------------------------------------------------- - "The externally callable methods are: LoadFile, Save, Print, SetZoom, and GoPage" - + # 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 + def LoadFile(self, pdf_file): """ - Read pdf file using pyPdf/pyPDF2. Assume all pages are same size, for now. - - :param `pdf_file`: can be either a string holding a filename path or - a file-like object. - + Read pdf file. Assume all pages are same size, for now. + + :param `pdf_file`: can be either a string holding + a filename path or a file-like object. """ - if isinstance(pdf_file, six.string_types): - # it must be a filename/path string, open it as a file - f = open(pdf_file, 'rb') + def create_fileobject(filename): + """ + Create and return a file object with the contents of filename, + only used for testing. + """ + f = open(filename, 'rb') + stream = f.read() + return BytesIO(stream) + + self.pdfpathname = '' + if isinstance(pdf_file, string_types): + # a filename/path string, save its name self.pdfpathname = pdf_file + # 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) else: - # assume it is a file-like object - f = pdf_file - self.pdfpathname = '' # empty default file name - self.pdfdoc = PdfFileReader(f) - self.numpages = self.pdfdoc.getNumPages() - page1 = self.pdfdoc.getPage(0) - self.pagewidth = float(page1.mediaBox.getUpperRight_x()) - self.pageheight = float(page1.mediaBox.getUpperRight_y()) - self.Scroll(0,0) # in case this is a re-LoadFile - self.CalculateDimensions(True) # to get initial visible page range - self.unimplemented = {} - self.pagedrawings = {} - self.formdrawings = {} - self.cachedpages = {} + self.pdfdoc = pypdfProcessor(self, pdf_file, self.ShowLoadProgress) + self.cachedpages = {} + + self.numpages = self.pdfdoc.numpages + self.pagewidth = self.pdfdoc.pagewidth + self.pageheight = self.pdfdoc.pageheight + self.page_buffer_valid = False + self.Scroll(0, 0) # in case this is a re-LoadFile + self.CalculateDimensions() # to get initial visible page range # draw and display the minimal set of pages - self.DrawFile(self.frompage, self.topage) + self.pdfdoc.DrawFile(self.frompage, self.topage) self.have_file = True # now draw full set of pages - wx.CallAfter(self.DrawFile, 0, self.numpages-1) + wx.CallAfter(self.pdfdoc.DrawFile, 0, self.numpages-1) def Save(self): - """ - A pdf-only Save. - """ - wild = "Portable document format (*.pdf)|*.pdf" - dlg = wx.FileDialog(self, message="Save file as ...", - wildcard=wild, - style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) - if dlg.ShowModal() == wx.ID_OK: - pathname = dlg.GetPath() - shutil.copy(self.pdfpathname, pathname) - dlg.Destroy() + "Save a copy of the pdf file if it was originally named" + if self.pdfpathname: + wild = "Portable document format (*.pdf)|*.pdf" + dlg = wx.FileDialog(self, message="Save file as ...", + wildcard=wild, style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) + if dlg.ShowModal() == wx.ID_OK: + pathname = dlg.GetPath() + shutil.copy(self.pdfpathname, pathname) + dlg.Destroy() def Print(self, prompt=True, printer_name=None, orientation=None): """ Print the pdf. - + :param boolean `prompt`: show the print dialog to the user (True/False). If False, the print dialog will not be shown and the pdf will be printed immediately. Default: True. :param string `printer_name`: the name of the printer that is to receive the printout. Default: as set by the O/S. - :param `orientation`: select the orientation (:class:`wx.PORTRAIT` or - :class:`wx.LANDSCAPE`) for the printout. Default: as set by the O/S. - + :param `orientation`: select the orientation (wx.PORTRAIT or + wx.LANDSCAPE) for the printout. Default: as set by the O/S. """ pdd = wx.PrintDialogData() pdd.SetMinPage(1) @@ -269,6 +260,11 @@ class pdfViewer(wx.ScrolledWindow): pdata.SetPrinterName(printer_name) if orientation: pdata.SetOrientation(orientation) + # PrintData does not return actual PrintQuality - it can't as printer_name not known + # but it defaults to wx.PRINT_QUALITY_HIGH, overriding user's own setting for the + # printer. However calling SetQuality with a value of 0 seems to leave the printer + # setting untouched + pdata.SetQuality(0) printer = wx.Printer(pdd) printout = pdfPrintout('', self) if (not printer.Print(self, printout, prompt=prompt) and @@ -281,47 +277,78 @@ class pdfViewer(wx.ScrolledWindow): def SetZoom(self, zoomscale): """ - Positive integer or floating zoom scale will render the file at - the corresponding size where 1.0 is "actual" point size (1/72"). + Positive integer or floating zoom scale will render the file at corresponding + size where 1.0 is "actual" point size (1/72"). -1 fits page width and -2 fits page height into client area Redisplay the current page(s) at the new size - + :param `zoomscale`: an integer or float - + """ pagenow = self.frompage self.zoomscale = zoomscale - self.cachedpages = {} - self.CalculateDimensions(True) + if hasattr(self, 'cachedpages'): + self.cachedpages = {} + self.page_buffer_valid = False self.GoPage(pagenow) def GoPage(self, pagenum): """ Go to page - + :param integer `pagenum`: go to the provided page number if it is valid - + """ if pagenum > 0 and pagenum <= self.numpages: - self.Scroll(0, pagenum*self.Ypagepixels/self.GetScrollPixelsPerUnit()[1]) + self.Scroll(0, pagenum*self.Ypagepixels/self.GetScrollPixelsPerUnit()[1] + 1) else: self.Scroll(0, 0) - self.Render() + # calling Scroll sometimes doesn't raise wx.EVT_SCROLLWIN eg Windows 8 64 bit - so + wx.CallAfter(self.Render) + + @property + def ShowLoadProgress(self): + """Property to control if file reading progress is shown (PyPDF2 only)""" + return self._showLoadProgress + + @ShowLoadProgress.setter + def ShowLoadProgress(self, flag): + """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" + # This section is concerned with rendering a sub-set of drawing commands on demand - def CalculateDimensions(self, force): + def CalculateDimensions(self): """ Compute the required buffer sizes to hold the viewed rectangle and - the range of pages visible. Override force flag and set true if - the current set of rendered pages changes. + the range of pages visible. Set self.page_buffer_valid = False if + the current set of rendered pages changes """ 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 + self.clientdc = dc = wx.ClientDC(self) # dc for device scaling + self.device_scale = dc.GetPPI()[0]/72.0 # pixels per inch / points per inch + self.font_scale = 1.0 + # for Windows only wx.GraphicsContext fonts are too big + # in the ratio of screen pixels per inch to points per inch + if wx.PlatformInfo[1] == 'wxMSW' and not have_cairo: + self.font_scale = 1.0 / self.device_scale + self.winwidth, self.winheight = self.GetClientSize() if self.winheight < 100: return @@ -345,18 +372,18 @@ class pdfViewer(wx.ScrolledWindow): self.Ypagepixels = nhi else: self.Ypagepixels = nlo - self.page_gap = self.Ypagepixels/self.scale - self.pageheight + self.page_gap = self.Ypagepixels/self.scale - self.pageheight self.maxwidth = max(self.winwidth, self.Xpagepixels) self.maxheight = max(self.winheight, self.numpages*self.Ypagepixels) self.SetVirtualSize((self.maxwidth, self.maxheight)) - self.SetScrollRate(self.scrollrate,self.scrollrate) + self.SetScrollRate(self.scrollrate, self.scrollrate) xv, yv = self.GetViewStart() dx, dy = self.GetScrollPixelsPerUnit() self.x0, self.y0 = (xv * dx, yv * dy) - self.frompage = min(self.y0/self.Ypagepixels, self.numpages-1) - self.topage = min((self.y0+self.winheight-1)/self.Ypagepixels, self.numpages-1) + self.frompage = int(min(self.y0/self.Ypagepixels, self.numpages-1)) + self.topage = int(min((self.y0+self.winheight-1)/self.Ypagepixels, self.numpages-1)) self.pagebufferwidth = max(self.Xpagepixels, self.winwidth) self.pagebufferheight = (self.topage - self.frompage + 1) * self.Ypagepixels @@ -369,108 +396,79 @@ class pdfViewer(wx.ScrolledWindow): self.page_x0 = 0 self.xshift = self.x0 - self.page_x0 self.yshift = self.y0 - self.page_y0 - if force: # by external request + if not self.page_buffer_valid: # via external setting self.cur_frompage = self.frompage self.cur_topage = self.topage else: # page range unchanged? whole visible area will always be inside page buffer - if self.frompage != self.cur_frompage or self.topage != self.cur_topage: - force = True # due to page buffer change + if self.frompage != self.cur_frompage or self.topage != self.cur_topage: + self.page_buffer_valid = False # due to page buffer change self.cur_frompage = self.frompage self.cur_topage = self.topage - return force + return - def Render(self, force=True): + def Render(self): """ Recalculate dimensions as client area may have been scrolled or resized. The smallest unit of rendering that can be done is the pdf page. So render the drawing commands for the pages in the visible rectangle into a buffer - big enough to hold this set of pages. For each page, use gc.Translate to - render wrt the pdf origin, which is at the bottom left corner of the page. - Force re-creating the page buffer only when client view moves outside it. + big enough to hold this set of pages. Force re-creating the page buffer + only when client view moves outside it. + With PyPDF2, use gc.Translate to render each page wrt the pdf origin, + which is at the bottom left corner of the page. """ if not self.have_file: return - force = self.CalculateDimensions(force) - if force: - # Initialize the buffer bitmap. + self.CalculateDimensions() + if not self.page_buffer_valid: + # 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, self.pagebuffer.GetWidth(), self.pagebuffer.GetHeight()) + path.AddRectangle(0, 0, + self.pagebuffer.GetWidth(), self.pagebuffer.GetHeight()) gc.SetBrush(wx.WHITE_BRUSH) gc.FillPath(path) for pageno in range(self.frompage, self.topage+1): self.xpageoffset = 0 - self.x0 self.ypageoffset = pageno*self.Ypagepixels - self.page_y0 - if pageno in self.cachedpages: + if not mupdf and pageno in self.cachedpages: self.pdc.Blit(self.xpageoffset, self.ypageoffset, - self.Xpagepixels, self.Ypagepixels, + self.Xpagepixels, self.Ypagepixels, self.cachedpages[pageno], 0, 0) - else: + else: t1 = time.time() gc.PushState() - gc.Translate(0 - self.x0, pageno*self.Ypagepixels + - self.pageheight*self.scale - self.page_y0) - gc.Scale(self.scale, self.scale) - self.RenderPage(gc, self.pagedrawings[pageno]) + 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) - gc.DrawRectangle(0, 0, self.pagewidth, self.page_gap) + 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 CACHE_LATE_PAGES and ttr * 1000 > LATE_THRESHOLD: + ttr = time.time()-t1 + if not mupdf and CACHE_LATE_PAGES and ttr * 1000 > LATE_THRESHOLD: self.CachePage(pageno) # save page out of buffer - #print('Page %d rendered in %.3f seconds' % (pageno+1, ttr)) - gc.PushState() + gc.PushState() gc.Translate(0-self.x0, 0-self.page_y0) self.RenderPageBoundaries(gc) gc.PopState() - self.Refresh(0) # Blit appropriate area of new or existing page buffer to screen - #print('Cached pages:', self.cachedpages.keys()) - #self.pagebuffer.SaveFile('pagemap.png', wx.BITMAP_TYPE_PNG) - def RenderPage(self, gc, pagedrawings): - """ - Render the set of pagedrawings - In a pdf file, bitmaps are treated as being of unit width and height and - are scaled via a previous ConcatTransform containing the corresponding width - and height as scale factors. wx.GraphicsContext/Cairo appear not to respond to - this so scaling is removed from transform and width & height are added - to the Drawbitmap call. - """ - drawdict = {'ConcatTransform': gc.ConcatTransform, - 'PushState': gc.PushState, - 'PopState': gc.PopState, - 'SetFont': gc.SetFont, - 'SetPen': gc.SetPen, - 'SetBrush': gc.SetBrush, - 'DrawText': gc.DrawText, - 'DrawBitmap': gc.DrawBitmap, - 'CreatePath': gc.CreatePath, - 'DrawPath': gc.DrawPath } - for drawcmd, args, kwargs in pagedrawings: - if drawcmd == 'ConcatTransform': - cm = gc.CreateMatrix(*args, **kwargs) - args = (cm,) - if drawcmd == 'CreatePath': - gp = drawdict[drawcmd](*args, **kwargs) - continue - elif drawcmd == 'DrawPath': - args = (gp, args[1]) - if drawcmd in drawdict: - drawdict[drawcmd](*args, **kwargs) - else: - pathdict = {'MoveToPoint': gp.MoveToPoint, - 'AddLineToPoint': gp.AddLineToPoint, - 'AddCurveToPoint': gp.AddCurveToPoint, - 'AddRectangle': gp.AddRectangle, - 'CloseSubpath': gp.CloseSubpath } - if drawcmd in pathdict: - pathdict[drawcmd](*args, **kwargs) + self.page_buffer_valid = True + self.Refresh(0) # Blit appropriate area of new or existing page buffer to screen def RenderPageBoundaries(self, gc): """ @@ -489,25 +487,104 @@ class pdfViewer(wx.ScrolledWindow): 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. + 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 - -#---------------------------------------------------------------------------- - "These methods interpret the PDF contents as a set of drawing commands" +#============================================================================ + +class mupdfProcessor(object): + """ + Create an instance of this class to open a PDF file, process the contents of + each page and render each one on demand using the GPL mupdf library, which is + accessed via the python-fitz package bindings (version 1.9.1 or later) + """ + def __init__(self, parent, pdf_file): + """ + :param `pdf_file`: a File object or an object that supports the standard + read and seek methods similar to a File object. + Could also be a string representing a path to a PDF file. + """ + self.parent = parent + if isinstance(pdf_file, string_types): + # a filename/path string, pass the name to fitz.open + pathname = pdf_file + self.pdfdoc = fitz.open(pathname) + else: + # assume it is a file-like object, pass the stream content to fitz.open + # and a '.pdf' extension in pathname to identify the stream type + pathname = 'fileobject.pdf' + if pdf_file.tell() > 0: # not positioned at start + pdf_file.seek(0) + stream = bytearray(pdf_file.read()) + self.pdfdoc = fitz.open(pathname, stream) + + self.numpages = self.pdfdoc.pageCount + page = self.pdfdoc.loadPage(0) + self.pagewidth = page.bound().width + self.pageheight = page.bound().height + self.page_rect = page.bound() + self.zoom_error = False #set if memory errors during render + + def DrawFile(self, frompage, topage): + """ + This is a no-op for mupdf. Each page is scaled and drawn on + demand during RenderPage directly via a call to page.getPixmap() + """ + self.parent.GoPage(frompage) + + def RenderPage(self, gc, pageno, scale=1.0): + " Render the set of pagedrawings into gc for specified page " + page = self.pdfdoc.loadPage(pageno) + matrix = fitz.Matrix(scale, scale) + try: + pix = page.getPixmap(matrix=matrix) # MUST be keyword arg(s) + bmp = wx.Bitmap.FromBufferRGBA(pix.width, pix.height, pix.samples) + gc.DrawBitmap(bmp, 0, 0, pix.width, pix.height) + self.zoom_error = False + except (RuntimeError, MemoryError): + if not self.zoom_error: # report once only + self.zoom_error = True + dlg = wx.MessageDialog(self.parent, 'Out of memory. Zoom level too high?', + 'pdf viewer' , wx.OK |wx.ICON_EXCLAMATION) + dlg.ShowModal() + dlg.Destroy() + +#============================================================================ + +class pypdfProcessor(object): + """ + Create an instance of this class to open a PDF file, process the contents of + every page using PyPDF2 then render each one on demand + """ + def __init__(self, parent, fileobj, showloadprogress): + self.parent = parent + self.showloadprogress = showloadprogress + self.pdfdoc = PdfFileReader(fileobj) + self.numpages = self.pdfdoc.getNumPages() + page1 = self.pdfdoc.getPage(0) + self.pagewidth = float(page1.mediaBox.getUpperRight_x()) + self.pageheight = float(page1.mediaBox.getUpperRight_y()) + self.pagedrawings = {} + self.unimplemented = {} + self.formdrawings = {} + self.page = None + self.gstate = None + self.saved_state = None + self.knownfont = False + self.progbar = None + + # These methods interpret the PDF contents as a set of drawing commands def Progress(self, ptype, value): - """ - This function is called at regular intervals during Drawfile. - """ + " This function is called at regular intervals during Drawfile" if ptype == 'start': - msg = 'Reading pdf file' - self.progbar = wx.ProgressDialog('Load file', msg, value, None, + pmsg = 'Reading pdf file' + self.progbar = wx.ProgressDialog('Load file', pmsg, value, None, wx.PD_AUTO_HIDE| wx.PD_ESTIMATED_TIME|wx.PD_REMAINING_TIME) elif ptype == 'progress': @@ -520,36 +597,73 @@ class pdfViewer(wx.ScrolledWindow): Build set of drawing commands from PDF contents. Ideally these could be drawn straight into a PseudoDC and the visible section painted directly into scrolled window, but we need to be able to zoom and scale the output quickly - without having to rebuild the drawing commands (slow). So roll our + without having to rebuild the drawing commands (slow). So build our own command lists, one per page, into self.pagedrawings. - """ - t0 = time.time() + """ numpages_generated = 0 - rp = (self.ShowLoadProgress and frompage == 0 and topage == self.numpages-1) + rp = (self.showloadprogress and frompage == 0 and topage == self.numpages-1) if rp: self.Progress('start', self.numpages) - for self.pageno in range(frompage, topage+1): + for pageno in range(frompage, topage+1): self.gstate = pdfState() # state is reset with every new page self.saved_state = [] - self.page = self.pdfdoc.getPage(self.pageno) + self.page = self.pdfdoc.getPage(pageno) numpages_generated += 1 pdf_fonts = self.FetchFonts(self.page) - self.pagedrawings[self.pageno] = self.ProcessOperators( - self.page.extractOperators(), pdf_fonts) + self.pagedrawings[pageno] = self.ProcessOperators( + self.page.extractOperators(), pdf_fonts) if rp: self.Progress('progress', numpages_generated) - ## print('Pages %d to %d. %d pages created in %.2f seconds' % ( - ## frompage, topage, numpages_generated,(time.time()-t0))) if rp: self.Progress('end', None) - self.GoPage(frompage) + self.parent.GoPage(frompage) + + def RenderPage(self, gc, pageno, scale=None): + """ + Render the set of pagedrawings + In a pdf file, bitmaps are treated as being of unit width and height and + are scaled via a previous ConcatTransform containing the corresponding width + and height as scale factors. wx.GraphicsContext/Cairo appear not to respond to + this so scaling is removed from transform and width & height are added + to the Drawbitmap call. + """ + drawdict = {'ConcatTransform': gc.ConcatTransform, + 'PushState': gc.PushState, + 'PopState': gc.PopState, + 'SetFont': gc.SetFont, + 'SetPen': gc.SetPen, + 'SetBrush': gc.SetBrush, + 'DrawText': gc.DrawText, + 'DrawBitmap': gc.DrawBitmap, + 'CreatePath': gc.CreatePath, + 'DrawPath': gc.DrawPath } + for drawcmd, args, kwargs in self.pagedrawings[pageno]: + if drawcmd == 'ConcatTransform': + cm = gc.CreateMatrix(*args, **kwargs) + args = (cm,) + if drawcmd == 'CreatePath': + gp = drawdict[drawcmd](*args, **kwargs) + continue + elif drawcmd == 'DrawPath': + args = (gp, args[1]) + if drawcmd in drawdict: + drawdict[drawcmd](*args, **kwargs) + else: + pathdict = {'MoveToPoint': gp.MoveToPoint, + 'AddLineToPoint': gp.AddLineToPoint, + 'AddCurveToPoint': gp.AddCurveToPoint, + 'AddRectangle': gp.AddRectangle, + 'CloseSubpath': gp.CloseSubpath } + if drawcmd in pathdict: + pathdict[drawcmd](*args, **kwargs) def FetchFonts(self, currentobject): - """ - Return the standard fonts in current page or form. - """ + " Return the standard fonts in current page or form" pdf_fonts = {} - fonts = currentobject["/Resources"].getObject()['/Font'] - for key in fonts: - pdf_fonts[key] = fonts[key]['/BaseFont'][1:] # remove the leading '/' + try: + fonts = currentobject["/Resources"].getObject()['/Font'] + for key in fonts: + pdf_fonts[key] = fonts[key]['/BaseFont'][1:] # remove the leading '/' + except KeyError: + pass return pdf_fonts def ProcessOperators(self, opslist, pdf_fonts): @@ -560,10 +674,10 @@ class pdfViewer(wx.ScrolledWindow): path = [] for operand, operator in opslist : g = self.gstate - if operator == 'cm': # new transformation matrix + if operator == 'cm' and operand: # new transformation matrix # some operands need inverting because directions of y axis # in pdf and graphics context are opposite - a, b, c, d, e, f = map(float, operand) + a, b, c, d, e, f = [float(n) for n in operand] drawlist.append(['ConcatTransform', (a, -b, -c, d, e, -f), {}]) elif operator == 'q': # save state self.saved_state.append(copy.deepcopy(g)) @@ -572,10 +686,10 @@ class pdfViewer(wx.ScrolledWindow): self.gstate = self.saved_state.pop() drawlist.append(['PopState', (), {}]) elif operator == 'RG': # Stroke RGB - rs, gs, bs = [int(v*255) for v in map(float, operand)] + rs, gs, bs = [int(float(n)*255) for n in operand] g.strokeRGB = wx.Colour(rs, gs, bs) elif operator == 'rg': # Fill RGB - rf, gf, bf = [int(v*255) for v in map(float, operand)] + rf, gf, bf = [int(float(n)*255) for n in operand] g.fillRGB = wx.Colour(rf, gf, bf) elif operator == 'K': # Stroke CMYK rs, gs, bs = self.ConvertCMYK(operand) @@ -584,7 +698,7 @@ class pdfViewer(wx.ScrolledWindow): rf, gf, bf = self.ConvertCMYK(operand) g.fillRGB = wx.Colour(rf, gf, bf) elif operator == 'w': # Line width - g.lineWidth = float(operand[0]) + g.lineWidth = max(float(operand[0]), 1.0) elif operator == 'J': # Line cap ix = float(operand[0]) g.lineCapStyle = {0: wx.CAP_BUTT, 1: wx.CAP_ROUND, @@ -594,10 +708,10 @@ class pdfViewer(wx.ScrolledWindow): g.lineJoinStyle = {0: wx.JOIN_MITER, 1: wx.JOIN_ROUND, 2: wx.JOIN_BEVEL}[ix] elif operator == 'd': # Line dash pattern - g.lineDashArray = map(int, operand[0]) + g.lineDashArray = [int(n) for n in operand[0]] g.lineDashPhase = int(operand[1]) elif operator in ('m', 'c', 'l', 're', 'v', 'y', 'h'): # path defining ops - path.append([map(float, operand), operator]) + path.append([[float(n) for n in operand], operator]) elif operator in ('b', 'B', 'b*', 'B*', 'f', 'F', 'f*', 's', 'S', 'n'): # path drawing ops drawlist.extend(self.DrawPath(path, operator)) @@ -608,8 +722,8 @@ class pdfViewer(wx.ScrolledWindow): elif operator == 'ET': # end text object continue elif operator == 'Tm': # text matrix - g.textMatrix = map(float, operand) - g.textLineMatrix = map(float, operand) + g.textMatrix = [float(n) for n in operand] + g.textLineMatrix = [float(n) for n in operand] elif operator == 'TL': # text leading g.leading = float(operand[0]) #elif operator == 'Tc': # character spacing @@ -630,71 +744,82 @@ class pdfViewer(wx.ScrolledWindow): g.font = pdf_fonts[operand[0]] g.fontSize = float(operand[1]) elif operator == 'Tj': # show text - drawlist.extend(self.DrawTextString(operand[0])) + drawlist.extend(self.DrawTextString( + operand[0].original_bytes.decode('latin-1'))) elif operator == 'Do': # invoke named XObject - drawlist.extend(self.InsertXObject(operand[0])) + dlist = self.InsertXObject(operand[0]) + if dlist: # may be unimplemented decode + drawlist.extend(dlist) elif operator == 'INLINE IMAGE': # special pyPdf case + operand is a dict - drawlist.extend(self.InlineImage(operand)) + dlist = self.InlineImage(operand) + if dlist: # may be unimplemented decode + drawlist.extend(dlist) else: # report once if operator not in self.unimplemented: if VERBOSE: print('PDF operator %s is not implemented' % operator) self.unimplemented[operator] = 1 - # Fix bitmap transform. Remove the scaling from any transform matrix that precedes - # a DrawBitmap operation as the scaling is now done in that operation. + # Fix bitmap transform. Move the scaling from any transform matrix that precedes + # a DrawBitmap operation into the op itself - the width and height extracted from + # the bitmap is the size of the original PDF image not the size it is to be drawn for k in range(len(drawlist)-1): if drawlist[k][0] == 'ConcatTransform' and drawlist[k+1][0] == 'DrawBitmap': - args = list(drawlist[k][1]) - args[0] = 1.0 - args[3] = 1.0 - drawlist[k][1] = tuple(args) - return drawlist + ctargs = list(drawlist[k][1]) + bmargs = list(drawlist[k+1][1]) + bmargs[2] = -ctargs[3] # y position + bmargs[3] = ctargs[0] # width + bmargs[4] = ctargs[3] # height + ctargs[0] = 1.0 + ctargs[3] = 1.0 + drawlist[k][1] = tuple(ctargs) + drawlist[k+1][1] = tuple(bmargs) + return drawlist def SetFont(self, pdfont, size): """ - Returns :class:`wx.Font` instance from supplied pdf font information. + Returns :class:`Font` instance from supplied pdf font information. """ self.knownfont = True pdfont = pdfont.lower() if pdfont.count('courier'): - family = wx.FONTFAMILY_MODERN + family = wx.FONTFAMILY_MODERN font = 'Courier New' elif pdfont.count('helvetica'): - family = wx.FONTFAMILY_SWISS + family = wx.FONTFAMILY_SWISS font = 'Arial' elif pdfont.count('times'): - family = wx.FONTFAMILY_ROMAN + family = wx.FONTFAMILY_ROMAN font = 'Times New Roman' elif pdfont.count('symbol'): - family = wx.FONTFAMILY_DEFAULT + family = wx.FONTFAMILY_DEFAULT font = 'Symbol' elif pdfont.count('zapfdingbats'): - family = wx.FONTFAMILY_DEFAULT + family = wx.FONTFAMILY_DEFAULT font = 'Wingdings' else: if VERBOSE: print('Unknown font %s' % pdfont) self.knownfont = False - family = wx.FONTFAMILY_SWISS + family = wx.FONTFAMILY_SWISS font = 'Arial' - - weight = wx.FONTWEIGHT_NORMAL + + weight = wx.FONTWEIGHT_NORMAL if pdfont.count('bold'): - weight = wx.FONTWEIGHT_BOLD + weight = wx.FONTWEIGHT_BOLD style = wx.FONTSTYLE_NORMAL if pdfont.count('oblique') or pdfont.count('italic'): style = wx.FONTSTYLE_ITALIC - return wx.Font(max(1,size), family, style, weight, faceName=font) + return wx.Font(max(1, size), family, style, weight, faceName=font) - def DrawTextString(self, text): + def DrawTextString(self, text): """ Draw a text string. Word spacing only works for horizontal text. - + :param string `text`: the text to draw - + """ dlist = [] g = self.gstate - f = self.SetFont(g.font, g.fontSize*FONTSCALE) + f = self.SetFont(g.font, g.fontSize*self.parent.font_scale) dlist.append(['SetFont', (f, g.fillRGB), {}]) if g.wordSpacing > 0: textlist = text.split(' ') @@ -702,29 +827,29 @@ class pdfViewer(wx.ScrolledWindow): textlist = [text,] for item in textlist: dlist.append(self.DrawTextItem(item, f)) - return dlist + return dlist def DrawTextItem(self, textitem, f): """ Draw a text item. - - :param `textitem`: the item to draw ??? what is the type - :param `f`: the font to use for text extent measuring ??? - + + :param `textitem`: the item to draw + :param `f`: the font to use for text extent measuring + """ - dc = wx.ClientDC(self) # dummy dc for text extents + dc = wx.ClientDC(self.parent) # dummy dc for text extents g = self.gstate x = g.textMatrix[4] y = g.textMatrix[5] + g.textRise if g.wordSpacing > 0: textitem += ' ' - wid, ht, descend, xlead = dc.GetFullTextExtent(textitem, f) - if have_rlwidth and self.knownfont: # use ReportLab stringWidth if available + wid, ht, descend, x_lead = dc.GetFullTextExtent(textitem, f) + if have_rlwidth and self.knownfont: # use ReportLab stringWidth if available width = stringWidth(textitem, g.font, g.fontSize) else: - width = wid/self.device_scale + width = wid g.textMatrix[4] += (width + g.wordSpacing) # update current x position - return ['DrawText', (textitem, x, -y-(ht-descend)/self.device_scale), {}] + return ['DrawText', (textitem, x, -y-(ht-descend)), {}] def DrawPath(self, path, action): """ @@ -760,12 +885,12 @@ class pdfViewer(wx.ScrolledWindow): else: dlist.append(['SetPen', (wx.TRANSPARENT_PEN,), {}]) - if fill: + if fill: dlist.append(['SetBrush', (wx.Brush(g.fillRGB),), {}]) - else: + else: dlist.append(['SetBrush', (wx.TRANSPARENT_BRUSH,), {}]) - dlist.append(['CreatePath', (), {}]) + dlist.append(['CreatePath', (), {}]) for xylist, op in path: if op == 'm': # move (to) current point x0 = xc = xylist[0] @@ -777,26 +902,29 @@ class pdfViewer(wx.ScrolledWindow): dlist.append(['AddLineToPoint', (x2, y2), {}]) xc = x2 yc = y2 - elif op == 're': # draw rectangle (x,y at top left) + elif op == 're': # draw rectangle x = xylist[0] y = -xylist[1] w = xylist[2] h = xylist[3] - dlist.append(['AddRectangle', (x, y-h, w, h), {}]) + retuple = (x, y-h, w, h) + if h < 0.0: + retuple = (x, y, w, -h) + dlist.append(['AddRectangle', retuple, {}]) elif op in ('c', 'v', 'y'): # draw Bezier curve args = [] if op == 'v': args.extend([xc, yc]) - args.extend([xylist[0], -xylist[1], + args.extend([xylist[0], -xylist[1], xylist[2], -xylist[3]]) if op == 'y': args.extend([xylist[2], -xylist[3]]) if op == 'c': - args.extend([xylist[4], -xylist[5]]) + args.extend([xylist[4], -xylist[5]]) dlist.append(['AddCurveToPoint', args, {}]) elif op == 'h': dlist.append(['CloseSubpath', (), {}]) - dlist.append(['DrawPath', ('GraphicsPath', rule), {}]) + dlist.append(['DrawPath', ('GraphicsPath', rule), {}]) return dlist def InsertXObject(self, name): @@ -810,7 +938,7 @@ class pdfViewer(wx.ScrolledWindow): # insert contents into current page drawing if not name in self.formdrawings: # extract if not already done pdf_fonts = self.FetchFonts(stream) - bbox = stream.get('/BBox') + x_bbox = stream.get('/BBox') matrix = stream.get('/Matrix') form_ops = ContentStream(stream, self.pdfdoc).operations oplist = [([], 'q'), (matrix, 'cm')] # push state & apply matrix @@ -819,25 +947,27 @@ class pdfViewer(wx.ScrolledWindow): self.formdrawings[name] = self.ProcessOperators(oplist, pdf_fonts) dlist.extend(self.formdrawings[name]) elif stream.get('/Subtype') == '/Image': - width = stream.get('/Width') - height = stream.get('/Height') - depth = stream.get('/BitsPerComponent') - filters = stream.get("/Filter", ()) - dlist.append(self.AddBitmap(stream._data, width, height, filters)) + width = stream['/Width'] + height = stream['/Height'] + x_depth = stream['/BitsPerComponent'] + filters = stream["/Filter"] + item = self.AddBitmap(stream._data, width, height, filters) + if item: # may be unimplemented + dlist.append(item) return dlist def InlineImage(self, operand): - """ - Operand contains an image. - """ + """ operand contains an image""" dlist = [] data = operand.get('data') settings = operand.get('settings') - width = settings['/W'] + width = settings['/W'] height = settings['/H'] - depth = settings['/BPC'] + x_depth = settings['/BPC'] filters = settings['/F'] - dlist.append(self.AddBitmap(data, width, height, filters)) + item = self.AddBitmap(data, width, height, filters) + if item: # may be unimplemented + dlist.append(item) return dlist def AddBitmap(self, data, width, height, filters): @@ -845,15 +975,22 @@ class pdfViewer(wx.ScrolledWindow): Add wx.Bitmap from data, processed by filters. """ if '/A85' in filters or '/ASCII85Decode' in filters: - data = _AsciiBase85DecodePYTHON(data) + data = ASCII85Decode.decode(data) if '/Fl' in filters or '/FlateDecode' in filters: data = FlateDecode.decode(data, None) + if '/CCF' in filters or '/CCITTFaxDecode' in filters: + if VERBOSE: + print('PDF operation /CCITTFaxDecode is not implemented') + return [] if '/DCT' in filters or '/DCTDecode' in filters: stream = BytesIO(data) image = wx.Image(stream, wx.BITMAP_TYPE_JPEG) bitmap = wx.Bitmap(image) - else: - bitmap = wx.BitmapFromBuffer(width, height, data) + else: + try: + bitmap = wx.Bitmap.FromBuffer(width, height, data) + except: + return [] # any error return ['DrawBitmap', (bitmap, 0, 0-height, width, height), {}] def ConvertCMYK(self, operand): @@ -865,47 +1002,18 @@ class pdfViewer(wx.ScrolledWindow): b = round((1-y)*(1-k)*255) g = round((1-m)*(1-k)*255) return (r, g, b) - - @property - def ShowLoadProgress(self): - """ - Property to control if loading progress be shown. - """ - return self._showLoadProgress - - @ShowLoadProgress.setter - def ShowLoadProgress(self, flag): - """ - Setter for showLoadProgress. - """ - self._showLoadProgress = flag - - @property - def UsePrintDirect(self): - """ - Property to control to use either Cairo (via a page buffer) or - dcGraphicsContext. - """ - return self._usePrintDirect - - @UsePrintDirect.setter - def UsePrintDirect(self, flag): - """ - Setter for usePrintDirect. - """ - self._usePrintDirect = flag - + #---------------------------------------------------------------------------- -class pdfState: +class pdfState(object): """ Instance holds the current pdf graphics and text state. It can be - saved (pushed) and restored (popped) by the owning parent. + saved (pushed) and restored (popped) by the owning parent """ def __init__ (self): """ - Creates an instance with default values. Individual attributes - are modified directly not via getters and setters. + Creates an instance with default values. Individual attributes + are modified directly not via getters and setters """ self.lineWidth = 1.0 self.lineCapStyle = wx.CAP_BUTT @@ -957,21 +1065,21 @@ class pdfPrintout(wx.Printout): Supply maximum range of pages and the range to be printed These are initial values passed to Printer dialog, where they can be amended by user. - """ - max = self.view.numpages - return (1, max, 1, max) + """ + maxnum = self.view.numpages + return (1, maxnum, 1, maxnum) def OnPrintPage(self, page): """ Provide the data for page by rendering the drawing commands - to the printer DC using either Cairo (via a page buffer) or - dcGraphicsContext depending on the self.view.usePrintDirect property. + to the printer DC either via a page buffer or directly using a + dcGraphicsContext depending on self.view.usePrintDirect """ - if self.view.UsePrintDirect: + if not mupdf and self.view.UsePrintDirect: self.PrintDirect(page) else: self.PrintViaBuffer(page) - return True + return True def PrintDirect(self, page): """ @@ -984,116 +1092,34 @@ class pdfPrintout(wx.Printout): self.FitThisSizeToPage(wx.Size(width, height)) dc = self.GetDC() gc = dcGraphicsContext.Create(dc, height, have_cairo) - self.view.RenderPage(gc, self.view.pagedrawings[pageno]) + 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 . + reduces anti-aliasing blur but be kept small to minimise printing time """ - sfac = 6.0 + 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. - buffer = wx.Bitmap(width, height) - mdc = wx.MemoryDC(buffer) + # 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) - gc.Translate(0, height) - gc.Scale(sfac, sfac) - self.view.RenderPage(gc, self.view.pagedrawings[pageno]) - dc.DrawBitmap(buffer, 0, 0) - -#------------------------------------------------------------------------------ - -""" -The following has been "borrowed" from from reportlab.pdfbase.pdfutils, -where it is used for testing, because the equivalent function in pyPdf -fails when attempting to decode an embedded JPEG image. -""" - -def _AsciiBase85DecodePYTHON(input): - """ - Decodes input using ASCII-Base85 coding. - - This is not used - Acrobat Reader decodes for you - - but a round trip is essential for testing. - """ - #strip all whitespace - stripped = ''.join(input.split()) - #check end - assert stripped[-2:] == '~>', 'Invalid terminator for Ascii Base 85 Stream' - stripped = stripped[:-2] #chop off terminator - - #may have 'z' in it which complicates matters - expand them - stripped = stripped.replace('z','!!!!!') - # special rules apply if not a multiple of five bytes. - whole_word_count, remainder_size = divmod(len(stripped), 5) - #print('%d words, %d leftover' % (whole_word_count, remainder_size)) - #assert remainder_size != 1, 'invalid Ascii 85 stream!' - cut = 5 * whole_word_count - body, lastbit = stripped[0:cut], stripped[cut:] - - out = [].append - for i in xrange(whole_word_count): - offset = i*5 - c1 = ord(body[offset]) - 33 - c2 = ord(body[offset+1]) - 33 - c3 = ord(body[offset+2]) - 33 - c4 = ord(body[offset+3]) - 33 - c5 = ord(body[offset+4]) - 33 - - num = ((85**4) * c1) + ((85**3) * c2) + ((85**2) * c3) + (85*c4) + c5 - - temp, b4 = divmod(num,256) - temp, b3 = divmod(temp,256) - b1, b2 = divmod(temp, 256) - - assert num == 16777216 * b1 + 65536 * b2 + 256 * b3 + b4, 'dodgy code!' - out(chr(b1)) - out(chr(b2)) - out(chr(b3)) - out(chr(b4)) - - #decode however many bytes we have as usual - if remainder_size > 0: - while len(lastbit) < 5: - lastbit = lastbit + '!' - c1 = ord(lastbit[0]) - 33 - c2 = ord(lastbit[1]) - 33 - c3 = ord(lastbit[2]) - 33 - c4 = ord(lastbit[3]) - 33 - c5 = ord(lastbit[4]) - 33 - num = (((85*c1+c2)*85+c3)*85+c4)*85 + (c5 - +(0,0,0xFFFFFF,0xFFFF,0xFF)[remainder_size]) - temp, b4 = divmod(num,256) - temp, b3 = divmod(temp,256) - b1, b2 = divmod(temp, 256) - assert num == 16777216 * b1 + 65536 * b2 + 256 * b3 + b4, 'dodgy code!' - #print('decoding: %d %d %d %d %d -> %d -> %d %d %d %d' % ( - # c1,c2,c3,c4,c5,num,b1,b2,b3,b4)) - - #the last character needs 1 adding; the encoding loses - #data by rounding the number to x bytes, and when - #divided repeatedly we get one less - if remainder_size == 2: - lastword = chr(b1) - elif remainder_size == 3: - lastword = chr(b1) + chr(b2) - elif remainder_size == 4: - lastword = chr(b1) + chr(b2) + chr(b3) + if mupdf: + self.view.pdfdoc.RenderPage(gc, pageno, sfac) else: - lastword = '' - out(lastword) - - #terminator code for ascii 85 - return ''.join(out.__self__) + gc.Translate(0, height) + gc.Scale(sfac, sfac) + self.view.pdfdoc.RenderPage(gc, pageno) + dc.DrawBitmap(abuffer, 0, 0)