Ora che sono state viste le "particolarità" derivate dai miei vezzi, passiamo all'effettivo esame del prototipo di visualizzatore di PDF
L'interfaccia utente
La finestra del visualizzatore, di per se, è molto semplice, si tratta essenzialmente di un canvas, un paio di barre di scorrimento, un insieme di pulsanti, ed una entry, per fare alcune elementari operazioni ed un paio di frame a far da pannelli contenitore.
Riguardo ai controlli dedicati alle operazioni di comando, possono essere così schematizzati :
- Controlli di navigazione - una entry per indicare direttamente il numero di pagina che si vuole visualizzare e quattro pulsante per spostarsi rispettivamente, andando dalla vostra sinistra a destra, alla prima pagina, alla pagina precedente, alla pagina successiva, all'ultima pagina, quando si inserisce un numero nella entry bisogna premere "Invio" perché lo spostamento venga eseguito;
- Comandi di rotazione - per ruotare di 90° la pagina verso sinistra o verso destra, gli angoli considerati al momento sono 0, 90, 180 e 270, la rotazione è applicata a tutte le pagine del documento;
- Comandi di zoom - Incrementano/decrementano del 10% le dimensioni delle pagine visualizzate ovvero adattano la pagina all'area disponibile (condizione di default) il range di impostazione va dal 10% al 400%, sempre da sinistra a destra : 1° decremento, 2° incremento, 3° switch Adatta/Dimensioni reali;
- Pulsante informazioni - fa comparire una finestra di dialogo con le informazioni generali lette dal documento
- Pulsante chiusura finestra - ... basta la parola .
I comandi relativi alla navigazione, rotazione e zoom hanno immediato riflesso sull'immagine esposta che viene ridefinita e mostrata nel canvas dedicato alla visualizzazione.
Come funzionano le cose
Ribadisco che ora si sta trattando un prototipo preliminare di fattibilità per la sola visualizzazione, al momento molto viene ignorato e tutti i processi avvengono "dentro" la finestra stessa, nel senso che sono implementati all'interno della classe PdfViewer, sub-classamento di un oggetto
TopLevel di tkinter, ciò al momento, per un utilizzo "operativo" le cose cambieranno.
PdfViewer espone il metodo "
Set_File(f_pathname)" che permette ad un processo esterno di impostare il path-name del file da visualizzare, in tale metodo viene verificata l'effettiva esistenza del file indicato che, se trovato, viene memorizzato quale documento corrente per l'istanza utilizzata, niente vieta che PdfViewer possa avere istanze concorrenti ma, al momento, la cosa darebbe problemi perché viene ripulita la directory dedicata alla cache delle immagini di pagina dei documenti, prima di dare il documento ricevuto in pasto
pdf2image per l'estrazione delle informazioni sul documento, tramite la funzione
pdf2info.pdfinfo_from_path(path_name), che vengono memorizzate. Immediatamente dopo ripulita una eventual cache ("
PdfViewer._clear_cache()") presente e chiamato il metodo della classe "
_pdf_cache()"
CODICE
def _pdf_cache(self):
self.pdf_images = []
for p in range(0, self.pages+1, 10):
images = p2i.convert_from_path(self.doc,
first_page=p, last_page=min(p+10-1, self.pages),
fmt='jpeg', output_folder='my_tmp/pdfcache')
for i in images:
self.pdf_images.append(i)
È il caso di dare una guardata a questo codice.
In primo luogo possiamo vedere che viene definita una proprietà della istanza corrente di PdfViewer, "
self.pdf_images" che è una lista delle immagini generate da pdf2image, Dette immagini sono memorizzare su file, resi temporanei dalla applicazione, memorizzati nella direttrice di cache definita ("<appdir>/my_tmp/pdfcache" al momento).
In merito alla generazione dei files immagine, osservate il ciclo for : vengono generate 10 pagine alla volta ... alla pagina del
progetto pdf2image, proprio in fondo, è scritto :
Un PDF relativamente grande consumerà tutta la tua memoria e causerà l'arresto del processo (a meno che tu non usi una cartella di output) (scusate la dozzinale traduzione)
Or bene, sono decenni che trasformo pagine PDF in immagini, utilizzando imagemagik per lo più, utilizzando processi analoghi, ho rilevato che quando i files pdf sono "grossi" immancabilmente arriva il crash, la memoria è molto più veloce dell'output su disco, dopo numerose prove ho determinato che operare su una decina di pagine alla volta è un buon compromesso, liberi di fare Vostre scelte ma io mi attengo a tale criterio anche avendo stabilito un output_folder.
Comunque, tralasciando la disgressione, viene utilizzata la funzione "
pdf2image.convert_from_path()" per la conversione delle singole pagine in immagini, in vero, si "potrebbe" estrarre una singola pagina mantenendola in memoria, con "
pdf2image.convert_from_bytes()", ci ho provato e funziona ma i tempi di attesa sono inaccettabilmente lunghi.
Dal punto di vista funzionale la finestra, al momento, si limita a visualizzare la pagina corrente, riposizionando controlli ed, eventualmente, il contenuto in caso di ridimensionamento della finestra. Tale riposizionamento viene effettuato, nel caso dei controlli, sfruttando la proprietà "
weight" di righe e colonne definite tramite il
gestore di geometria grid, nel metodo "
_populate(self)" della classe PdfViewer :
CODICE
...
p_tools.grid_columnconfigure(7, weight=1)
p_tools.grid_columnconfigure(10, weight=1)
p_tools.grid_columnconfigure(14, weight=1)
p_tools.grid_columnconfigure(16, weight=1)
...
p_img.grid_rowconfigure(0, weight=1)
p_img.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
...
essa viene applicata specificatamente ai vari pannelli (frame) per le proprie esigenze di gestione.
Da tener presente che l'immagine della pagina corrente, in caso si sia nella condizione di adattamento alla dimensione finestra (self.adapt = True), viene ridisegnata adattandola alle dimensioni del canvas contenitore.
... In merito al "ridisegno" della immagine, c'è da dire che in un primo momento lo avevo implementato il maniera tale che si verificasse al ridimensionamento del canvas che la rappresenta, constatando un ritardo di riposta nella riconfigurazione dei controlli della finestra, principalmente riguardo ai pulsanti di zoom e rotazione, tant'è che ho provato a ritardarne il ridisegno nelle rispettive istruzioni di callback (self.after(200, self._show_page)), senza grossi risultati.
Empiricamente, sono giunto alla conclusione che variando le dimensioni della immagine, cosa che avviene tanto zoommando, quanto ruotando, anche le dimensioni del canvas si modificano, scatenando un nuovo evento "<configure>" sul canvas, con conseguente nuovo ridisegno ... sarà giusto?, non ne sono certo ma eliminando detto binder dal canvas e spostandolo sul ridimensionamento della finestra i problemi "di ridisegno" dei controlli sono svaniti. Ho lasciato, commentata nel codice, l'istruzione del binding per il ridimensionamento del canvas, nel caso qualcuno volesse effettuare delle prove.
In merito a rotazioni, zoom ed adattamento, essi sono controllate da una serie di variabili di istanza definite nello "__init__" della classe
CODICE
...
class PdfViewer(tk.Toplevel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title('tkPDFViewer => Nessun File')
self.doc = ''
self.pages = 0
self.page = 0
self.adapt = True
self.zoom = 0.0
self.rotation = 0
self._populate()
...
Ve ne sono varie, come si può vedere, banalmente, "
self.pages" e "
self.page" sono destinate a contenere il numero complessivo di pagine e la pagina corrente del documento, "
self.adapt" controlla l'adattamento dell'immagine alla finestra, "
self.zoom" e "
self.roration" stabiliscono, rispettivamente, i fattori di zoom e rotazione da applicare alla immagine originale.
le variabili relative alle pagine vengono definite/utilizzate in svariati punti della classe, le variabili di zoom e rotazione utilizzate solo nel metodo "
self._show_page(self)".
Vi è, inoltre, un'altra variabile di istanza "importante" di cui abbiamo già parlato : "
self.pdf_images"
Navigazione pagine documento
Piuttosto scontato come metodo, avviene tramite l'utilizzo di quattro pulsanti, "
self.bt_first", "
self.bt_previous", "
self.bt_next", "
self.bt_last", ed una casella di immissione testo, "
self.e_page", la definizione di tali controlli quale variabili di istanza non è strettamente necessaria ma è comoda sotto vari aspetti, tutti tali controlli hanno il metodo "
self._set_page(self, evt)" quale callback dei loro binding
CODICE
...
def _set_page(self, evt):
if not self.doc or not self.pdf_images: return
w_name = str(evt.widget).split('.')[-1]
if w_name == 'bt_first':
self.page = 0
elif w_name == 'bt_previous':
self.page -= 1
if self.page < 0: self.page = 0
elif w_name == 'e_page':
try:
value = int(self.e_page.get())
if 0 <= (value - 1) <= self.pages - 1:
self.page = value - 1
else:
raise ValueError('Inserimento non valido')
except ValueError:
message = 'Numero di pagina non valido'
msgb.showinfo('Selezione pagina', message)
return
elif w_name == 'bt_next':
self.page += 1
if self.page > self.pages-1: self.page = self.pages -1
elif w_name == 'bt_last':
self.page = self.pages - 1
self._show_page()
...
Da notare come l'algoritmo applicato si basi sul nome ricavato dal descrittore dell'oggetto scatenante l'evento per decidere la pagina da rendere "corrente" prima di invocarne il ridisegno. Si è provveduto ad assegnare il "nome" ai controlli al momento della loro istanza.
Data l'intrinseca semplicità del codice non ritengo vi sia bisogno di ulteriori spiegazioni.
L'immagine sottostante mostra l'effetto del pulsante di spostamento alla pagina successiva nelle condizioni di default per la visualizzazione
Rotazione pagine
Anche questa è una funzione molto semplice, due pulsanti, "
self.bt_r_left" e "
self.bt_r_right", per rotazione a destra o a sinistra, in realtà ne sarebbe stato sufficiente uno, entrambi riferenti al metodo "
self._rotate(self, evt)" per i callback
CODICE
...
def _rotate(self, evt):
w_name = str(evt.widget).split('.')[-1]
if w_name == 'bt_r_left':
self.rotation += 90
elif w_name == 'bt_r_right':
self.rotation -= 90
if self.rotation > 270:
self.rotation = 0
elif self.rotation < 0:
self.rotation = 270
self.after(200, self._show_page)
...
Nuovamente, è il nome del widget scatenante l'evento a determinare l'angolo di rotazione da applicare all'immagine, espresso in gradi sessagesimali, ogni singola pressione varia di +/- 90° l'angolo ri rotazione corrente contenendolo nel range di valori ammessi : 0, 90, 180 e 270.
L'angolo di rotazione definito è, per il momento, applicato alla visualizzazionwe tutte le pagine di un documento.
L'immagine sottostante mostra l'effetto di una rotazione a sinistra applicata alla precedente immagine
Adattamento e zoom della visualizzazione
Per lo zoom delle pagine, sono dedicati i pulsanti del terzo gruppo in figura all'inizio di questo post, i tre pulsanti, dalla vostra sinistra andando verso destra, consentono, i primi due rispettivamente, di ridurre del 10% la dimensione dell'immagine e di aumentarla del 10%, entro un range che va dal 10% al 400% della dimensione effettiva.
La funzione di callback che entra in azione è :
CODICE
def _set_zoom(self, evt):
w_name = str(evt.widget).split('.')[-1]
if w_name == 'bt_zout':
self.zoom -= 0.1
if self.zoom < -0.9:
self.zoom = -0.9
return
elif w_name == 'bt_zin':
self.zoom += 0.1
if self.zoom > 3.00:
self.zoom = 3.00
return
self.bt_zout.configure(state='disabled')
self.bt_zin.configure(state='disabled')
self.after(200, self._show_page)
self.bt_zout.configure(state='normal')
self.bt_zin.configure(state='normal')
ancora una volta, nella definizione dell'azione da intraprendere, entra in gioco il nome del widget ... ma notate le ultime 5 righe, decisamente "strane", esse sono "frutto" di tentativi di controllo del ridisegno dei widget nella predisposizione iniziale con binding sul ridimensionamento del canvas, indicato in precedenza, contenente l'immagine : i pulsanti restavano "premuti" per eccessivo ritardo causato dai molteplici cicli di re-paint che si scatenavano.
Ho lasciato li quel codice nel caso ci si voglia provare ma sparirà, l'istruzione "
self.after(200, self._show_page)! (che inserisce un ritardo di 200 millisecondi) dovrebbe essere sufficiente.
Comunque, sotto vedete l'effetto dell'immagine ruotata precedente con qualche fattore di zoom applicato
Entrambi i pulsanti appena trattati vengono abilitati o disabilitati dall'azione del terzo pulsante, quest'ultimo è una specie di switch, modifica la condizione della variabile di istanza "
self.adapt" da
False a
True e viceversa, quanto detta variabile è vera i due pulsanti di zoom vengono disabilitati, abilitati alrimenti
CODICE
...
def _adapt(self):
self.adapt = not self.adapt
if self.adapt:
self.zoom = 0.0
self.bt_zout.configure(state='disabled')
self.bt_zin.configure(state='disabled')
else:
self.bt_zout.configure(state='normal')
self.bt_zin.configure(state='normal')
self._show_page()
...
noterete senz'altro che la variabile di istanza "
self.zoom" viene azzerata, non ha molto senso parlare di zoom quando l'immagine viene adattata dinamicamente ad una superficie variabile. Per altro passando alla visualizzazione a "dimensione fissa" (fatte salve le zoommate) l'immagine viene visualizzata alla sua dimensione "effettiva" che è quella della dimensione pagina (probabilmente A4) a 200 DPI di risoluzione
... piuttosto grande, non trovate
Applicazione delle impostazioni date
Vi sarete senz'altro accorti che quasi tutti i metodi di callback visti includono una chiamata al metodo di classe "
self._show_page()", detto metodo è il "cuore" del visualizzatore
CODICE
def _show_page(self):
self.config(cursor='watch')
self.update()
self.cnv_img.delete('all')
image = self.pdf_images[self.page]
# eventuale rotazione
if self.rotation:
image = image.rotate(self.rotation, expand=True)
img_x, img_y = image.size
if self.adapt:
# calcola i rapporti di scala
r_img = img_x / img_y
r_cnv = self.cnv_img.winfo_width() / self.cnv_img.winfo_height()
if r_cnv <= r_img:
f_scala = self.cnv_img.winfo_width() / img_x
else:
f_scala = self.cnv_img.winfo_height() / img_y
fx = int(img_x * f_scala)
fy = int(img_y * f_scala)
image = image.resize((fx, fy))
x = self.cnv_img.winfo_width() // 2
y = self.cnv_img.winfo_height() // 2
x_region = self.cnv_img.winfo_width()
y_region = self.cnv_img.winfo_height()
elif self.zoom != 0.0:
f_x = int(img_x * (1 + self.zoom))
f_y = int(img_y * (1 + self.zoom))
image = image.resize((f_x, f_y))
x = f_x // 2
y = f_y // 2
x_region = f_x
y_region = f_y
else:
x = img_x // 2
y = img_y // 2
x_region = img_x
y_region = img_y
self.img = ImageTk.PhotoImage(image)
self.cnv_img.create_image(x, y, image=self.img, anchor='center')
self.cnv_img.configure(scrollregion=(0, 0, x_region, y_region))
self.cnv_img.update()
self.e_page.delete(0, tk.END)
self.e_page.insert(0, '%d' % (self.page + 1))
self.e_page.update()
self.config(cursor='')
self.update()
malgrado la sua "lunghezza", occupata più che altro dal calcolo delle impostazioni di visualizzazione, è piuttosto semplice limitandosi ad ottenere l'immagine di indice corrente, ruotarla se è il caso ed, eventualmente, ridimensionarla.
Le "immagini" fornite dal
pdf2image hanno tutto il necessaire per le operazioni di ridimensionamento e rotazione occorrenti, essenzialmente presentano le caratteristiche funzionali di un oggetto Image di PIL (anche se non completamente), l'accorgimento da tener presente è la conversione ad immagine compatibile con tkinter tramite l'istruzione "
self.img = ImageTk.PhotoImage(image)"
Ultima piccolezza
Gli ultimi due pulsanti sono piuttosto banali, quello etichettato "4" nella prima figura del post espone le informazioni sul documento aperto, raccolte da pdf2image, l'ultimo procede a chiudere la finestra, ripulendo la cache e distruggendo la finestra
... badate bene, chiude la finestra
NON l'applicazione, la GUI di cui qui si discute è un prototipo mirato ad essere integrato in un contesto più ampio che si raggiungerà (spero) tra molti post, dopo un lungo percorso.
Torniamo un pochino sul "pulsante 4", con un certo raccapriccio, lo confesso, ho utilizzato i dialoghi standard di tkinter, trovandoli visivamente molto sgradevoli nel mio sistema e molto poco "maneggiabili", definita la funzione "
self._show_info(self)" associata al command del pulsante
CODICE
...
def _show_info(self):
message = ''
maxsize = 0
for key in self.info.keys():
if len(key) > maxsize: maxsize = len(key)
for key in self.info.keys():
message += key + ' '*(maxsize-len(key)) + ' : ' + str(self.info[key]) + '\n'
msgb.showinfo('File aperto con successo', message)
...
ho avuto questa raccapricciante resa visiva nel mio desk (gnome3 su ubuntu 20.04)
Noterete, dal codice, che ho cercato di allineare i valori informativi alla massima lunghezza delle chiavi di informazione ... con risultati ben miseri, il massimo che mi è riuscito ad ottenere è questo :
ottenuto dando la direttiva
CODICE
...
# configuro font e lunghezza messaggi per le msgbox
self.option_add('*Dialog.msg.font', 'Courier 9')
...
nella inizializzazione della finestra, nel codice che seguirà alla fine del post troverete un paio di insoddisfacenti tentativi per allargare la label del messaggio : niente da fare! Quel maledetto Title va sempre a capo allo stesso punto e non c'è verso (beh, non lo ho trovato) di sub-classare la finestra di dialogo e manipolarne l'output (cosa che in wx si può fare)
In futuro, per tkinter, costruirò delle finestre di dialogo personali!
Per finire, il sorgete intero del pdfviewer:
CODICE
# -*- coding: utf-8 -*-
import tkinter as tk
import tkinter.messagebox as msgb
import viewer_ico_and_tip as IaT
from my_tk_object import CreaToolTip as ctt
import pdf2image as p2i
import os
import glob
from PIL import ImageTk, Image
from io import BytesIO
# images = p2i.convert_from_bytes(open('pdf_file', 'rb').read())
class PdfViewer(tk.Toplevel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title('tkPDFViewer => Nessun File')
self.doc = ''
self.pages = 0
self.page = 0
self.adapt = True
self.zoom = 0.0
self.rotation = 0
self._populate()
# configuro font e lunghezza messaggi per le msgbox
self.option_add('*Dialog.msg.font', 'Courier 9')
#self.option_add('*Dialog.msg.width', 100)
#self.option_add('*Dialog.msg.wraplength', '24i')
def _populate(self):
# TOOLS-BAR
p_tools = tk.Frame(self)
p_tools.grid(row=0, column=0, sticky='ew')
lbl = tk.Label(p_tools, text='Pagina:', justify='left')
lbl.grid(row=0, column=0, padx=5, pady=5)
ids = IaT.IcoDispencer()
# controlli navigazione pagine
self.ico_pgfirst = tk.PhotoImage(data=ids.getIco('pg_first'))
self.bt_first = tk.Button(p_tools, image=self.ico_pgfirst, name='bt_first')
bt_first_ttp = ctt(self.bt_first, ids.getDescr('pg_first'))
self.bt_first.grid(row=0, column=1, padx=5, pady=5, sticky='nsew')
self.ico_pgleft = tk.PhotoImage(data=ids.getIco('pg_left'))
self.bt_previous = tk.Button(p_tools, image=self.ico_pgleft, name='bt_previous')
bt_previous_ttp = ctt(self.bt_previous, ids.getDescr('pg_left'))
self.bt_previous.grid(row=0, column=2, padx=5, pady=5, sticky='nsew')
self.e_page = tk.Entry(p_tools, width=5, name='e_page')
dida = 'Inserire la pagina da visualizzare'
e_page_ttp = ctt(self.e_page, dida)
self.e_page.grid(row=0, column=3, padx=5, pady=5, sticky='ew')
self.lbl_pages = tk.Label(p_tools, text='di 100.000')
self.lbl_pages.grid(row=0, column=4, padx=5, pady=5)
self.ico_pgright = tk.PhotoImage(data=ids.getIco('pg_right'))
self.bt_next = tk.Button(p_tools, image=self.ico_pgright, name='bt_next')
bt_next_ttp = ctt(self.bt_next, ids.getDescr('pg_right'))
self.bt_next.grid(row=0, column=5, padx=5, pady=5, sticky='nsew')
self.ico_pglast = tk.PhotoImage(data=ids.getIco('pg_last'))
self.bt_last = tk.Button(p_tools, image=self.ico_pglast, name='bt_last')
bt_last_ttp = ctt(self.bt_last, ids.getDescr('pg_last'))
self.bt_last.grid(row=0, column=6, padx=5, pady=5, sticky='nsew')
# controlli rotazione pagine
self.ico_rleft = tk.PhotoImage(data=ids.getIco('r_left'))
self.bt_r_left = tk.Button(p_tools, image=self.ico_rleft, name='bt_r_left')
bt_r_left_ttp = ctt(self.bt_r_left, ids.getDescr('r_left'))
self.bt_r_left.grid(row=0, column=8, padx=5, pady=5, sticky='nsew')
self.ico_rright = tk.PhotoImage(data=ids.getIco('r_right'))
self.bt_r_right = tk.Button(p_tools, image=self.ico_rright, name='bt_r_right')
bt_r_right_ttp = ctt(self.bt_r_right, ids.getDescr('r_right'))
self.bt_r_right.grid(row=0, column=9, padx=5, pady=5, sticky='nsew')
# controlli zoom pagine
self.ico_zout = tk.PhotoImage(data=ids.getIco('zoom_out'))
self.bt_zout = tk.Button(p_tools, image=self.ico_zout, state='disabled', name='bt_zout')
bt_zout_tpp = ctt(self.bt_zout, ids.getDescr('zoom_out'))
self.bt_zout.grid(row=0, column=11, padx=5, pady=5, sticky='nsew')
self.ico_zin = tk.PhotoImage(data=ids.getIco('zoom_in'))
self.bt_zin = tk.Button(p_tools, image=self.ico_zin, state='disabled', name='bt_zin')
bt_zin_tpp = ctt(self.bt_zin, ids.getDescr('zoom_in'))
self.bt_zin.grid(row=0, column=12, padx=5, pady=5, sticky='nsew')
self.ico_zview = tk.PhotoImage(data=ids.getIco('zoom_view'))
self.bt_zview = tk.Button(p_tools, image=self.ico_zview, command=self._adapt)
bt_zview_tpp = ctt(self.bt_zview, ids.getDescr('zoom_view'))
self.bt_zview.grid(row=0, column=13, padx=5, pady=5, sticky='nsew')
self.ico_info = tk.PhotoImage(data=ids.getIco('info'))
self.bt_info = tk.Button(p_tools, image=self.ico_info, command=self._show_info)
bt_info_tpp = ctt(self.bt_info, ids.getDescr('info'))
self.bt_info.grid(row=0, column=15, padx=5, pady=5, sticky='nsew')
self.ico_close = tk.PhotoImage(data=ids.getIco('win_close'))
self.bt_close = tk.Button(p_tools, image=self.ico_close, command=self._on_closing)
bt_close_tpp = ctt(self.bt_close, ids.getDescr('win_close'))
self.bt_close.grid(row=0, column=17, padx=5, pady=5, sticky='nsew')
p_tools.grid_columnconfigure(7, weight=1)
p_tools.grid_columnconfigure(10, weight=1)
p_tools.grid_columnconfigure(14, weight=1)
p_tools.grid_columnconfigure(16, weight=1)
p_img = tk.Frame(self)
p_img.grid(row=1, column=0, sticky='nsew')
self.cnv_img = tk.Canvas(p_img)
self.cnv_img.grid(row=0, column=0, sticky='nsew')
s_v = tk.Scrollbar(p_img, orient=tk.VERTICAL,
command=self.cnv_img.yview)
s_v.grid(row=0, column=1, sticky='ns')
s_h = tk.Scrollbar(p_img, orient=tk.HORIZONTAL,
command=self.cnv_img.xview)
s_h.grid(row=1, column=0, sticky='ew')
self.cnv_img.configure(yscrollcommand=s_v.set, xscrollcommand=s_h.set)
p_img.grid_rowconfigure(0, weight=1)
p_img.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
#self.cnv_img.bind('<Configure>', self._on_resize)
self.bt_first.bind('<Button-1>', self._set_page)
self.bt_first.bind('<Return>', self._set_page)
self.bt_first.bind('<KP_Enter>', self._set_page)
self.bt_previous.bind('<Button-1>', self._set_page)
self.bt_previous.bind('<Return>', self._set_page)
self.bt_previous.bind('<KP_Enter>', self._set_page)
self.e_page.bind('<Return>', self._set_page)
self.e_page.bind('<KP_Enter>', self._set_page)
self.bt_next.bind('<Button-1>', self._set_page)
self.bt_next.bind('<Return>', self._set_page)
self.bt_next.bind('<KP_Enter>', self._set_page)
self.bt_last.bind('<Button-1>', self._set_page)
self.bt_last.bind('<Return>', self._set_page)
self.bt_last.bind('<KP_Enter>', self._set_page)
self.bt_r_left.bind('<Button-1>', self._rotate)
self.bt_r_left.bind('<Return>', self._rotate)
self.bt_r_left.bind('<KP_Enter>', self._rotate)
self.bt_r_right.bind('<Button-1>', self._rotate)
self.bt_r_right.bind('<Return>', self._rotate)
self.bt_r_right.bind('<KP_Enter>', self._rotate)
self.bt_zout.bind('<Button-1>', self._set_zoom)
self.bt_zout.bind('<Return>', self._set_zoom)
self.bt_zout.bind('<KP_Enter>', self._set_zoom)
self.bt_zin.bind('<Button-1>', self._set_zoom)
self.bt_zin.bind('<Return>', self._set_zoom)
self.bt_zin.bind('<KP_Enter>', self._set_zoom)
self.bind('<Configure>', self._on_resize)
self.protocol('WM_DELETE_WINDOW', self._on_closing)
self.update()
# *** CALLBACK BINDING ***
def _on_resize(self, evt):
if self.doc and self.pdf_images:
self._show_page()
else:
return
def _on_closing(self):
self._clear_cache()
self.destroy()
def _set_page(self, evt):
if not self.doc or not self.pdf_images: return
w_name = str(evt.widget).split('.')[-1]
if w_name == 'bt_first':
self.page = 0
elif w_name == 'bt_previous':
self.page -= 1
if self.page < 0: self.page = 0
elif w_name == 'e_page':
try:
value = int(self.e_page.get())
if 0 <= (value - 1) <= self.pages - 1:
self.page = value - 1
else:
raise ValueError('Inserimento non valido')
except ValueError:
message = 'Numero di pagina non valido'
msgb.showinfo('Selezione pagina', message)
return
elif w_name == 'bt_next':
self.page += 1
if self.page > self.pages-1: self.page = self.pages -1
elif w_name == 'bt_last':
self.page = self.pages - 1
self._show_page()
def _adapt(self):
self.adapt = not self.adapt
if self.adapt:
self.zoom = 0.0
self.bt_zout.configure(state='disabled')
self.bt_zin.configure(state='disabled')
else:
self.bt_zout.configure(state='normal')
self.bt_zin.configure(state='normal')
self._show_page()
def _set_zoom(self, evt):
w_name = str(evt.widget).split('.')[-1]
if w_name == 'bt_zout':
self.zoom -= 0.1
if self.zoom < -0.9:
self.zoom = -0.9
return
elif w_name == 'bt_zin':
self.zoom += 0.1
if self.zoom > 3.00:
self.zoom = 3.00
return
self.bt_zout.configure(state='disabled')
self.bt_zin.configure(state='disabled')
self.after(200, self._show_page)
self.bt_zout.configure(state='normal')
self.bt_zin.configure(state='normal')
def _rotate(self, evt):
w_name = str(evt.widget).split('.')[-1]
if w_name == 'bt_r_left':
self.rotation += 90
elif w_name == 'bt_r_right':
self.rotation -= 90
if self.rotation > 270:
self.rotation = 0
elif self.rotation < 0:
self.rotation = 270
self.after(200, self._show_page)
# *** METODI ***
def _clear_cache(self):
files = glob.glob('my_tmp/pdfcache/*.*')
for f in files:
try:
os.remove(f)
except OSError:
pass
def set_file(self, f_pathname):
if not os.path.exists(f_pathname) or not os.path.isfile(f_pathname):
return
self.doc = f_pathname
f_name = os.path.basename(f_pathname)
self.title('tkPDFViewer => ' + f_name)
self.config(cursor='watch')
self.update()
self._clear_cache()
self.info = p2i.pdfinfo_from_path(self.doc)
self.pages = self.info['Pages']
self.lbl_pages.configure(text='di %d'%self.pages)
self.page = 0
self.e_page.delete(0, tk.END)
self.e_page.insert(0, '%d' % (self.page + 1))
self._pdf_cache()
self.config(cursor='')
self.update()
def _pdf_cache(self):
self.pdf_images = []
for p in range(0, self.pages+1, 10):
images = p2i.convert_from_path(self.doc,
first_page=p, last_page=min(p+10-1, self.pages),
fmt='jpeg', output_folder='my_tmp/pdfcache')
for i in images:
self.pdf_images.append(i)
def _show_page(self):
self.config(cursor='watch')
self.update()
self.cnv_img.delete('all')
image = self.pdf_images[self.page]
# eventuale rotazione
if self.rotation:
image = image.rotate(self.rotation, expand=True)
img_x, img_y = image.size
if self.adapt:
# calcola i rapporti di scala
r_img = img_x / img_y
r_cnv = self.cnv_img.winfo_width() / self.cnv_img.winfo_height()
if r_cnv <= r_img:
f_scala = self.cnv_img.winfo_width() / img_x
else:
f_scala = self.cnv_img.winfo_height() / img_y
fx = int(img_x * f_scala)
fy = int(img_y * f_scala)
image = image.resize((fx, fy))
x = self.cnv_img.winfo_width() // 2
y = self.cnv_img.winfo_height() // 2
x_region = self.cnv_img.winfo_width()
y_region = self.cnv_img.winfo_height()
elif self.zoom != 0.0:
f_x = int(img_x * (1 + self.zoom))
f_y = int(img_y * (1 + self.zoom))
image = image.resize((f_x, f_y))
x = f_x // 2
y = f_y // 2
x_region = f_x
y_region = f_y
else:
x = img_x // 2
y = img_y // 2
x_region = img_x
y_region = img_y
self.img = ImageTk.PhotoImage(image)
self.cnv_img.create_image(x, y, image=self.img, anchor='center')
self.cnv_img.configure(scrollregion=(0, 0, x_region, y_region))
self.cnv_img.update()
self.e_page.delete(0, tk.END)
self.e_page.insert(0, '%d' % (self.page + 1))
self.e_page.update()
self.config(cursor='')
self.update()
def _show_info(self):
message = ''
maxsize = 0
for key in self.info.keys():
if len(key) > maxsize: maxsize = len(key)
for key in self.info.keys():
message += key + ' '*(maxsize-len(key)) + ' : ' + str(self.info[key]) + '\n'
msgb.showinfo('File aperto con successo', message)
if __name__ == '__main__':
app = tk.Tk()
viewer = PdfViewer()
viewer.set_file('reportlab-sample.pdf')
app.mainloop()
Se volete riprodurre quanto sopra ricordate dei due sorgenti nel precedente post.
Il prototipo del viewer versione tkinter è completato, il prossimo post stessa cosa, con wxPython
Edited by nuzzopippo - 12/5/2021, 17:29