Skip to content

Commit

Permalink
Refactor python only pdfviewer to support displaying pdf files where …
Browse files Browse the repository at this point in the history
…not all pages have the same size
  • Loading branch information
jmoraleda committed Sep 21, 2024
1 parent b2733b5 commit 2dc886a
Showing 1 changed file with 109 additions and 84 deletions.
193 changes: 109 additions & 84 deletions wx/lib/pdfviewer/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@
This module provides the :class:`~wx.lib.pdfviewer.viewer.pdfViewer` to view PDF
files.
"""

import sys
import os
import time
import types
import bisect
import itertools
import copy
import shutil
from six import BytesIO, string_types
Expand Down Expand Up @@ -213,8 +210,8 @@ def create_fileobject(filename):
self.pdfdoc = pypdfProcessor(self, pdf_file, self.ShowLoadProgress)

self.numpages = self.pdfdoc.numpages
self.pagewidth = self.pdfdoc.pagewidth
self.pageheight = self.pdfdoc.pageheight
self.pagesizes = [self.pdfdoc.GetPageSize(i) for i in range(self.numpages)]

self.page_buffer_valid = False
self.Scroll(0, 0) # in case this is a re-LoadFile
self.CalculateDimensions() # to get initial visible page range
Expand Down Expand Up @@ -298,12 +295,19 @@ def GoPage(self, pagenum):
:param integer `pagenum`: go to the provided page number if it is valid
"""
# calling Scroll sometimes doesn't raise wx.EVT_SCROLLWIN eg Windows 8 64 bit - so
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, pagenum * self.Ypagepixels // self.GetScrollPixelsPerUnit()[1] + 1)
self.Scroll(0, self.cumYpagespixels[pagenum-1] //
self.GetScrollPixelsPerUnit()[1])
return True
else:
self.Scroll(0, 0)
# calling Scroll sometimes doesn't raise wx.EVT_SCROLLWIN eg Windows 8 64 bit - so
wx.CallAfter(self.Render)
return False

@property
def ShowLoadProgress(self):
Expand Down Expand Up @@ -338,53 +342,73 @@ def CalculateDimensions(self):
if not have_cairo:
self.font_scale_size = 1.0 / device_scale

self.winwidth, self.winheight = self.GetClientSize()
self.Ypage = self.pageheight + self.nom_page_gap
clientSize = self.GetClientSize()
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:
self.scale = self.zoomscale * device_scale
self.scales = [self.zoomscale * device_scale]*self.numpages
else:
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
self.scale = self.winheight / self.pageheight
if self.scale == 0.0: # this could happen if the window was not yet initialized
self.scale = 1.0
self.Xpagepixels = int(round(self.pagewidth*self.scale))
self.Ypagepixels = int(round(self.Ypage*self.scale))

# adjust inter-page gap so Ypagepixels is a whole number of scroll increments
# and page numbers change precisely on a scroll click
idiv = self.Ypagepixels // self.scrollrate
nlo = idiv * self.scrollrate
nhi = (idiv + 1) * self.scrollrate
if nhi - self.Ypagepixels < self.Ypagepixels - nlo:
self.Ypagepixels = nhi
else:
self.Ypagepixels = nlo
self.page_gap = self.Ypagepixels/self.scale - self.pageheight
self.scales = [self.winheight / self.pagesizes[pageno][1]
for pageno in range(self.numpages)]
self.Xpagespixels = [int(round(self.pagesizes[pageno][0] * self.scales[pageno]))
for pageno in range(self.numpages)]

self.Ypagespixels = [None]*self.numpages
self.page_gaps = [None]*self.numpages
for pageno in range(self.numpages):
Ypagepixels = int(round(self.Ypages[pageno] * self.scales[pageno]))
# adjust Ypagespixels (total number of vertical pixels per page including bottom
# inter-page gap so Ypagepixels is a whole number of scroll increments and pages
# change precisely on a scroll click
idiv = Ypagepixels // self.scrollrate
nlo = idiv * self.scrollrate
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.maxheight = max(self.winheight, self.numpages*self.Ypagepixels)
self.cumYpagespixels = list(itertools.accumulate(self.Ypagespixels))

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.SetScrollRate(self.scrollrate, self.scrollrate)

xv, yv = self.GetViewStart()
dx, dy = self.GetScrollPixelsPerUnit()

self.x0, self.y0 = (xv * dx, yv * dy)
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
self.frompage = max(bisect.bisect_left(self.cumYpagespixels, self.y0+1), 0)
self.topage = min(bisect.bisect_right(self.cumYpagespixels, self.y0+self.winheight-1),
self.numpages-1)
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
if self.buttonpanel:
self.buttonpanel.Update(self.frompage, self.numpages,
self.scale/device_scale)

self.page_y0 = self.frompage * self.Ypagepixels
self.page_x0 = 0
self.scales[self.frompage]/device_scale)

self.xshift = self.x0 - self.page_x0
self.yshift = self.y0 - self.page_y0

if not self.page_buffer_valid: # via external setting
self.cur_frompage = self.frompage
self.cur_topage = self.topage
Expand All @@ -393,7 +417,8 @@ def CalculateDimensions(self):
self.page_buffer_valid = False # due to page buffer change
self.cur_frompage = self.frompage
self.cur_topage = self.topage
return

return True

def Render(self):
"""
Expand All @@ -407,7 +432,8 @@ def Render(self):
"""
if not self.have_file:
return
self.CalculateDimensions()
if not self.CalculateDimensions():
return # Invalid dimensions: Nothing to render
if not self.page_buffer_valid:
# Initialize the buffer bitmap.
self.pagebuffer = wx.Bitmap(self.pagebufferwidth, self.pagebufferheight)
Expand All @@ -423,31 +449,40 @@ def Render(self):
gc.FillPath(path)

for pageno in range(self.frompage, self.topage+1):
scale = self.scales[pageno]
pagegap = self.page_gaps[pageno]
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()
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, scale=self.scale)
# Show inter-page gap
gc.SetBrush(wx.Brush(wx.Colour(180, 180, 180))) #mid grey
self.pagesizes[pageno][1]*scale)
gc.Scale(scale, scale)
self.pdfdoc.RenderPage(gc, pageno, scale=scale)

# Show non-page areas as gray
gc.PushState()
gc.SetBrush(wx.Brush(self.GetBackgroundColour()))
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)
gc.PopState()
gc.Scale(1.0, 1.0)

#inter-page gap
gc.DrawRectangle(0, self.pagesizes[pageno][1]*scale,
self.pagesizes[pageno][0]*scale, pagegap*scale)
# gap to the right of the page
extrawidth = self.winwidth - self.Xpagespixels[pageno]
if extrawidth > 0:
gc.DrawRectangle(self.pagesizes[pageno][0]*scale, 0,
extrawidth, self.Ypagespixels[pageno])
gc.PopState() # Pop non-page area

gc.PopState() # Pop page area

self.page_buffer_valid = True
self.Refresh(0) # Blit appropriate area of new or existing page buffer to screen
Expand All @@ -457,20 +492,6 @@ def Render(self):
self.GoPage(self.page_after_zoom_change)
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):
Expand Down Expand Up @@ -503,15 +524,18 @@ def __init__(self, parent, pdf_file):
self.numpages = self.pdfdoc.page_count
except AttributeError: # old PyMuPDF version
self.numpages = self.pdfdoc.pageCount

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(0)
page = self.pdfdoc.load_page(pageNum)
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

page = self.pdfdoc.loadPage(pageNum)
bound = page.bound()
return bound.width, bound.height

def DrawFile(self, frompage, topage):
"""
This is a no-op for mupdf. Each page is scaled and drawn on
Expand Down Expand Up @@ -555,9 +579,6 @@ def __init__(self, parent, fileobj, showloadprogress):
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 = {}
Expand All @@ -566,6 +587,10 @@ def __init__(self, parent, fileobj, showloadprogress):
self.saved_state = None
self.knownfont = False
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

Expand Down Expand Up @@ -1078,8 +1103,8 @@ def OnPrintPage(self, page):
if mupdf:
sfac = 4.0
pageno = page - 1 # zero based
width = self.view.pagewidth
height = self.view.pageheight
width = self.view.pagesizes[pageno][0]
height = self.view.pagesizes[pageno][1]
self.FitThisSizeToPage(wx.Size(int(width*sfac), int(height*sfac)))
dc = self.GetDC()
gc = wx.GraphicsContext.Create(dc)
Expand Down

0 comments on commit 2dc886a

Please sign in to comment.