Merge pull request #2480 from jmoraleda/variable-page-size-pdfviewer
Some checks failed
ci-build / build-source-dist (push) Has been cancelled
ci-build / Build wxPython documentation (push) Has been cancelled
ci-build / build-wheels (x64, macos-13, 3.10) (push) Has been cancelled
ci-build / build-wheels (x64, macos-13, 3.11) (push) Has been cancelled
ci-build / build-wheels (x64, macos-13, 3.12) (push) Has been cancelled
ci-build / build-wheels (x64, macos-13, 3.13-dev) (push) Has been cancelled
ci-build / build-wheels (x64, macos-13, 3.8) (push) Has been cancelled
ci-build / build-wheels (x64, macos-13, 3.9) (push) Has been cancelled
ci-build / build-wheels (x64, ubuntu-22.04, 3.10) (push) Has been cancelled
ci-build / build-wheels (x64, ubuntu-22.04, 3.11) (push) Has been cancelled
ci-build / build-wheels (x64, ubuntu-22.04, 3.12) (push) Has been cancelled
ci-build / build-wheels (x64, ubuntu-22.04, 3.13-dev) (push) Has been cancelled
ci-build / build-wheels (x64, ubuntu-22.04, 3.8) (push) Has been cancelled
ci-build / build-wheels (x64, ubuntu-22.04, 3.9) (push) Has been cancelled
ci-build / build-wheels (x64, windows-2022, 3.10) (push) Has been cancelled
ci-build / build-wheels (x64, windows-2022, 3.11) (push) Has been cancelled
ci-build / build-wheels (x64, windows-2022, 3.12) (push) Has been cancelled
ci-build / build-wheels (x64, windows-2022, 3.13-dev) (push) Has been cancelled
ci-build / build-wheels (x64, windows-2022, 3.8) (push) Has been cancelled
ci-build / build-wheels (x64, windows-2022, 3.9) (push) Has been cancelled
ci-build / build-wheels (x86, windows-2022, 3.10) (push) Has been cancelled
ci-build / build-wheels (x86, windows-2022, 3.11) (push) Has been cancelled
ci-build / build-wheels (x86, windows-2022, 3.12) (push) Has been cancelled
ci-build / build-wheels (x86, windows-2022, 3.13-dev) (push) Has been cancelled
ci-build / build-wheels (x86, windows-2022, 3.8) (push) Has been cancelled
ci-build / build-wheels (x86, windows-2022, 3.9) (push) Has been cancelled
ci-build / Publish Python distribution to PyPI (push) Has been cancelled
ci-build / Create GitHub Release and upload source (push) Has been cancelled
ci-build / Upload wheels to snapshot-builds on wxpython.org (push) Has been cancelled

Add support to python only pdfviewer for displaying pdf files where not all pages are the same size
This commit is contained in:
Scott Talbert
2024-09-23 17:16:18 -06:00
committed by GitHub

View File

@@ -23,11 +23,8 @@
This module provides the :class:`~wx.lib.pdfviewer.viewer.pdfViewer` to view PDF This module provides the :class:`~wx.lib.pdfviewer.viewer.pdfViewer` to view PDF
files. files.
""" """
import bisect
import sys import itertools
import os
import time
import types
import copy import copy
import shutil import shutil
from six import BytesIO, string_types from six import BytesIO, string_types
@@ -213,8 +210,8 @@ class pdfViewer(wx.ScrolledWindow):
self.pdfdoc = pypdfProcessor(self, pdf_file, self.ShowLoadProgress) self.pdfdoc = pypdfProcessor(self, pdf_file, self.ShowLoadProgress)
self.numpages = self.pdfdoc.numpages self.numpages = self.pdfdoc.numpages
self.pagewidth = self.pdfdoc.pagewidth self.pagesizes = [self.pdfdoc.GetPageSize(i) for i in range(self.numpages)]
self.pageheight = self.pdfdoc.pageheight
self.page_buffer_valid = False self.page_buffer_valid = False
self.Scroll(0, 0) # in case this is a re-LoadFile self.Scroll(0, 0) # in case this is a re-LoadFile
self.CalculateDimensions() # to get initial visible page range self.CalculateDimensions() # to get initial visible page range
@@ -298,13 +295,20 @@ class pdfViewer(wx.ScrolledWindow):
:param integer `pagenum`: go to the provided page number if it is valid :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] + 1)
else:
self.Scroll(0, 0)
# calling Scroll sometimes doesn't raise wx.EVT_SCROLLWIN eg Windows 8 64 bit - so # calling Scroll sometimes doesn't raise wx.EVT_SCROLLWIN eg Windows 8 64 bit - so
wx.CallAfter(self.Render) wx.CallAfter(self.Render)
if not hasattr(self, "cumYpagespixels"):
return False # This could happen if the file is still loading
if pagenum > 0 and pagenum <= self.numpages:
self.Scroll(0, self.cumYpagespixels[pagenum-1] //
self.GetScrollPixelsPerUnit()[1])
return True
else:
self.Scroll(0, 0)
return False
@property @property
def ShowLoadProgress(self): def ShowLoadProgress(self):
"""Property to control if file reading progress is shown (PyPDF2 only)""" """Property to control if file reading progress is shown (PyPDF2 only)"""
@@ -338,53 +342,73 @@ class pdfViewer(wx.ScrolledWindow):
if not have_cairo: if not have_cairo:
self.font_scale_size = 1.0 / device_scale self.font_scale_size = 1.0 / device_scale
self.winwidth, self.winheight = self.GetClientSize() clientSize = self.GetClientSize()
self.Ypage = self.pageheight + self.nom_page_gap if clientSize.width < 5 or clientSize.height < 5:
return False # Window is too small to render
self.winwidth, self.winheight = clientSize
self.Ypages = [self.pagesizes[pageno][1] + self.nom_page_gap
for pageno in range(self.numpages)]
if self.zoomscale > 0.0: if self.zoomscale > 0.0:
self.scale = self.zoomscale * device_scale self.scales = [self.zoomscale * device_scale]*self.numpages
else: else:
if int(self.zoomscale) == -1: # fit width if int(self.zoomscale) == -1: # fit width
self.scale = self.winwidth / self.pagewidth self.scales = [self.winwidth / self.pagesizes[pageno][0]
for pageno in range(self.numpages)]
else: # fit page else: # fit page
self.scale = self.winheight / self.pageheight self.scales = [self.winheight / self.pagesizes[pageno][1]
if self.scale == 0.0: # this could happen if the window was not yet initialized for pageno in range(self.numpages)]
self.scale = 1.0 self.Xpagespixels = [int(round(self.pagesizes[pageno][0] * self.scales[pageno]))
self.Xpagepixels = int(round(self.pagewidth*self.scale)) for pageno in range(self.numpages)]
self.Ypagepixels = int(round(self.Ypage*self.scale))
# adjust inter-page gap so Ypagepixels is a whole number of scroll increments self.Ypagespixels = [None]*self.numpages
# and page numbers change precisely on a scroll click self.page_gaps = [None]*self.numpages
idiv = self.Ypagepixels // self.scrollrate for pageno in range(self.numpages):
nlo = idiv * self.scrollrate Ypagepixels = int(round(self.Ypages[pageno] * self.scales[pageno]))
nhi = (idiv + 1) * self.scrollrate # adjust Ypagespixels (total number of vertical pixels per page including bottom
if nhi - self.Ypagepixels < self.Ypagepixels - nlo: # inter-page gap so Ypagepixels is a whole number of scroll increments and pages
self.Ypagepixels = nhi # change precisely on a scroll click
else: idiv = Ypagepixels // self.scrollrate
self.Ypagepixels = nlo nlo = idiv * self.scrollrate
self.page_gap = self.Ypagepixels/self.scale - self.pageheight nhi = (idiv + 1) * self.scrollrate
if nhi - Ypagepixels < Ypagepixels - nlo:
self.Ypagespixels[pageno] = nhi
else:
self.Ypagespixels[pageno] = nlo
self.page_gaps[pageno] = (self.Ypagespixels[pageno]/self.scales[pageno] -
self.pagesizes[pageno][1])
self.maxwidth = max(self.winwidth, self.Xpagepixels) self.cumYpagespixels = list(itertools.accumulate(self.Ypagespixels))
self.maxheight = max(self.winheight, self.numpages*self.Ypagepixels)
self.maxwidth = max(self.winwidth,
max(self.Xpagespixels[pageno] for pageno in range(self.numpages)))
self.maxheight = max(self.winheight, self.cumYpagespixels[-1])
self.SetVirtualSize((self.maxwidth, self.maxheight)) self.SetVirtualSize((self.maxwidth, self.maxheight))
self.SetScrollRate(self.scrollrate, self.scrollrate) self.SetScrollRate(self.scrollrate, self.scrollrate)
xv, yv = self.GetViewStart() xv, yv = self.GetViewStart()
dx, dy = self.GetScrollPixelsPerUnit() dx, dy = self.GetScrollPixelsPerUnit()
self.x0, self.y0 = (xv * dx, yv * dy) self.x0, self.y0 = (xv * dx, yv * dy)
self.frompage = int(min(self.y0/self.Ypagepixels, self.numpages-1)) self.frompage = max(bisect.bisect_left(self.cumYpagespixels, self.y0+1), 0)
self.topage = int(min((self.y0+self.winheight-1)/self.Ypagepixels, self.numpages-1)) self.topage = min(bisect.bisect_right(self.cumYpagespixels, self.y0+self.winheight-1),
self.pagebufferwidth = max(self.Xpagepixels, self.winwidth) self.numpages-1)
self.pagebufferheight = (self.topage - self.frompage + 1) * self.Ypagepixels if self.frompage > self.topage:
return False # Nothing to render. Can happen during initialization
self.page_x0 = 0
self.pagebufferwidth = max(self.Xpagespixels[pageno]
for pageno in range(self.frompage, self.topage+1))
self.page_y0 = self.cumYpagespixels[self.frompage-1] if self.frompage else 0
self.pagebufferheight = self.cumYpagespixels[self.topage] - self.page_y0
# Inform buttonpanel controls of any changes # Inform buttonpanel controls of any changes
if self.buttonpanel: if self.buttonpanel:
self.buttonpanel.Update(self.frompage, self.numpages, self.buttonpanel.Update(self.frompage, self.numpages,
self.scale/device_scale) self.scales[self.frompage]/device_scale)
self.page_y0 = self.frompage * self.Ypagepixels
self.page_x0 = 0
self.xshift = self.x0 - self.page_x0 self.xshift = self.x0 - self.page_x0
self.yshift = self.y0 - self.page_y0 self.yshift = self.y0 - self.page_y0
if not self.page_buffer_valid: # via external setting if not self.page_buffer_valid: # via external setting
self.cur_frompage = self.frompage self.cur_frompage = self.frompage
self.cur_topage = self.topage self.cur_topage = self.topage
@@ -393,7 +417,8 @@ class pdfViewer(wx.ScrolledWindow):
self.page_buffer_valid = False # due to page buffer change self.page_buffer_valid = False # due to page buffer change
self.cur_frompage = self.frompage self.cur_frompage = self.frompage
self.cur_topage = self.topage self.cur_topage = self.topage
return
return True
def Render(self): def Render(self):
""" """
@@ -407,7 +432,8 @@ class pdfViewer(wx.ScrolledWindow):
""" """
if not self.have_file: if not self.have_file:
return return
self.CalculateDimensions() if not self.CalculateDimensions():
return # Invalid dimensions: Nothing to render
if not self.page_buffer_valid: if not self.page_buffer_valid:
# Initialize the buffer bitmap. # Initialize the buffer bitmap.
self.pagebuffer = wx.Bitmap(self.pagebufferwidth, self.pagebufferheight) self.pagebuffer = wx.Bitmap(self.pagebufferwidth, self.pagebufferheight)
@@ -423,8 +449,12 @@ class pdfViewer(wx.ScrolledWindow):
gc.FillPath(path) gc.FillPath(path)
for pageno in range(self.frompage, self.topage+1): for pageno in range(self.frompage, self.topage+1):
scale = self.scales[pageno]
pagegap = self.page_gaps[pageno]
self.xpageoffset = 0 - self.x0 self.xpageoffset = 0 - self.x0
self.ypageoffset = pageno*self.Ypagepixels - self.page_y0 self.ypageoffset = (self.cumYpagespixels[pageno] - self.Ypagespixels[pageno] -
self.page_y0)
gc.PushState() gc.PushState()
if mupdf: if mupdf:
gc.Translate(self.xpageoffset, self.ypageoffset) gc.Translate(self.xpageoffset, self.ypageoffset)
@@ -432,22 +462,27 @@ class pdfViewer(wx.ScrolledWindow):
else: else:
gc.Translate(self.xpageoffset, self.ypageoffset + gc.Translate(self.xpageoffset, self.ypageoffset +
self.pageheight*self.scale) self.pagesizes[pageno][1]*scale)
gc.Scale(self.scale, self.scale) gc.Scale(scale, scale)
self.pdfdoc.RenderPage(gc, pageno, scale=self.scale) self.pdfdoc.RenderPage(gc, pageno, scale=scale)
# Show inter-page gap
gc.SetBrush(wx.Brush(wx.Colour(180, 180, 180))) #mid grey # Show non-page areas as gray
gc.PushState()
gc.SetBrush(wx.Brush(self.GetBackgroundColour()))
gc.SetPen(wx.TRANSPARENT_PEN) gc.SetPen(wx.TRANSPARENT_PEN)
if mupdf: gc.Scale(1.0, 1.0)
gc.DrawRectangle(0, self.pageheight*self.scale,
self.pagewidth*self.scale, self.page_gap*self.scale) #inter-page gap
else: gc.DrawRectangle(0, self.pagesizes[pageno][1]*scale,
gc.DrawRectangle(0, 0, self.pagewidth, self.page_gap) self.pagesizes[pageno][0]*scale, pagegap*scale)
gc.PopState() # gap to the right of the page
gc.PushState() extrawidth = self.winwidth - self.Xpagespixels[pageno]
gc.Translate(0-self.x0, 0-self.page_y0) if extrawidth > 0:
self.RenderPageBoundaries(gc) gc.DrawRectangle(self.pagesizes[pageno][0]*scale, 0,
gc.PopState() extrawidth, self.Ypagespixels[pageno])
gc.PopState() # Pop non-page area
gc.PopState() # Pop page area
self.page_buffer_valid = True self.page_buffer_valid = True
self.Refresh(0) # Blit appropriate area of new or existing page buffer to screen self.Refresh(0) # Blit appropriate area of new or existing page buffer to screen
@@ -457,20 +492,6 @@ class pdfViewer(wx.ScrolledWindow):
self.GoPage(self.page_after_zoom_change) self.GoPage(self.page_after_zoom_change)
self.page_after_zoom_change = None self.page_after_zoom_change = None
def RenderPageBoundaries(self, gc):
"""
Show non-page areas in grey.
"""
gc.SetBrush(wx.Brush(wx.Colour(180, 180, 180))) #mid grey
gc.SetPen(wx.TRANSPARENT_PEN)
gc.Scale(1.0, 1.0)
extrawidth = self.winwidth - self.Xpagepixels
if extrawidth > 0:
gc.DrawRectangle(self.winwidth-extrawidth, 0, extrawidth, self.maxheight)
extraheight = self.winheight - (self.numpages*self.Ypagepixels - self.y0)
if extraheight > 0:
gc.DrawRectangle(0, self.winheight-extraheight, self.maxwidth, extraheight)
#============================================================================ #============================================================================
class mupdfProcessor(object): class mupdfProcessor(object):
@@ -503,15 +524,18 @@ class mupdfProcessor(object):
self.numpages = self.pdfdoc.page_count self.numpages = self.pdfdoc.page_count
except AttributeError: # old PyMuPDF version except AttributeError: # old PyMuPDF version
self.numpages = self.pdfdoc.pageCount self.numpages = self.pdfdoc.pageCount
try:
page = self.pdfdoc.load_page(0)
except AttributeError: # old PyMuPDF version
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
self.zoom_error = False #set if memory errors during render
def GetPageSize(self, pageNum):
""" Return width, height for the page """
try:
page = self.pdfdoc.load_page(pageNum)
except AttributeError: # old PyMuPDF version
page = self.pdfdoc.loadPage(pageNum)
bound = page.bound()
return bound.width, bound.height
def DrawFile(self, frompage, topage): def DrawFile(self, frompage, topage):
""" """
This is a no-op for mupdf. Each page is scaled and drawn on This is a no-op for mupdf. Each page is scaled and drawn on
@@ -555,9 +579,6 @@ class pypdfProcessor(object):
self.showloadprogress = showloadprogress self.showloadprogress = showloadprogress
self.pdfdoc = PdfFileReader(fileobj) self.pdfdoc = PdfFileReader(fileobj)
self.numpages = self.pdfdoc.getNumPages() 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.pagedrawings = {}
self.unimplemented = {} self.unimplemented = {}
self.formdrawings = {} self.formdrawings = {}
@@ -566,6 +587,10 @@ class pypdfProcessor(object):
self.saved_state = None self.saved_state = None
self.knownfont = False self.knownfont = False
self.progbar = None self.progbar = None
def GetPageSize(self, pageNum):
mediaBox = self.pdfdoc.getPage(pageNum).mediaBox
return float(mediaBox.getUpperRight_x()), float(mediaBox.getUpperRight_y())
# These methods interpret the PDF contents as a set of drawing commands # These methods interpret the PDF contents as a set of drawing commands
@@ -1078,8 +1103,8 @@ class pdfPrintout(wx.Printout):
if mupdf: if mupdf:
sfac = 4.0 sfac = 4.0
pageno = page - 1 # zero based pageno = page - 1 # zero based
width = self.view.pagewidth width = self.view.pagesizes[pageno][0]
height = self.view.pageheight height = self.view.pagesizes[pageno][1]
self.FitThisSizeToPage(wx.Size(int(width*sfac), int(height*sfac))) self.FitThisSizeToPage(wx.Size(int(width*sfac), int(height*sfac)))
dc = self.GetDC() dc = self.GetDC()
gc = wx.GraphicsContext.Create(dc) gc = wx.GraphicsContext.Create(dc)