Αντρω δι Νεξυνω

[Python] GUI & PDF a passetti 2 : testo e csv

« Older   Newer »
  Share  
view post Posted on 14/6/2021, 10:20
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,226

Status:


Questa "seconda puntata", pur se più articolata rispetto al precedente post della serie, è ancora nella fase di definizione di prototipi funzionali senza molte pretese, con omesse adeguata strutturazione e la gestione delle eccezioni è decisamente minimale, limitandosi ad eccezioni "di selezione" testo per le manipolazioni impostate.

Qui si provvederà a dare la possibilità di selezionare un file PDF, estrarne il testo e salvarlo direttamente od anche manipolarlo per creare file CSV, rispondendo a questa particolare domanda posta nella discussione che ha dato l'idea per questa "serie".

L'estrazione del testo è prevista tanto quale estrazione diretta tramite pdftotext, con metodologie in linea con quelle già viste in altro post, ovvero tramite OCR utilizzando la libreria pytesseract.
Si faccia attenzione, in merito a pytesseract, tale libreria è un wrapper del programma di ocr "tesseract" che deve essere installato nel sistema. Nei sistemi Linux (ambiente in cui si sta sviluppando) è in genere disponibile nelle varie distribuzioni, nelle quali viene configurato, così potrebbe non essere negli altri sistemi operativi, nei quali potrebbe essere necessaria una configurazione d'uso, si legga la documentazione in merito al proprio s.o.

Informazioni spicciole.


Al fine di contenere le dimensioni del file dedicato alla serializzazione di icone e tooltip, si è optato a creare moduli di risorse specifici per le singole finestre (o genere di finestre) occorrenti, si preciserà in seguito.
Si è anche definita una classe per sostituire le finestre per messaggi di tkinter (che NON mi piacciono) ed una classe specializzata (se fa 'pe di' ;) ) alla "preparazione" del testo per codifica CSV.


I messaggi "personalizzati"


Al momento, una singola classe ("Message"), che può rappresentare, a scelta, un messaggio di informazione, di errore o di warning, è una finestra Toplevel contenente una icona contestuale, una label ed un pulsante; alla sua visualizzazione assume in esclusiva la gestione degli eventi, deve essere chiusa perché le altre finestre possano proseguire nelle loro funzionalità.
Possono essere impostati un messaggio, un titolo ed il font di rappresentazione, per tutti e tre i parametri il passaggio è previsto nelle funzioni di invocazione della tipologia di messaggio da visualizzare. Il messaggio è un parametro obbligatorio, il titolo della finestra ed il font da utilizzare sono opzionali, il font di default è quello di sistema per le label.
Nelle immagini sottostanti vi è esempio delle tre varianti di messaggio.

png
pngpng



Le icone sono selezionate in base al tipo di messaggio tra quelle serializzate nel modulo di risorse
global_ico_and_tip.py
CODICE
# -*- coding: utf-8 -*-

icodlg_error = '''
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
'''

icodlg_info = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGIUlEQVR42sWXA3SrXxbFbz22bVs1n21
btZna7bOt2nabNqrNv2375Rl7zu3NIJN89VqTtX4f9z17nxMzANPBhDAjLAhLwkoHP7bQ3TOdTs2pCk
11Rl8ivk/8mvgrYUPYEn8nfkf8iPgK8ampBpm8Y2H8VeKXVxpH3YMuqxtCrra9npDfc+NYxfD9E1Uj9
1OL+7WyrK43IzI7FJnNT/jqwnxDF8RkpgFMic8SPwy6qEzcebTxnUNlg8hWPo98zUsobn8FFd2voar7
dVR0vUa8ilzVCzheNQrPs6r3ZRnt6QB+SnyeMJtuAG7+uaOl3c6Lo0ue8b+kRobieVyTv4DD5U/iUJl
RyPxp0r2AzJbnEJ7dhTUpNS+crRpcCOCLUiGkxv6ZzQllmxyD8z6Oze3D5abnkFz4+LS40vQsUoqHsS
Su8sb+ow07eEOE6VQCWDj7Z6yy9c+5d6hsFGklTyAmd3RGpBU/gVPVT8BVVnpveVTxBgBWkwUw+cu+M
7/59oYTH4Re60F4xpAklxufwwtvaTn8eEJtUuEQfrbn8ocLwvL+yKcgGSAhIcH0UwsS+xZFVyAsYxCB
V/qN4n+pB+9/cge6Bz/m1yT1EZmD2HG0GV9edXRQrVabSwZgdhHbvr72OAKv9sL7gjR+F7rx8OGjfwf
gx74XuiZcE5E1gJ/vvgKr+Yk7jQaghwlziHxmY5oc+053TcjOY23QjL7+7wCakdfoWuuk6zzPtoG5Jj
5LriaGAf4WYvu1lYew62Q7dhyfnI0HlYi42jnOxnTllNb4XOjBD7eeA7MOszMM8I/Ak7/dl4FNhzUTs
vGQiszVhAob0gkKsoEC0DWhmYSFUZVgdrJThgGsg/psg8qwOlUhycqkZkg9ViTIuWZS1qY0gdlH9hkL
8LJTeCWWJsolWRTbJBlgQUwjliYrsTRJQdoWyRqrkymAg+xlvQDjL0CboBsushrMp0IGxBJxzXCLlWM
+4RRRbxDASUaaBBWhxPz4Fq4nrV4dMm/B6vEJyLTc838DaG2Cq6l4nT6RDXCKaoJTdDOcYhRwilPBJq
zBIIB1OOni1IRK6Ei/JFGBNWkKuEbV4s/+ZfjSpkyw5ZfAbMN0AfSfgjd+61UE65AqoloQWjte2Fomh
3WUAtbRKljHaPCnEMMAjjFyLEzvxPzUDsxPaYUNBfjugXKwNWS68goZXxQsuwBq9g1j74In2JIzMFlx
EeYrL+HT6zPwhS35+PKuEnxzfxW+512Pnwcp8MtwFX7kXYc7d+7g9u3buHnzJrRaLcy2kNmuerAdtWD
bqsC2lIFtLAJbn6sfYuk5kNeTxgLksoVHuUiMafV1sHW5oggvtr1aGOxpAttaiQ8++EAPtrUCbF+LuE
860v8nBNWheqLuwmM8QJ5hgL8ELGfO8WArroKtItZmg20o0Dff2wy2X0XnNYYBuMa9VdwnnV4IXofXW
52JcY+/Ba4wDLBhgxm9Dt4TwgwaXR7YpmIaZyXYzjpR9AAV92in4g2GAXaRxrtb3D8gQtA6sZ7qUD0B
eXAv419G/wiIY86xYJtL/tP9jhqw3U3UmZJGrODHxiewlYz2yMlczUNwvdDSeqpDVIC5xPLxx0t/G/I
p/CPoDbb8HIUop/QV/+neXcMNhKE0pG0B8+oSejEFEWLlBVDtN7mHZAAOPT8OzCbwDttSSp1WURcNon
Pe1T7l5AH4BHx6dXqFWE91qOZd9ld/xyn9KKUQXswx6iFbdpZGqRAvLM8OgQfh3QPmOwAWMAwWOAbmT
3vfPuq8W5jTsdC2gf1ejvFafwvyntbPclrgS6nvsbUZwpQX9O0nw1Gw0KfAZC+CxbwOlvCO2NM5Xef3
hY6HXJcFXoMa8p3J/wIKEeBi8o+gD5lrAhXLpI5HhEHIk2ARz4NFvUzmr4k9Pw+hAMFPgP1FA+YaD1O
b4I/YX4NcZ/XXjLkkmJvbBJ8ytw+5wxYdBtvTKF6c/FXu30+hBmhCZLijWnwILTgEC8ewu5b2oWf42r
n6b8jYX90tLO0j9n/aTdZj5Sa7Ye4mu2/qFPbIxCkMZvMiH1jOj9J+an5Un6WTbD/Xzu2fUwloPOacW
dUA8H/lnyu7kcofUaquAAAAAElFTkSuQmCC
'''

icodlg_warning = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA
DdgAAA3YBfdWCzAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAATHSURBVHja1Z
ddbBRVFMf/5947H/vV7rZsv2grpFCk0EIEoiQEMRCjAUwk0UgwvEj0yUR91WCIMcYEE43ogy/6zgM+a
YKY8KIvhjRiAL+iIVrQttDu7sxs5/N6pzt1mt1USWklnuSXczL3njn/c2bnY0lKiXtpTPE/FnCW+Dx3
Ycu/BJ9SEb3F4/BnawhwDvvk7H8n4DzlIp1/HbWfGAMk2OzHl1mbvxs7pLP6l+AiiUhjLwZ256joeBq
i7TCCGX00ssRLAPHVFxAgJ132smh/imB0AJkeiI5jFNl4BReQXzUBaffi1dDr6mK9BwGvOg/rewyhU+
iEECfjPasnoIBiVKcTYs0RgHPArwKBAiFE95MIHHoOQHF1BHxEWlTjb0rqLbHyHkSq+O3pusJBpETw0
k5IWWqPOH8rnsLKC9iCcuSyo6x0EJAuHMtGdcpGZdKBbTmAtMGL+xHa7BlwlFdWwCXVvc/eifhggbeP
AlENgVtDVFc4Fvy5GhBVwXLrIdGV9332XpyzcgJ8rPUdeoKX9gORNV+Ms6oSYCF0auC8CsiYCnj7LiW
KDsFF38oIOEt6YLH3SR/O8kwfEDWKcaEEqOJSTUHoFQA1hTpu5gExkAls9mGce3cCiAgd2BDW6VHeuR
sgS5EUUkVl3VZYSkAVoATMQJSGENh0IM4FES1fwGfQ/YCdQXarzs1MXFhRm0cYlfnupaPiTAVgtYY45
bnpAubaOPcDXIG2TAFEaNO2exbfI8pbAKo0BLAG3KiCPAvkW6pgpXGcJ+s0C63cC7+GPbilbQOWnoLA
UvYJDK8cvMvzwxoXUwB0QDMAEooMIAKY5RyIa4CuA7kIgGwgAU4EmpoQ3uzEGf1z7MXjcO9cwCli2Cc
e9m6wXeZ908DcDwAl86KYOMjD+y0P6dqAUwHcZC022XBadzfqV+RO1sP3ilP0JV6X0Z0JGIFRvxW+zd
p6uCj8/nfh1EfKV5HpanQLrjCRWCpAGNNgpW7mzU6eFiN4CED9Dn4DxMJOfti7nR3Te11ARwMjwUy8B
vQ/W0P/8Vqy3oSu0EKYQxq8SRoN2/iheLL//kFyk7LWFxinNRuGc5t+BljSIUugNHZ+1MEyEuY6f6Hr
2KdEsWewxwcgp67/lN+K7c0fLaz5G88fZ8f8SsdGc10V0BY6SVjU6cyFHG6eLmLijRKscXOh63S/SOA
RMvcT3Elto3+DHQWILS1gEDnnunxN39hHvDAJCKRoqY+L+b8KcMI83i9aWpgvmhg1YNnrMLeuhzr3SV
xFtklA2r07wV4InJ5Bc2gyXmlh8YnbD9ZhdKnuBkIUDtSBCK3IhVgisymAO20O1q+x5+Narb+Bi1Sc+
YauGdse6MluvrS4YApvuhuQ+OgfCBNUbF8bg/vtd392PCKH8aCsphMgInBsc6eMbmPQbkpOCBR+gruI
umIuiT204qe5mSEGv2J2qXhz63OAIdK72ySonyAo7TQ2an7IpD6JE9L9rfkEeFmYG7IAq0etl+Aq5ee
+186Fnrnfmy6Rc/kPrKTxYgHGAJeZ/pnz5qB/JL4dW58DX1EBvtgBTa5Fs4UKjnSzXCJmKg4XqiYxC2
U8YQA3kMdljEm7aQJN74EREFbDrkI2vw/u+d/zvwDcGQDDCcV61AAAAABJRU5ErkJggg==
'''

setico = {"dlg_error" : [icodlg_error, "Messaggio di errore"],
         "dlg_info" : [icodlg_info, "Messaggio di informazione"],
         "dlg_warning" : [icodlg_warning, "messaggio di allerta"]
         }


class IcoDispencer:
   def __init__(self):
       self.icoset = setico

   def getIco(self, chiave):
       return self.icoset[chiave][0]

   def getDescr(self, chiave):
       try:
           descr = self.icoset[chiave][1]
           return descr
       except:
           return None

   def printData(self):
       for chiave in self.icoset:
           print(chiave, ':', self.icoset[chiave][1])

   def getData(self):
       return self.icoset

di 114 righe di codice che, al momento, contiene quelle tre sole icone.

La definizione del particolare tipo di finestra che si vuol visualizzare è "postuma" allo istanziamento della classe, utilizzando lo specifico metodo "show_qualcosa", esempio relativo alla finestra di informazioni, prima in figura :
CODICE
for key in self.info.keys():
           message += key + ' '*(maxsize-len(key)) + \
               ' : ' + str(self.info[key]) + '\n'
       msg = mydialog.Message(self).show_message(message,
                                                 title='File aperto con successo',
                                                 font='Courier 10')
       center_win(msg)

i metodi previsti sono :
  1. show_message(self, message, title=None, font=''), per un generico messaggio informativo;
  2. show_warning(self, message, title=None, font=''), per un avvertimento.
  3. show_error(self, message, title=None, font=''), per segnalare un errore.
il modulo rende anche disponibile una funzione "center_win" propria, per centrare i messaggi sullo schermo evitando di doverla prevedere nei singoli moduli per GUI, come nel caso dell'esempio soprastante.

Il codice delle finestre di messaggio, modulo mydialog.py :
CODICE
import tkinter as tk
import global_ico_and_tip as giat

class Message(tk.Toplevel):
   def __init__(self, master):
       super().__init__(master)
       self.resizable(False, False)
       self.image = None
       self.lbl_image = tk.Label(self)
       self.lbl_image.grid(row=0, column=0, padx=15, pady=15, sticky='nw')
       self.lbl_msg = tk.Label(self, justify='left')
       self.lbl_msg.grid(row=0, column=1, padx=15, pady=15, sticky='nsew')
       btn = tk.Button(self, text=' Ok ', command=self.destroy)
       btn.grid(row=1, column=0, columnspan=2, padx=15, pady=15, sticky='nsew')

   
   def show_message(self, message, title=None, font=''):
       if title:
           self.title(title)
       else:
           self.title('Informazioni')
       image = tk.PhotoImage(data=giat.IcoDispencer().getIco('dlg_info'), master=self)
       self.image=image
       self.lbl_image.configure(image=self.image)
       if font:
           self.lbl_msg.configure(font=font)
       self.lbl_msg.configure(text=message)
       self.update()
       self.grab_set()
       return self
   
   def show_warning(self, message, title=None, font=''):
       if title:
           self.title(title)
       else:
           self.title('Attenzione')
       image = tk.PhotoImage(data=giat.IcoDispencer().getIco('dlg_warning'), master=self)
       self.image=image
       self.lbl_image.configure(image=self.image)
       if font:
           self.lbl_msg.configure(font=font)
       self.lbl_msg.configure(text=message)
       self.update()
       self.grab_set()
       return self
   
   def show_error(self, message, title=None, font=''):
       if title:
           self.title(title)
       else:
           self.title('Informazioni')
       image = tk.PhotoImage(data=giat.IcoDispencer().getIco('dlg_error'), master=self)
       self.image=image
       self.lbl_image.configure(image=self.image)
       if font:
           self.lbl_msg.configure(font=font)
       self.lbl_msg.configure(text=message)
       self.update()
       self.grab_set()
       return self


def center_win(gui):
   ''' Centra una finestra, passata come parametro, sullo schermo '''
   l = gui.winfo_screenwidth()
   a = gui.winfo_screenheight()
   wx = gui.winfo_reqwidth()
   wy = gui.winfo_reqheight()
   gui.geometry('{}x{}+{}+{}'.format(wx, wy, (l-wx)//2, (a-wy)//2))


Ovviamente, in seguito, è prevedibile una diversa "collocazione" per center_win.

Per oggi basta così, il prossimo post verterà su alcune piccole variazioni apportate al modulo pdf_viewer precedentemente trattato.
 
Web  Top
view post Posted on 20/6/2021, 09:41
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,226

Status:


Come detto nel precedente post, ora si sta mirando a definire dei prototipi di GUI capaci di estrarre il testo da un pdf, per poi cercare di affrontare la "complessità" del far impostare ad un generico utente le condizioni di manipolazione del testo estratto finalizzandola a creare dei files tipo csv.
Per realizzare quanto sopra ci si appoggerà alle librerie "pdf2image", "pdftotext" e "pytesseract" alla cui documentazione si rimanda per la installazione/impostazione nel Vostro sistema.

Un venv minimale


Un virtual-environment minimale sufficiente alle esigenze del momento potrebbe essere analogo a quello sotto esposto:
CODICE
(pdftk_v) NzP:~$ python -m pip list
Package       Version
------------- -------
appdirs       1.4.4
pdf2image     1.14.0
pdftotext     2.1.5
Pillow        8.2.0
pip           21.1.2
pkg-resources 0.0.0
pytesseract   0.3.7
setuptools    44.0.0
wheel         0.36.2
(pdftk_v) NzP:~$

...non dimenticate che io opero in ambiente linux, fate attenzione per eventuali esigenze "altre" che possano essere necessarie per il vostro sistema operativo.

Comunque, il venv sopra esposto è mirato all'uso con il framework tkinter, non dimenticate, inoltre, di creare manualmente la struttura di sub-directory "my_tmp/pdfcache" nella direttrice della vostra versione (ammesso vi sia qualcuno che provi), al momento non viene gestita dallo script, il perché lo vedrette quando si affronteranno "operazioni massive" e la strutturazione del codice sarà molto diversa, per ora la situazione file/directory funzionale è la seguente:
NzP:~$ tree
.
├── global_ico_and_tip.py
├── mydialog.py
├── my_tk_object.py
├── my_tmp
│ └── pdfcache
├── pdf_viewer.py
├── text_ico_and_tip.py
├── textutility.py
└── viewer_ico_and_tip.py

2 directories, 7 files
NzP:~$

dei files contenuti "global_ico_and_tip.py" ed "mydialog.py" li trovate sopra mentre "my_tk_object.py" lo trovate nella precedente serie di post collegata, è lo stesso file, copiate il codice da li.


Il "nuovo" viewer


È, invece, cambiato, rispetto al primo post della serie, il file di risorse collegato al visualizzatore di PDF, è un elemento che cambierà frequentemente, così come cambierà il visualizzatore stesso, che verrà "arricchito" (od anche "impoverito" di) da funzionalità occorrenti in un determinato momento.
Il file di risorse del momento per il viewer, "viewer_ico_and_tip.py" ha il seguente contenuto:
CODICE
# -*- coding: utf-8 -*-

icoexec_ocr = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFcklEQVR42qRSB4saYRDNDwj9BPwJ0sQ
Tk6igWCkKSLMg0qUK/tMLnIJu0JwUKUbXtu6ujZd5w+4RQgwfyYPH8C1T3rzZDyGenp4+SngWpv+BX4
QFg7znSCTCOX/EJ/wHrtcrTCBzPj8SkINgs17D3mywFe63W/xYrXDY7XA8HPB9Ptf4zv1e6RyPmM9m+
u0gb5K1e9tWspctfQMB+b8K2LJIBrIJG1LA0XHgui5Gr6/wJJ7k/Ss9z4M1nTKHNRTE+LsgMwFMZlP3
dIIvDVcigAPuYvF4PMYjWJMJLr6PtWwailQhZCDESIDaKVZPZaOZ2Pr15QWLtzd1JRqNIpvNIp1OKzO
ZjDIej2Mi+cz5ZlnYyba7wMXQSdJYADfg1tfLBcvlEpfzGUQikUC320Wn03lnu91GvV5Xkff7HZYIOE
s+63kSOnkSOo5jfgJuoD+jbWOxWGgzIhaLoVqtolgsolwuo1Kp6Dufz+tgYjQa6UA9o9R5FCHkNyMBV
MwiPxiay+XQbDa5pd7zEVKplDqSTCZB+L6vpHjSNRYQ2O9JMdFoNDAYDNDr9dTG2+0WkpZrJEqlEobD
IQqFAp88AxkKoRtmAjg8LCJqtRr6/T5arRaIn51XgUeccRh229CpoaJaxUIUQkvszF0LqEMxDINyVdW
OuxEKBZRK/0VINAIdJEQntG7doeKm73bftbtrlRHTbeN79z6v78e3067vvpfH6lu+9/m97/M+7++T5D
wRSK6AQCtmZmbQnnsJ3DklkGJxqUAyOB5QXAG0aH5+/h8Cv1nEeA9+LouAYq5EhTAMg0oFdLK4uKgII
DlgrYQzAltbW+IH2WyWhoaGKBgMAqKLUCgkmJ6epkAgQAsLC+Tz+aQqP2DFPFEQLrQDbdkigMSqzIhY
LEYq5ubmaGVlRYGWl5dpaWlJfgaR1dVVjGTxFJQ3hhcXF3R6ekrJZJLS6TTt7e3B1VBOMaGxsTGamJg
QTE5O0tTUlFRjZGRENOD1egmBUwPKhFAJWwRQfqhciWtgYEASDA8PU6l4yxqYnZ0V3xAClh0Ai4a52d
cAlw5ADA4OUjgcVgSEoAAlNp1O/R3agAoYPB3Yqljr11dXslFzuVwZY2gC4ff7pdRQuSJg7e2tWdr+/
n4aHR0lj8cj/5fL5yUpdkkmk6HU+bki4ClNADZsqQAsFqfr7e2le8Mcz+7ubiGLffGLW3jDp//GevrC
E3RyckLx42ODdZXlPM8eJHDLvcMLsNuxZNBHkOro6AAhSfaiq4s6OzsFra2ttLOzQ7u7u7S2tkb19fX
U1tZGLS0tlEgksN4NXmyFqqoqf11dncuWD0D1cDqUUAVmHNUAIE7YdF9fnxD7XxgcKU37466oeMPJHy
PRg8sII5PnHmIVR6NROjo6Ej+orq6m5uZmamhoIL7dEr9QTutyuQihtIEKXrPwvmqakeDSc4Xe1dTUP
EESWwTkRQyM4hWrGP8CxYEZ13WdIpEIqerh/gjl6+m0kTw7Q9UCxcltrWMARHAnLHBL8ByAqcCYkARG
9ZmvbOvr64TA3N/w84yuGxqLj/fCOPe9RPJSFxKzErgVFSzXK7QHBDDbQoBbs7GxQepKn+GTp1Mp3Jb
eu91uSV4+AYaqBE4FAngO4He42vfLS7kHxlgfHzc3CYHkOuvmdU9PsLKy0pLcQQUEZlLrvQ4E4HKXEK
mm0afDQ4psb4vc2WwwomEWqCR3REB9E9xZiYCA+RECT4DF5nM5WdMHBweYEAPlb29v/8BXd5XcOQEBT
m+B+tAAAQgQLoevoej+vvGTnzc1NYUYjpMjXlKZge+FeDx+wyUfb2xsdJ4cUVtb+xRfyAxfCXgtwO+v
GM/ZoB45zfsXMe155nlHkWIAAAAASUVORK5CYII=
'''

icoexec_txt = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFrElEQVR42p1XA7AkTRP8/8+2bRtn27Z
t274LnG3btm2bz7bfW0x+lR1TZ8xuRWT07qAryz3/U3nnnXdekeUvQU6HyM/VR/z17rvvUs9DJRt8kM
zMTPgjoif7owjkg0hcbCzi4+KQGB9v1qTERCQmJCBZ1mR7TUtLw5nTp5GWmorYmBikJCcTvKfgO2YPI
kH2iZd9KfTcYwkkyAtUyg2Dg4KQLsqMgpQUpBKiNEusP7B/P1xZWQgJDkZGerohw3t8JoUgISIpySBR
9nREgOz5Ije6du0aMmVzbkwFtJyE3C4X9u3dC4/bjcsXLxpCvE7wGX02OjLyDhmnBPhiZkaGURIWGop
HydmzZ0G5ceMGHiUpYnlWVhaJkcgTCRSwLAvnz53DoUOHcPTIEfTu3Ru1atdGvXr1UKlSJZQoUQL58+
dHnjx58PPPPyNfvnz47bffkDt3boMCBQqgaNGiKFmyJMqVK4c+ffvi4qVLCAoMdB6CDLGesaXMnz8fw
4YNw4gRI7B+/Xps2bIFGzdtwvbt242CTp06oUqVKtixYweGDh2KIUOGoGXLlqhTpw4GDx6M5cuXA5ZF
j9KzzgiYWErcvR4PZs2ahbFjx8Lr9d4DCgnQQ9WqVUNUVBQ88rzKRckLem3RokW8boxKd0qAyvkCwzF
16lQES5a7JdkoCVJaqqhq1aq02FhL4fV4qSCVCRMmYPHixWafTCap7OmIQKJdBS5JwjFjxpgK4OYhIS
E4ePCguU6pW7cuRo0ahSZNmkCJ8756g3nE+1TOBCQcVwE3oyJaz5Wg60mMK6Vp06aYMmUK2rRpAwqvU
wlXYt++fUiSKsiw4++YAF9g6dDtQUFBtIYEzH9CQ9CuXTvMmzcPXbp00RDwvlEeKfV/9epVhoTvcj96
wjcCBGOuHribBKVbt25YuXIl+kqpUXidFoeHhyMmJsYQiY2N5XtUTq86J6CMuSruJkGh5STQp08fUO6
+r1aTAFcqZxgcE6ByIktw2wMMy10EWrVqhTlz5pheQFHSfF/BBKZiro5zIJMeIAm1/G7rbRKUxo0bY9
KkSabxUDiQCJ0FZijZw4jDjVPReQhs65WAksiyXUupLS2aHbJBgwagqJVJ9uRLsMc5x3VMdDQiIiJ8C
AFxHxFdCUrlypUxaNAg0wkpVMiYU1m09AJWApWGhYWZHnLr5k0lkOPxBOhKOweUhAkHYV+jlC5d2lRC
mTJlzP0wUcLzQ6AMngDBrVu3cFMm5TUpxwsXL/IAY8nkDBc9HzrygJJQxUw+lpd6oFChQmjdurVZef3
EiROm+x04cMB0xCOHD5uJevLkSQTcumVJWDJfeumlku+9997/n0hAK4He4Ko9gZvTzZRcuXKhfv36yJ
kzJ86fP49HCYdBYECA+/nnnqsoB9KnqcjRMKJihfYGtmJ64vjx44YAR/E///yDFStWgKLP8rlYyYVAs
fzcmTPW119/XfOtt956hkqeRICKtOWSvAIqPAGVKlWKBEweZMuWzRxU1HusBGZ+SFCQdf3KFR5eGr75
5ptGuSMC7OH75cC5efNmbJbDxybBxo0bTdP58ssvIZbg448/xttvv41PP/0U8j3BzAaFdR8nykODg60
AyfqCBQs2ffmVV1S5MwI8hNILPHASxhv62+4DXHft2oU4qfM1a9aYlqxHeioX63k0a/nCCy84Vn7PiS
hDcDsXbPA6kWKfmHk8i5Aa3yBHtZV2DtDtQgD58uZtJRmvyv0gQOWEEhHoQNFvBJ4LQ6X2d+7YYUIkY
knS4e+//24nYVHlvhNgHzcklIgSsL8N2Nv51XNCKiEgIMCU5qlTpyx2wu+++679hx9+qMr9JCDW3f0R
otABQwKMdYTM/auS5fKFZJHgRx991FZA5X5LdvgoXknMc+fOxUlFNPXbchXZ5FVZsgnyPgZ5FPb/XIL
Ppc6f8lfvf1ZuvChTv9atAAAAAElFTkSuQmCC
'''

icoinfo = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFn0lEQVR42s2XA5QjSRjH1zrbeDibD2f
bvls9r22Ogk46zrxMDde2bds7WI2TQU/S3dMZ131fXnV2s0lnNYd67xd++FfVv9WGUvqf8v8VkJCQcK
/Naf7YZDH9wnEpfxnNuu7x4HjDX7yN/9nisHyk1+vvuiEBViv3enZ2xh++Kl/3pqam7pTSqwFi67uXl
ZV1z8hO+8PpNLxyXQIMBsPjs2ZN+QMbB4Nyd0EQgKqrIhAQQjmYOx1q8Dz/2DULcLgtX8pyAAtFNcg9
B7NbV9A9Ye7x7hPTV/ezEcLZnDY3Z9KRK2Ey69N4u4m3OvjBOElNAbjfOAOcjSBEsuFIcfd1Bwu7z1k
wZ9KUKVkeX6WPQCyBvKsBYhuI1+slGVlpbpuN+zGmADQUvIOAQEy2bNsydPbcGZ7m5mYSDNYSv18ggl
B9VYiiEMrB3Bkzp7g58JqmAFmWY5KeSaySJEIhKapB3nkvyVqfTxLmnCBjph8h7lV5ZMuR4qg4zBWhh
t3JT9AUAB6ICe4lzkAU/Tj7CDZBs7UHC8m5Eh8p9FaShTvPkeHZh0l+sS8iDnOxBnjDc7GxMeVtMMgm
3moSjSZdC0DjwZn11JVqV6ZOzy4+dvzYIlEUyeV4KwUyFASs3l8Y9R/0DJkz1NxmN9typmQoFZU+2tj
YSK9yQGwD9XrLadaU9MalyxcfliSJqAh+kbhW5JGUBSeIr9of/p0RFoBL/vvMWdOCsCRUUWqp319D4Z
C7KgKBmlAO5s6aM71x565tG9BkgigR69LTIXyCiMaLIizA5uBBuQiFZFpTUx1B7oVyCqaiiXOOUzAVT
V2ZS7ccLYqKw1xRCtDMbFIVDAbJ+sMlZNzMYyEh8D0mYQGwl40tLS0wGz/OPoJNR4oomIqeLamgheWV
FExFh+ccpvlFvog4zMUaJt7QXFdXR+btPE8MC08S+KxJWAAaCocoBq5IeaVAh4GA1QcKo/7DgbXwZCP
BDP1SkMBnLaIFyLIUFzAVda3IpSnzT1AwVdT/TAD4oYnYluURDlYAPmvCBHhUAWgmTWoCMrUsPR3CJw
RixlwU0Iz7Hzr84HNMsDkeijabOSUsAIyhybrDpRSK0hpR1oqJEBAHdk1oInA6dyckjH8zLKC+vk6Te
TsuUDBV3BgcrFZcAecLz3vsTqtLp5v0WZs2bTqhAPWkoomkBKlfUuLGqAIWLJrvbmhoILGE4OHHW426
b7/99j5o3gXoGDoMMRmMoYlteS41LjoVNwYH1GoaOLDvxxYr58jNPeVBEZcKQWF2J5/ImncOCXC4rEc
URcHjWBPYfzz8tP5nh7FILVbTOSh61zvvvPOA0ajvmzMlyy3VShEirHaTVW0OdMAbkF5z581WoFQ8EX
Gbg6ngVDwjOGbMyN5Q9HbgFuCmISP7vWy2GM379u1JVQ3IWznPc8891wmbA+2ANu1hyVzTpucocK1GF
9PLRzwB5y+cbwJT1SYkjR8Hte4AbsPmQBfkqaeeukVn1P3uTnO6q6srCYG7ob59+2JcO7wQhgQAncaN
G/WRxW7eyttMVerlmJlKgb2LKQS3DmZ07ptvvnkam6szZ0vcidERGT58+GMWmzkBz34pfEr4ZhVHOxb
UhSXfyordDdw3cuSQ/tBEzMs73Xj5iqAwu9NymuXcDHRlzdsz2jHaIpDaluN0H+gt+icjbkrVVWAFsN
Btl4i45/3333/ayOkWT5mapci1coQIMFWV2pzVaHfNT0ZMYQemXhVxKzMUCrkT3T1k+IDvYTXK9+7bU
68aEL43Pvjgg93UmV/fo1mkiE5sO7oxIbegGJUXX3zxXr1J50z1uJSqqkqanuVRBg4c+LDa/HoFaAnp
zMR0RUEI+9xlzJjhr1nt5pNoVANveK3VH06ZkHbMHx3CJ46L5mqbkJDQDgQMNJiT379eAX8D/yxsjEx
hE6MAAAAASUVORK5CYII=
'''

icoopen_pdf = '''
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
'''

icopdf_close = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA
DdgAAA3YBfdWCzAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAVOSURBVHjarZ
dLixVHHMVPVfe93sfoyJAZh2EUxNe4iCFhwJ1ZJLhKHJKVklU0ZKE4fgMXfgFRcBViIBuzig+ykmThM
iKE4CLxEURmGIwBHXXuu7sq51DdnSbOMA7egt/9V1d31zn1uFXVxnuPdZMx5rIxdQd8bIAjAPaTbWQS
IT0hf5M/PHDDAj8f976DN6hcBtYTnqbwWQscjYCRGEBELDEIyRNHUpKEuMLrHyxwjkYWZWTDBn60tvE
sCM9XgXqj2cT04cPYsnMnGtu3o05qRKm7sIAOaZOXjx5h8eZNtFst9IGOAy6O0cjnzrXf2MB31k6z9G
oEzG6dmMDuY8cwNTeHysiIOiW8SMrJl+JgZQVL167h4ZUrWH76VD1yxwCffenc4roGLlv7PsNPVWOm3
j11CtuPHEEUx7BhRCSszNoGvA9DwpimKRauX8fdS5fQ934JwCfHnfttTQNquQN+rVs7deDMGUweOoSI
YjbDEIiSkSJJOI8ZjiTkya1buHvhAtrOLVngoHriNQMa82XgVtXa2fdOnsTEwYOIAUTWQuJgNORNDAj
vHJxgfsD49PZt/K6ecO7OVuDDfE5YAsP0HDhLwdmJXbswNjMD8+oVDCcSiG+3/6PTAYjvdoESvA7lJH
8WRHVYMrZ/P7bt3g1pSEua0o4JvuVfjWF+E4DxvXuhF9RaH0XwjGA0BFneZ8Ph/98DBIJjD+eg6IlhP
mJ8Z98+PH/wQMMyT81LABZiOfnGmLMUrzcAbNmxA+AslpDwcRzyitYWRrzyJQMSl6jwElZMEkAGGA3j
KOtu8J2e9/Ve6IWvYzqpAziq1itj2W3qAQn6SgVGwiJNZSSnmA+GSNwQn7W6gMI5ZjCA5TBxgqOteeH
9UWqf0RB8pBVOBmq8mdy/D3/gABzFLfEEmRGjvMTz3ggGiu43anXWWk9B5RUdUUwfPkSd79f4bCdNR1
Jqxw6Yk3iFFQm3uAi/Zw+enT6NYaax8+eRLi2hQuPSiUnq/RwvMUNQoauYV255GYPHjzHslHCZBuuO2
QNVikeheCb2wKQuYhUSQ/r37qHRaGCYacChNdKwNl/cNGyTcb6lGpHdUH58fBzDTP+oborbkoa041W2
YIhmszlcA6X6y8mSJwQ+UCwmQ0+l+h1RXtrW8Cc7SIQbgeGLOweRisyEtK0D/iQYkERbaPbgsFMunpA
+gJSA2rEFrifACRnQruWy9fuv6WlEtVqBzaKpVmGJ0eJkbb4SSiAsOv0+PEm56jmiKFRnSqQxyLZpUD
sG8IvOcFyfR7osrPGBSpLAEp+hlQyZmA29pPX9dQMsQzARYL6og/RpoEN6fFaa0o5uAMmnxuwE8EEEo
CLCmlAIIKe834eeyg2KvAdWpUdavP+Sz7fUC8D3X3l/NfZMPIadY8EXbe5Sm3izyocibTp8qbTj5Ttd
aH3YkMpjnBsKRkSvh4R1iC6vW7zXDkPQ0YlZ2ozACR6dPXCxB6DlPVqsSC8kJGUlohhP0emsTem5RAZ
IlwZWWNeK6tU8o5Y0Vz2SVYDZUWMwyhZujmPUONliEhEjWKbWi2J4NCSrDEtCOhKmgWWWvyB974sj2d
qHUmCqaS1kpEkhmahKmNHm4ta+PgSlf1Gfwj0aUctfsFyt73hfHErXP5bLBA1sJg2ZIBUS04jNxUs94
ASFxIDCXQq2yUtet0j5WL6hD5M6AJ1idJSqMVYyysd01ZFm9CnWJR2ik49i+cNkw59mNDFfoQft4Zsy
YiBsqQjJCR2ztJUzdomiZnta+jQb3sepMWt+nKoXkg18nL7V57nJzhL+LT7P/wWz+yo33cvmpwAAAAB
JRU5ErkJggg==
'''

icopg_first = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA
DdgAAA3YBfdWCzAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAATRSURBVHja7Z
dbbBRVHMa//+yZnd2ZLkW27ZbStNweClJYQCoJCZBAAC0CklKCIcHEBwkJDyZGNEok8oAJvuHtUdQnH
7Q2xgeJl0feNMZEExFNJMASZLe7O9vZmTnnOLdw0i6XrG1iYtyTL5OZ7+z5fvv/n92cJSkl/s2XFui/
AUBPEqPnKdWGP38AtJd2g+H348Xjf9B4awjta/HnD4CeolNm1pw4/ezp/kaz0Q8PC2b7mXRm4uS+k/f
02T8OLlIaA/hwsDA4fuLICbK6LJz94CyQmun3LeobPzZ6jLILsrjw/gWAgDkD0CEaxAAmRlaNFMf2jk
HqEjW7pvz9tAyD+LS4vFjcs3UPOHHU7TpgAHDnBJD0E7h4YOuBwsjmEdjchpgW4IIj+XQ7kcLbOzft7
Fm7ai1szwb3OYjTXABUP420cebo6NFMz0APyo0yhBTgkkMIEQUYhvHR6JZRI9+VR2W6AuGLSJrQYoBa
2wCqn4VHCuMHnzhIrIOh6lRVeHCVUqKruws7Ht9hsDRDrVmD4IHv8giACRYDoC2ApN9LMbFyYGVx++b
tcHUXTtOJ+iogovBoCIFtW7ahGYxpbzoO9zmEJyLpQm8fgJ6m3dBxcd2qdYXh1cOwhQ3hCvBgCBIKIh
yqFWH4XQDuxRAGjPYA6DC9zCz99Y0rNmSW9C1BzamBpziEJkLFAFoIkoQjDudiBkR8z2NI0jVIiBaA1
n4P0cemZY1tGFxPVs6KwxmHYEloSsTCvauQ7IsYRMSVICKQTpAPqgA9Q0uxhia7OvLDQ91DIKIoXOhC
LcZUIEdYkQSCRKLkOZKW8Ph9TGMq7V4A4W90T2/hu269e7BDWGi6zRm7PBySJIgRpCajCkgmIwBoiDw
pAwl5916TGggUXRtuA5QODPD7V2DX2l3Z0o0Svrn0NWRWAiaQyqVAJoEsgjAEREaAGxzIIN5U6WQVLR
AFklGGkheLeQxkavevgPxE8p4Xe7bcrt/53Bw2V/s3XDQpqkIyIQlhSagBBaEHSiUAIpAfyE3my/gZS
QJ0evB54NZbt67Iy3y9LeqTtILJjnwuXDgWS6SHALMgsoFMpeg+AzVPR7QBSdMefiCRP0hXvif2O/b0
G84i10UvhbNiKRgFYkABWOF1VjiDErVxIpLv8jN+1T8E0u7A1KAzPVpAwcxqhzkrXIcC1hB/DbWHtKA
Vwp9ElW9CSvulw+qEpVvKpFBqX7AMg8lMIK1CofJiANLaPxPKd+RV/OQXy9XylyR1uUjPAwJKPJYudG
QudyBXW6CeCyUSIUB7FVAQk7Ipz/mj9Ur1nD3leH2sH+QR4CW73QHIJjgNB873nofrWujF8kPNDUCBv
MlfbZadwzdKpcoSPgDDNYBpADZA9bjE3jX3uQBgCjdT0F09BAwVAZNsvwWtEOf9z2TZ23itdP3KwmoX
OpudQB2gGiEc+JV/gbL/GBrab51eHjmRA5oAufNQAQUhr+Kyt6ZUKX1Fd9Ky11sMLQaI/bPyCr71Hr1
dv31JY4YsZHuhhQByfgDUvnjF212ZKp+v/FX3lqWWI58xZ/oveLumapXzU6btLesPfMOcNwAV9Jp3yq
k1jvx48+fS4oXGn9AxNcN/KfCrgS8CP9fqz+ufUyIK15Pt+P//O/4b3OWd5QW/m80AAAAASUVORK5CY
II=
'''

icopg_last = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA
DdgAAA3YBfdWCzAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAATzSURBVHja7V
dbb1RVGF3f3ufMmXamlHIbLkotjQkV1EmMgA/eSBXSSBlISnkwqPAgCU88qTERjQ+Q9FXxB5gYrxExM
VEf5AnhQSzRmEYqgXBLQ6dzPTNz5lw+9+xzkpmmF7lMog+u05Xv7L2mWet8357JDDEz/k0Ixf9WAHqd
JA2RgQiL6O0PQPtI4hbGd27ZeZOGaQcUFtRfUnrbO+BiCYDNw08Pr4yb8VO0i96YT98/tH9lZ0en1ts
bINrJeTkcHTkaX9uz9rjqxKeUphhaUOmo4NjhY/HeVG9Tb1sACygUC/AtHwcyByjdnx5FL87TbupDhE
KpACSBI68eoS0DW0axXukj1Nu2AHbVRrlShu3Z2P7MdgxuHUxD4hyAQRBQqpZQrio9sJHZlUHm2UwaD
s7rc9OWADW7YaCN8tU8+h7uw94X9q6yktbHDb3sKK2mQtTKyFVzGEgP4NCeQykrZulz0Y4A2qDklEIz
VTuWdiAzmLFWrFwBu26H+5FerBXRmerEwZGD8dSy1F2di1mfhLSHerABM0NrhuAKFyImIEwBaUoIQ91
LdQ8ZrqVakyLCKlnqe3IJZ86eweTVyXGYyPAXfBWLwJi3A54NhxwIigxJIrSKQvhqHah7oYjwNYJ11f
vbtm5DIplIX7x08bx6qFf4a/7+jgOQKVD1qqigog2klBB+ZOZrk9BURPuInp5FyECtA4n+Df2Ix+OpX
/68cIpG6T3+jE/c0QjEE3Jmk/8IilyEjEsISzRHYUhNHYZmP70INMPu+FH1BOyijQuXf+WKXfkSE/wy
j3N98QBb5Uxf7SHk/TxEh9ABpCXDaujZh6SW1gfaVFN6obFwo7UrwS5j4tYEpovZ3xDwMH/CVxYegSF
Q8SowYgbIJE02GDChK0vWtYFAXY0/9lkHIZ9QD+ogj8JAdR1AB+pf0o9VydSj2Vr2DO2jfv6c/fkDCM
Lt+m146oIEIBBWF00EihxVT7Gu6ADSUWaO0OQKa/plH7ABqpI6nE/hucefT5z85oPFDiEBmgBiipZiP
Kqx6D9EFMCPgglo+IEP3/MxCwRYCQvGgzGcnfr5j6vOld366RcMIAQQtjwKodkMYbYE8BRlS1f8aM/Q
1FqypwtePGC7VP4W53jk2vi1+uKfA6QoIxqac0PIyNBVpJZRuFprjm45oVav172ie5w/Ct7FHCxwBtA
gNeevabaEMCPTWjNIZKppGiZc6QMuzXhF7zU+6Z++869kJEAgtKI1TKfohJEwAKvlPJCmRsJIICm6AR
YTKPpP8klPm99FAAIxAQFm0we6qksQv5CE6Zl6HbL5mmViOcg2OZfPfYffvTR/yJfxDzDmCwCPwmhey
2yzArUZ1/UBk8oU7jnhW5Bcwhp/HbJTOdcpVsf4hP/2vX8rZgFqBKhD0/RNIC+B26Lg3qgf0iMqEVAC
YAOWY2GdvR63rk/lnVxtdI75PXXADUfQleyCJRKYrmb/Qs7diUvI0gMN8zBgt7riuSSuZ29OKn0HjzV
bfh8dIJBDSHWthkhaPG1P/4if3E38Pk9qvXEVCaudNaCpGE/NTP2Ac+7mpvl9BkBAeGxgIwrdtlso5c
f4qPsin2Znlh7fiPx02c0XcmP8lrtD6y249xGYKJT90qXs0hs9tWuVw/ym99UcnZVeUXpJ6e8o/T4x5
8cpKfDszfn1NuH/X8d/AxvQTaxLJ9wMAAAAAElFTkSuQmCC
'''

icopg_left = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA
DdgAAA3YBfdWCzAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAN4SURBVHja7Z
fPaxxlGMe/zzvv7GxmN2Zpkk3SLE3bQAkhDRtFDwr20NAFNbaCzaI3T+YPEKoopPVSoV7Vo1Q8eTHm4
EFRPPYoIuihVEExbtHsZnd2M7/e93F238iIhwayuymIzzPfmYGBeT7zfIeZ5yVmxsMMkei/CUDPkKRX
yXooAPQcVSDx00Z542daJ+tYAWiNrrkj7tbmK5ulTtApIcIjeEDIgRUuUwan8NHs5Oz6RnWDiieLuPH
hDcAChg5AV2kuKb61PL9crl6uQmYlvI4HE0MGoOepAuD26hOrUxeevoCQQ/j7PmxhAzRUAOO3Le3rV1
avZOfPzaPpN6FZQ7GCIxwg2RANHCD1u5AvrK9V1qgwXkAraP1dvHcMrXAoAMbv09gqzZTKF5+8CDki4
QUeFCnobnJvjyxnDYA3QAB6gSqwcXvx3OJUeamMkEL4oQ+VpCadQiTJxAYAAwKgKr0uc/bmytlytjRb
QjtoQ1kKWuiuDIAwIJo1SBIoI8DQfQEYvxfo41w+/+Kjcyvk5l14vgclFbQ8KGppI6RdsMjqQXA/HaC
X6TSWaHtydOL8wuQCutHyW9C2htY9GRCTxgphICTL/izofsOL01PfTMiJuZx20dnvgDMMpZUpzArdEF
IAFnodYMm9c2EJCEeAkiOgjt6BS8uXRmo7NXz95VfQWQ3KEaxRC1beArkE7WjETgzlKCAL88QZc1cZS
ZAUR+8Af8Kq+FrxqT+83c/c8+5ivBMioACaNSIdARoAJZIHRR2kEDZANgEW9TcP3H/3/l2+o1ba2tum
ecn58VHAgpE8kN0F+BfECEAuGQv6nQf4Ww75A33Zb++/7Z8IQ0wTIGCUwqQgjgGAa64NbCDh99X1uBl
fBYlduAK2tI0FKcw/7TAdIBocgIGIt9FUj8MSP+ZzY8jZufQiIQWSADlHs+BwiPf4Hr6Py/Vm/XNim0
/Y44BGKmVEipDkcIZS3uaAb8bPeo3mzfaeH52UJVBE5s8XJvIBavdvweEg76g3g7pf3anVGrPqFJzQA
fYBtAHyugBi+GM534o/5Xr02K+13+4WmhMYC8YAD6DWAC04HILv4U60VGvUvqDdDE9HMxDHCJC+F29E
lcZe/VbjTy86Y53FeNY9/pURvxVd81udl777/YfaTMH5BTb28IAY6uKUkuBDCvy/Ov4LbxV7Dx5uBrE
AAAAASUVORK5CYII=
'''

icopg_right = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA
DdgAAA3YBfdWCzAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANrSURBVHja7Z
ffaxRXFMe/585kd93dtPnhYiTBBFrRShqiaKMUhBZhrdqkCmtiX4pSUPwH2lAQn1TIq8R/oQilVR/60
Jc+tRby4EtLoaVUrCmBJrvO3NnMzL13bi93FkaEgnR3kj70LB/u7r58P3POgd1LWmtsZzHDf1+ALpND
p8jdFgE6Tw7+xKOTb51cpTNU3/oOCLwCYPLC6Qu18o7yV/Q+fbItOxCXYyx+vFgarY3epFn6nKapsKU
CHvdQGCjg6kdXaer1qXnswQ/UoPGtESAgCAPwTY6YYjTONnBi5sQ0IiMxS/X8BYpAEAXwQx885PBCD4
dnDqPxXmNXn9tn9yJ3AT824RGHH2XnyPgIFj5YKA30D2R7kZdAW7bBJQcX3MoYbDcK1QLmzszR2NjYP
CbSvchFYFNtwhcmWFpSEWFPuxfH3z6OA/sPTIOMxFmq91SACgyBDsA1B1cdZIqRsAQiwL69+3Bk8sgu
t2r2Yp4+7Z2AS2hTG37iW3jC0/cq64YdScwxNDiEY28cLVX6qzdogd21e9GLEXBtQsiEkDnhp91Ism5
YiSjdDTDg0PhBqg3vbGCSVuhDmsA/lPtSHXAYWMlADEgAnWgopaCRnpGKAAE40gGLGRzhgARhojqBne
XamxvRxrd0nl7Td7X6dwIuYcVfgVTSCkAaYkMEOJEDN3bBIgbdNkKBguJGjmuwkOHozDG8M/1uZfne7
W46QEDFoJAisuGpxARKBWhYbBFQrBbh7ingu7Xvf3oc/T5nn76bEaCs03BpcJBWguw7t4MDVAf7IUuJ
Dnz+AA9148mjJ3FXO2ADK50gYSAbnn12OjDDMCGM41h64qa+k1xHVl2MgAgoUxoY2vAsmKWY3wQIRwG
CNqQnL+pldR8vVJcjSOzSQXZCyWKr4lZQYDvQ1K2f4cnTeln/BlO9EwABwqCQkaQMsWHEgdLNVvNr/K
jO6fs6ArLq3Qi4QSDtQgyQIOxWo1hfa4rI21zSt9Rn6FQOAgzwrYClKIqoBSN4urra0s3kkl5SX8JUf
gKgVCAGXjWvUrOKP9ZXf0VT1PVSOu/8BTzCCO1G2JJ6ja99g4dyNpt33n/JEsJUaT9af3HRetZc0oui
/nx4vh3owzOu/V/W208HQ799RV+TX6CH9VKXUzKlc7rF/n87/hsFAMu0HcYh/QAAAABJRU5ErkJggg=
=
'''

icor_left = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFiElEQVR42r2XA5BcWRiF17YRd084trl
j21MZs9NjhD22bcYZBD12M+mObSfrLawxZ9+NjR68qq/q1r3nP/95fu8FABIRabFHPtpMvC/KTNQuqQ
fhuQucnbe8HG0uWpXkJP67b8sJUCEwYwHijPdLUQ0F+ZEiHBBewpnj34FpIpz+AJTqxUhTUXispfC3L
VUHcPzA1TuEGY4RToUbcYcYRvzkSFOB4pQGiLLifx1pzO9L8edhvOckDu6+eB/8odPY0bYfDbk85ESN
IsF5DBGGnOORxgKtSQeIMhXZMk35v9Rn7oZo/CxEnHPPxMYKMaJtxv5jGPHyWPojb0gcgDqs51uLReA
PnnpuRtnHURDLB8OYe5zaEVWJAgQbDNsEGrB/KVkzgpFdxyjT52dL9T5EmnH+IiEkugY81dpnOSs1DI
fYNKO9cQ/6Ow/fh7F8PPzt8rGOuQHNJSMPrRNqMvhYYcw/Tk6HxHeBmXR2nIl02h9ZCV1gb9p/B7mvf
aA0OzhEfpZvq+zXnj+Guhaio2XPfRpCRigHTGNh3qSeA/q0ldLq85iHPM2ysLGGh50b9mHx5464vb5s
lv9H0l+6srWW+E5UZPags0V8h/ZGEZKcBP9Fmou0JvUk1H+B9YrslwGFqvN9/81K2gLax5Z4UCPzlVu
4+mJvtJZzsLlWeIfKZC55cB2fTIA7LP7CXY/+se0x2sdWgketL/ncvs/XLgtt5TwK7g1aSqjTYD6KSF
Ox4qQDPDXg1z4fS3/l+lPBmp1oKhpDQ8FNkrz6EWMqTp72AAT5Wcvbon3qUJ8/iprsYVRlDiGd0U2dB
sHwpAO4qdfqu6rXnSKQ8aM0SrMDQj1MslCbM4KqjCGUpfQjO64bYYbDpycdgDQ+ILwMAhk/SqNBC6Mb
ycajJmsYFakDYIW3I8iqHoF63b89cwBqe3EyAQylE1G8thdJfpuxwqUFwTaN8NXpfLYA5uYlr1t/xXp
L0lOgsYDh56CVhbUh25DouwnRHm0ItW2Ct/bmc08NUMLofp3h0DZLn5ZJl+w0kQCxVUHW1UiJ6MTqwK
2I99l4I4CneqvgiQEYVPNsRvusNYEdet8szgpUunMUnh2lBQnva9MSrpHGGZHbsS60/cZp8DWug7tGY
9FjA5DmLKp5TtwOvYLVPVGBlo1dBlIp9c8bQHNBXKuvRSUKV/UgO3bnraOwBU4apXBTbdR5ZADnZazX
GA61szIYO/TyEtlRJay+rrLkAdgpF47oSK11fNbmWrQ4V3P59Iky1gBK1vUhL4GNdOZ2RDg3wkYh/+J
jH8XL9RvfsFZI0UuO6IgqWNnTVZ4ygLqckdGCpD7YKhdO6NFZ9Wp01ntPOuxa8xObLRQyJvITu6naUR
APyos6Au2wlE+bcFAqsXziu8BcJt3czSCnq2h1LyrTBkcb8sawvoyPDRV8MF1boS+VfFmHvq5Qe16iu
+b8VXMJWnPjPbXnJxXr0lddiXBpwoZy/o0aqpZ4gHhRnrCUyWh76utYfx7rDYMl8VdjfBpHydOrqWAc
GysE6GgQg73xAFqKuUiP3I5wJ+pwquTeINSpAWmRnWgu5tzQEC2pIbXEI8anAcTTRDb37acGIFgqpBk
bLIn8N9G/CU2FHGyqFKCzSYyeTQcx3HUMvL7TEI+dx2HhFcKNMZkja0RDtKSG1BIP4mWukG7+XJ9kpt
LJlhr00Ouuxiw05I88dwBSQ2qJB/GS6KNUfxnrHd2FzHatxQETabEbbxzewc6j4HSfxO6hs9g7foFwY
0zmyBrREC2pIbXEY9J/RkaLYpyU5nl/byAfhBU+BajOY6OjmY/RnccIN8ZkjqwRDdGSmin/NzRYEmeh
QQvZoDjP65TsLOcfl31t9zeBjMkcWSOaSfyazTz/A4R29YWpIMzKAAAAAElFTkSuQmCC
'''

icor_right = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFiUlEQVR42r2XA3RcaRzF17ZRz6SMbW5
sO6cxpxOjnNi2WTcoJsZEb9JMWNtY78EauTtf7XZm2r5z7jkf/vd+v+f3XgAgtMINJ1rDjSZGQk3GpY
XNEA3AaAJdO48hzm7i7wjDybX29jtefq4ATIMxnDr6HWbGLiA7lAc+EDdKf1rsmQGEGnJlGXpUfLDeS
F+Q7uAJvnB05vIt7SibQaTp2G+hhrxgfvKLTw0gVJ+rFqI7fDTGfhAZYRzUZI5iT9M0qL6TmN1//i4N
dRxHgvcoQvWprjAz6muRAFjaA28w9Eazwi0G/9taMgHe8Jkn09BpVKeNg2lI/RJqNG4jFECYIU+RoT9
yNCeSAod9FFTvCYHVmM8D/3SdFRiALB5qNPzXjvIp/uJHBNbAviMo2MCBrw77F3+dflOBAMhhX6NPHa
1IodDdevA+1RcMYBNzC7ytsqEvHX3ffHPtOAIs6mEvV9PvqtQ8T+BrgKk/lpUSOAz2tum71NIwjkDHX
Eh+7fqj9DzPRrn5/gFSX3vcVZMW0wYD8aQ/jMTTo4S6C0KNeWpxdtz/mmt5aG2YuKWS1A6orfCcE//S
kb1qnvdHN+uXf26LvVumsLViFK5GaVBexDygTVsrLuxzgDxYjpbGj2B75dgtNRYPQ3m5OyS+cgq+10z
72BRpa3dAcbHnv1Jfeudov8B6RegnYajhhCzTmIOGgmE0FY/c0Cg8rdKw4nPrrgeZaR+ZUfSPzM8s/8
JZS+R3QYThRHycWzdqcgavqS5vEDkb9kL8K8efln/t8TGpeRa64/Bz+5MZ7ShL7UNFej+qszkI96iC9
LzVTWT+mQME6fafTI9qR1FCN8pS+lCZMQAXgzTIzfcJfJDRSblS21G56gQRaYsM4KvV/pufWTVYwc0o
SexBRVo/9CSjoUILoj/ISBaeGbsIItIWGcBTo/U3f4tarHFoQJzXduRv7ISueKxIAPztxScGcFfffib
Qsg7hLk2I9dyGjQG7YKOWBpUlDC9hT4H5V6y3jI0LXn8iAFflRi4BiPbYivW+O5EQ0go/83I+QGSZsI
dXm5ZKZ9g0zStgtL/+WABnldo8T/2qa4d/U2AzUkJ3XwNRp8VckVsS876gi8vx9/6b5Wm+G3xbtNIZz
fMYD4G4fUgVazXsVAr5i+64tvfpkXuRu64DnialUF0S1SgogI5YQrWvaW1bzvqOsIyoPVqsh0Dc1bGQ
yT4fYl+LZOZuZMWwUbCpC0WsHhhLJ8+p0aIcn3RxDbGNtlbyuQNF8T0oYHW1ZcWyw1IYe7QYNpXz7Fe
xXnsogI1cgampdNJcQkgzctZ2oDihB1UZHGTHtsNEJmVObXFs/aNOhxKd9Z4WnVVtKZ87lxPXxfcOcE
gGP6stPqQlzFwmQWu1du0bj3wdm0qkNDnpZCBvfSdKk3pRkzWIzUUUthRTCHGogyZ93SX1xXH5agujX
VUXr1tIpL4o1lmDvilXWyz+ItOxEVtKKOIhXpLBIVn8zDZjiWTjx34PGEhmvq2zIvpyhEfNtSdiXc4Q
tpZw0VIzAfbWGdTnDyMptBWBdjWwUMi8pmC7WiSH7kZD/si1GlJLPMRLMiI8ajkkU3sR640n+iQzlkk
21lkR+m+sdx3qcoexrZSL1roJdGybRX/bEYx2ncTE4FkcHLt0TaRNxsgcqSG1xEO8JINkmcok6Qv0UW
ooHm+qQg+86qjPQk32gMAAxEO8JINkCfVZrr2K9Y7mUmaz2nKfuaTIrdcOb2/rYQy3H8f+vtOYHDp3T
aRNxsgcqSG1xEO8JEPkHxO9ZRF2covcv9eR9sMajxyUZ7HRUk+Bs/fINZE2GSNzpIbUEs9T/zXTWRFl
okIL2CK7yO2E5Dz7H1d9bfU3EWmTMTJHakT4N3z++h+JqPWERw4ITwAAAABJRU5ErkJggg==
'''

icowin_close = '''
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
'''

icozoom_in = '''
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
jWL6JoAAAAASUVORK5CYII=
'''

icozoom_out = '''
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
nwAAAAASUVORK5CYII=
'''

icozoom_view = '''
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
pTOqn8X8C7S8NXA11u60AAAAASUVORK5CYII=
'''

setico = {"exec-ocr" : [icoexec_ocr, "Esegue lo OCR della pagina o del documento corrente"],
         "exec-txt" : [icoexec_txt, "Estrae il testo dalla pagina o dal documento corrente"],
         "info" : [icoinfo, "Informazioni sul documento corrente"],
         "open-pdf" : [icoopen_pdf, "Apre un file pdf"],
         "pdf-close" : [icopdf_close, "Chiude il pdf corrente"],
         "pg_first" : [icopg_first, "Sposta alla prima pagina"],
         "pg_last" : [icopg_last, "Sposta all'ultima pagina"],
         "pg_left" : [icopg_left, "Sposta alla pagina precedente"],
         "pg_right" : [icopg_right, "Sposta alla pagina successiva"],
         "r_left" : [icor_left, "Ruota a sinistra (90°)"],
         "r_right" : [icor_right, "Ruota a destra (90°)"],
         "win_close" : [icowin_close, "Chiudi la finestra"],
         "zoom_in" : [icozoom_in, "Aumenta dimensione immagine"],
         "zoom_out" : [icozoom_out, "Riduce dimensione immagine"],
         "zoom_view" : [icozoom_view, "Adatta l'immagine alla finestra"]
         }


class IcoDispencer:
   def __init__(self):
       self.icoset = setico

   def getIco(self, chiave):
       return self.icoset[chiave][0]

   def getDescr(self, chiave):
       try:
           descr = self.icoset[chiave][1]
           return descr
       except:
           return None

   def printData(self):
       for chiave in self.icoset:
           print(chiave, ':', self.icoset[chiave][1])

   def getData(self):
       return self.icoset

... quasi 500 righe di codice per rendere disponibili le icone presenti nella immagine sottostante:
png

Se comparate l'immagine sopra con l'analoga immagine del precedente post, noterete la presenza di due "toolbars" contenenti tutti i pulsanti già prima esistenti, che mantengono le stesse funzionalità, più alcuni nuovi elementi
... tale "rimaneggiamento" è temporaneo, stiamo parlando di prototipi, ricordate?, in futuro, probabilmente, manterrò la barra inferiore e trasferirò il resto in un sistema di menu che verrà articolato man mano che verranno esplorate nuove ipotetiche possibilità da implementare.

I nuovi controlli


In questa versione del viewer sono stati aggiunti sei nuovi controlli, cioè i quattro pulsanti e le due check-box in figura sotto

png



Le relative funzioni, seguendo la numerazione, sono :
  1. Apre un file PDF;
  2. chiude un file PDF aperto;
  3. esegue lo OCR del file PDF aperto;
  4. estrae il testo dal file PDF aperto;
  5. se selezionato lo OCR o l'estrazione del testo viene effettuato su tutte le pagine del documento aperto (di default solo sulla corrente);
  6. se selezionato un nuovo OCR od estrazione aggiunge il testo alle precedenti operazioni (di default sostituisce).

In sostanza, è ora data la possibilità ad un eventuale utente di scegliere ed aprire un file pdf, estrarne il testo, che verrà trasferito ad una "nuova" finestra, e ripetere l'operazione su un successivo file. Sempre a discrezione dell'utente operazioni successive potranno essere aggiuntive una all'altra oppure sostituire ex-novo l'operazione precedente.
In detta nuova finestra, che tratteremo nel prossimo post, sarà possibile editare direttamente il testo e salvarlo od anche impostare l'ambiente per codificarlo in formato CSV, per quest'ultimo tipo di operazioni verrà aperta una ulteriore finestra di destinazione dei dati codificati che renderà possibile salvarli.
L'immagine sotto riporta una "panoramica" di uno scenario in fase di impostazione.

png


... se qualcuno ha dato 'na guardata all'immagine soprastante, probabilmente avrà pensato che, a parte il colore, le due nuove finestre sembrano uguali ... in effeti è così, sono due diverse istanze della stessa classe ma ne parleremo nel prossimo post.

Al momento, guardiamo un po' le differenze tra il corrente viewer e la precedente versione.

Le differenze in questione riconducono, essenzialmente, a nuovi metodi e proprietà implementate nel viewer per realizzare la capacità di aprire e chiudere documenti per estrarne il testo da rendere disponibile all'utilizzatore. Tali proprietà e capacità, ovviamente, riconducono ai nuovi controlli prima elencati, conservando, nel resto, quanto visto nel primo post della serie.

Apertura di un documento


Mentre nel prototipo iniziale era prevista l'apertura di un solo specifico file di esempio impostato nello script, circostanza che rende lo script stesso praticamente inutile, ora l'azione del pulsante "1" in figura, ossia della variabile di istanza "self.bt_openpdf", tramite il metodo di callback "_openpdf(self)", rende possibile selezionare un file con estensione "pdf" o "PDF" (in ambiente linux sono "cose" diverse) a discrezione dell'utilizzatore per "farci su qualcosa".
Il calback invocato da detto pulsante si limita ad ottenere il nome del file da aprire, instanziando un oggetto "tkinter.filedialog" ed utilizzandone il metodo "askopenfilename(...)", ottenuto il nome del file che si vuole aprire lo si da in pasto al metodo "set_file(...)", già esistente dal primo prototipo. Il codice del callback:
CODICE
def _openpdf(self):
       f_types = [('File pdf', '*.pdf .PDF')]
       f = fdlg.askopenfilename(parent=self,
                                title='Selezione PDF',
                                filetypes=f_types)
       if f:
           self.set_file(f)


Naturalmente, delle "manipolazioni" non è detto si limitino ad un singolo pdf, giusto quando sviluppavo le estrazioni di testo implementate in questo secondo prototipo mi si è presentata l'esigenza di citare in un elaborato ampi straci di testo proveniente da vari pdf, faccenda che mi ha stimolato ad aggiungere minimali, al momento, capacità di OCR unitamente alla possibilità di "appendere" più estrazioni di testo in successione.

Chiusura di un documento


È abbastanza intuitivo che la capacità di "chiudere" un documento aperto è associata al calback del pulsante "2" nella figura dei pulsanti precedente, banalmente "self.bt_closepdf" il pulsante e "self._closepdf()" il callback.
Il processo si limita ad azzerare le variabili di ambiente dell'istanza di pdfviewer e procedere alla auto-valutazione (metodo "self._evaluate_self()") dello stato della classe. Il codice del callback :
CODICE
def _closepdf(self):
       self.doc = ''
       self.pages = 0
       self.page = 0
       self.adapt = True
       self.zoom = 0.0
       self.rotation = 0
       self.info = None
       self.state = ''
       self.cnv_img.delete('all')
       self._evaluate_self()

La auto-valutazione del contesto renderà disponibile la possibilità di aprire un nuovo pdf per poi eventualmente integrarne i dati con quelli già acquisiti.

Estrazione del testo


L'idea iniziale per l'implemetazione di questo prototipo era di "fornire" un metodo per permettere ad un generico utente di impostare una suddivisione per "righe e colonne" del testo estratto da un pdf, in tarda risposta a un quesito posto in forumpython.it nella discussione che ha dato avvio a questa serie.
Lo OCR del testo, pur previsto per una fase successiva, è stato incidentale e, al momento, è trattato in meniera elementare, rimandando a successivi approfondimenti sulla libreria una migliore implementazione.
Comunque, al momento è possibile estrarre testo da un file PDF tanto in forma, per così dire, "diretta" utilizzando la libreria "pdftotext", quanto da "immagine" utilizzando la libreria "pytesseract" ... in linea di massima la soluzione migliore è utilizzare la prima delle librerie ma non sempre è possibile, infatti pdftotext non restituirà nulla per file pdf derivanti da scansione, quindi da immagini, in tal caso sarà necessario eseguire un OCR.
L'estrazione tramite OCR è qualitativamente molto inferiore, giusto per fare un esempio guardate le immagini giù, si tratta di estrazione testo dal pdf di esempio nella prima immagine di questo post.
Nell'immagine che segue, il testo è stato estratto utilizzando pdftotext, si nota la distribuzione regolare del testo estratto che presenta eccellente incolonnamento e distanziamento dei dati.

png



Tale risultato si ottiene dalla pressione del pulsante "4" nella precedente figura dei nuovi controlli aggiunti (quello con la "T" ben definita.

Per contro l'output di un processo OCR, avviabile tramite la pressione del pulsante "3" dei nuovi controlli (quello con la "a" sfumata), presenta una strutturazione tutt'altro che regolare, guardate giù

png



Il testo ottenuto non presenta alcuna regolarità nella sua distribuzione, anche se tale regolarità è presente nella pagina trattata, numerose esperienze nell'uso di tesseract, applicazione di cui la libreria "pytesseract" utilizzata nell'estrazione è wrapper, mi hanno ampiamente dimostrato che il testo estratto non è del tutto attendibile, mancando spesso gli accenti delle lettere accentate e presentando spesso dei caratteri "strani" intercalati in maniera imprevedibile nel testo.
Eventi non strani, tutto sommato, dato che il processo OCR in sostanza è una interpretazione di gruppi di bit formanti una immagine finalizzata a "riconoscere" la presenza di un carattere, molto dipendente dalla qualità iniziale dell'immagine, dal contrasto nei colori, etc.

Comunque, entrambi i procedimenti effettuano le loro operazioni o sulla sola pagina corrente o su tutte le pagine del documento dipendentemente dallo stato di selezione della check-box al n° "5" dei nuovi controlli (label = "Tutte le pagine"; ed in fine inviano il loro output ad un oggetto "textutility.TextUtility" memorizzato nella variabile di istanza "self.text_win", che tratteremo nel prossimo episodio di questo post, per successivi "trattamenti", tale invio sarà effettuato invocando metodi che sostituiscono un eventuale testo già presente in "self.text_win" oppure lo aggiungono in coda, ciò dipendentemente dallo stato di selezione di una seconda check-box al n° 6 dei nuovi controlli (label = "Aggiungi").

Potrebbe essere interessante esaminare e comparare la strutturazione dei processi in gioco per le due modalità di estrazione del testo dal PDF.

Riguardo la modlaità OCR, il calback "_exec_ocr(self)"
CODICE
def _exec_ocr(self):
       if self.text_win == None or not self.text_win.winfo_exists():
           self.text_win = TextUtility(self)
           self.config(cursor='watch')
           self.update()
       if self.all_page.get():
           text = self._make_ocr_text()
           self.text_win.set_text(text)
       else:
           text = self._make_ocr_text(self.page)
           if self.append_page.get():
               self.text_win.add_text(text)
           else:
               self.text_win.set_text(text)
       self.config(cursor='')
       self.update()

in primo luogo esamina lo stato di esistenza dell'oggetto "self.text_win" di destinazione, che potrebbe non essere mai stato istanziato prima ovvero essere stata chiusa dall'utente, istanziandolo nel caso.
Successivamente viene esaminato lo stato di selezione delle check-box "5" e "6" ed invocato in modo opportuno il metodo di classe "_make_ocr_text(self, page=None)"
CODICE
def _make_ocr_text(self, page=None):
       if not self.pdf_images:
           return
       if not page == None:
           image = self.pdf_images[page]
           # eventuale rotazione
           if self.rotation:
               image = image.rotate(self.rotation, expand=True)
           return pytesseract.image_to_string(image)
       else:
           text = ''
           for i in range(len(self.pdf_images)):
               image = self.pdf_images[i]
               # eventuale rotazione
               if self.rotation:
                   image = image.rotate(self.rotation, expand=True)
               text += pytesseract.image_to_string(image)
           return text

per ottenerne il testo estratto da inviare a self.text_win.

Sostanzialemnte, del tutto analoga è la logica applicata per la estrazione "diretta" del testo, il cui callback "_getext(self)" applica un algoritmo del tutto analogo al precedente, invocando però metodo di classe "_make_text(self, page=None)"
CODICE
def _getext(self):
       if self.text_win == None or not self.text_win.winfo_exists():
           self.text_win = TextUtility(self)
       if self.all_page.get():
           text = self._make_text()
           self.text_win.set_text(text)
       else:
           text = self._make_text(self.page)
           if self.append_page.get():
               self.text_win.add_text(text)
           else:
               self.text_win.set_text(text)

. . .

   def _make_text(self, page=None):
       if not self.doc: return
       with open(self.doc, 'rb') as f:
           pdf = pdftotext.PDF(f)
       if not pdf: return
       if not page == None:
           return '\n'.join(pdf[page].splitlines())
       else:
           alltext=''
           for i in range(self.pages):
               alltext += '\n'.join(pdf[i].splitlines())
           return alltext

Comparando i due metodi "_make_ocr_text" e "_make_text" salteranno agli occhi due notevoli differenze :
  1. nel processo OCR viene utilizzata direttamente la lista di immagini ottenuta alla apertura del documento corrente, mentre nella estrazione viene riaperto il documento e letto il testo contenuto
  2. nel processo OCR è tenuto conto della rotazione definita per il documento, nella estrazione del testo no.

Il secondo punto evidenzia una mia considerazione banale ma, forse, non scontata : se il documento contiene del testo estraibile esso è pure leggibile ... può darsi, però, che mi sbagli.
Per altro, se il contenuto del pdf sono immagini pdftotext non restituirà testo, al più una serie di righe vuote.
Per contro, l'esecuzione di un OCR di norma restituirà più o meno sempre qualcosa interpretato come testo, anche quando il contenuto sarà posto in verticale mentre è orizzontale, od anche sarà capovolto ... certo, sarà spazzatura quella restituita in tali casi, per tanto la rotazione della pagina diventa importante.

Vi sono molte altre considerazioni da tenere in conto già sulla sola rotazione, oltre a numerosi piccoli aspetti applicativi utilizzati, per le prime si vedrà in futuro, per i secondi si esamini e comprenda il codice sottostante del modulo trattato.

Codice corrente di pdf_viewer.py (528 righe):
CODICE
# -*- coding: utf-8 -*-

import tkinter as tk
import mydialog
import tkinter.filedialog as fdlg
import viewer_ico_and_tip as IaT
from my_tk_object import CreaToolTip as ctt
from textutility import TextUtility
import pdf2image as p2i
import pdftotext
import pytesseract
import os
import glob
from PIL import ImageTk, Image
from io import BytesIO


class PdfViewer(tk.Tk):
   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.info = None
       self.state = ''
       self.text_win = None
       self._populate()
       self.update()
       self.minsize(self.winfo_reqwidth(), self.winfo_reqheight())
       center_win(self)
       self._evaluate_self()

   def _populate(self):
       # TOOLS-BAR COMMANDS
       ids = IaT.IcoDispencer()
       p_cmd = tk.Frame(self)
       p_cmd.grid(row=0, column=0, sticky='ew')
       self.ico_openpdf = tk.PhotoImage(data=ids.getIco('open-pdf'))
       self.bt_openpdf = tk.Button(
           p_cmd, image=self.ico_openpdf, command=self._openpdf)
       bt_openpdf_ttp = ctt(self.bt_openpdf, ids.getDescr('open-pdf'))
       self.bt_openpdf.grid(row=0, column=0, padx=5, pady=5, sticky='nsew')
       self.ico_closepdf = tk.PhotoImage(data=ids.getIco('pdf-close'))
       self.bt_closepdf = tk.Button(p_cmd, image=self.ico_closepdf, command=self._closepdf)
       bt_closepdf_ttp = ctt(self.bt_closepdf, ids.getDescr('pdf-close'))
       self.bt_closepdf.grid(row=0, column=1, padx=5, pady=5, sticky='nsew')
       self.ico_ocr = tk.PhotoImage(data=ids.getIco('exec-ocr'))
       self.bt_ocr = tk.Button(p_cmd, image=self.ico_ocr, command=self._exec_ocr)
       bt_ocr_ttp = ctt(self.bt_ocr, ids.getDescr('exec-ocr'))
       self.bt_ocr.grid(row=0, column=3, padx=5, pady=5, sticky='nsew')
       self.ico_getext = tk.PhotoImage(data=ids.getIco('exec-txt'))
       self.bt_getext = tk.Button(p_cmd, image=self.ico_getext, command=self._getext)
       bt_getext_ttp = ctt(self.bt_getext, ids.getDescr('exec-txt'))
       self.bt_getext.grid(row=0, column=4, padx=5, pady=5, sticky='nsew')
       self.all_page = tk.BooleanVar()
       self.ck_all = tk.Checkbutton(p_cmd, text='Tutte le pagine', onvalue=True,
                                    offvalue=False, variable=self.all_page, command=self._on_all)
       message = "Effettua l'operazione su tutte le pagine del documento"
       ck_all_ttp = ctt(self.ck_all, message)
       self.ck_all.grid(row=0, column=5, padx=5, pady=5, sticky='ew')
       self.append_page = tk.BooleanVar()
       self.ck_append = tk.Checkbutton(p_cmd, text='Aggiungi', onvalue=True,
                                       offvalue=False, variable=self.append_page)
       message = "Aggiunge l'output di una operazione al precedente (operazioni a singola pagina)"
       ck_append_ttp = ctt(self.ck_append, message)
       self.ck_append.grid(row=0, column=6, padx=5, pady=5, sticky='ew')
       self.ico_info = tk.PhotoImage(data=ids.getIco('info'))
       self.bt_info = tk.Button(
           p_cmd, 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=8, padx=5, pady=5, sticky='nsew')
       self.ico_close = tk.PhotoImage(data=ids.getIco('win_close'))
       self.bt_close = tk.Button(
           p_cmd, 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=10, padx=5, pady=5, sticky='nsew')
       p_cmd.grid_columnconfigure(2, weight=1)
       p_cmd.grid_columnconfigure(7, weight=1)
       p_cmd.grid_columnconfigure(9, weight=1)
       # TOOLS-BAR NAVIGAZIONE
       p_tools = tk.Frame(self)
       p_tools.grid(row=1, column=0, sticky='ew')
       lbl = tk.Label(p_tools, text='Pagina:', justify='left')
       lbl.grid(row=0, column=0, padx=5, pady=5)
       # 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')
       p_tools.grid_columnconfigure(7, weight=1)
       p_tools.grid_columnconfigure(10, weight=1)
       p_img = tk.Frame(self)
       p_img.grid(row=2, 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(2, weight=1)
       
       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.option_clear()
       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'
               msg = mydialog.Message(self).show_warning(message,
                                                         title='Attenzione')
               center_win(msg)
               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.after(200, self._show_page)

   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.state = 'opened'
       self._evaluate_self()
       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):
       if not self.pages:
           return
       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):
       if not self.info:
           return
       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'
       msg = mydialog.Message(self).show_message(message,
                                                 title='File aperto con successo',
                                                 font='Courier 10')
       center_win(msg)

   def _openpdf(self):
       f_types = [('File pdf', '*.pdf .PDF')]
       f = fdlg.askopenfilename(parent=self,
                                title='Selezione PDF',
                                filetypes=f_types)
       if f:
           self.set_file(f)

   def _closepdf(self):
       self.doc = ''
       self.pages = 0
       self.page = 0
       self.adapt = True
       self.zoom = 0.0
       self.rotation = 0
       self.info = None
       self.state = ''
       self.cnv_img.delete('all')
       self._evaluate_self()

   def _exec_ocr(self):
       if self.text_win == None or not self.text_win.winfo_exists():
           self.text_win = TextUtility(self)
           self.config(cursor='watch')
           self.update()
       if self.all_page.get():
           text = self._make_ocr_text()
           self.text_win.set_text(text)
       else:
           text = self._make_ocr_text(self.page)
           if self.append_page.get():
               self.text_win.add_text(text)
           else:
               self.text_win.set_text(text)
       self.config(cursor='')
       self.update()

   def _getext(self):
       if self.text_win == None or not self.text_win.winfo_exists():
           self.text_win = TextUtility(self)
       if self.all_page.get():
           text = self._make_text()
           self.text_win.set_text(text)
       else:
           text = self._make_text(self.page)
           if self.append_page.get():
               self.text_win.add_text(text)
           else:
               self.text_win.set_text(text)

   def _on_all(self):
       value = self.all_page.get()
       if value:
           self.ck_append.configure(state='disabled')
       else:
           self.ck_append.configure(state='normal')

   def _evaluate_self(self):
       if not self.state:
           self.bt_openpdf.configure(state='normal')
           self.bt_closepdf.configure(state='disabled')
           self.bt_ocr.configure(state='disabled')
           self.bt_getext.configure(state='disabled')
           self.ck_all.configure(state='disabled')
           self.ck_append.configure(state='disabled')
           self.bt_info.configure(state='disabled')
           self.bt_first.configure(state='disabled')
           self.bt_previous.configure(state='disabled')
           self.e_page.configure(state='disabled')
           self.bt_next.configure(state='disabled')
           self.bt_last.configure(state='disabled')
           self.bt_r_left.configure(state='disabled')
           self.bt_r_right.configure(state='disabled')
           self.bt_zout.configure(state='disabled')
           self.bt_zin.configure(state='disabled')
           self.bt_zview.configure(state='disabled')
       else:
           self.bt_openpdf.configure(state='disabled')
           self.bt_closepdf.configure(state='normal')
           self.bt_ocr.configure(state='normal')
           self.bt_getext.configure(state='normal')
           self.ck_all.configure(state='normal')
           self.ck_append.configure(state='normal')
           self.bt_info.configure(state='normal')
           self.bt_first.configure(state='normal')
           self.bt_previous.configure(state='normal')
           self.e_page.configure(state='normal')
           self.bt_next.configure(state='normal')
           self.bt_last.configure(state='normal')
           if self.state:
               self.bt_r_left.configure(state='disabled')
               self.bt_r_right.configure(state='disabled')
           else:
               self.bt_r_left.configure(state='normal')
               self.bt_r_right.configure(state='normal')
           self.bt_zout.configure(state='normal')
           self.bt_zin.configure(state='normal')
           self.bt_zview.configure(state='normal')
       
   def _make_text(self, page=None):
       if not self.doc: return
       with open(self.doc, 'rb') as f:
           pdf = pdftotext.PDF(f)
       if not pdf: return
       if not page == None:
           return '\n'.join(pdf[page].splitlines())
       else:
           alltext=''
           for i in range(self.pages):
               alltext += '\n'.join(pdf[i].splitlines())
           return alltext
   
   def _make_ocr_text(self, page=None):
       if not self.pdf_images:
           return
       if not page == None:
           image = self.pdf_images[page]
           # eventuale rotazione
           if self.rotation:
               image = image.rotate(self.rotation, expand=True)
           return pytesseract.image_to_string(image)
       else:
           text = ''
           for i in range(len(self.pdf_images)):
               image = self.pdf_images[i]
               # eventuale rotazione
               if self.rotation:
                   image = image.rotate(self.rotation, expand=True)
               text += pytesseract.image_to_string(image)
           return text



# ****************************
# *** FUNZIONI DI SERVIZIO ***
# ****************************


def center_win(gui):
   ''' Centra una finestra, passata come parametro, sullo schermo '''
   l = gui.winfo_screenwidth()
   a = gui.winfo_screenheight()
   wx = gui.winfo_reqwidth()
   wy = gui.winfo_reqheight()
   gui.geometry('{}x{}+{}+{}'.format(wx, wy, (l-wx)//2, (a-wy)//2))


if __name__ == '__main__':
   app = PdfViewer()
   app.mainloop()


Nel prossimo post vedremo TextUtility.
Ciao

Edited by nuzzopippo - 9/7/2021, 12:41
 
Web  Top
view post Posted on 29/6/2021, 08:25
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,226

Status:


È giunto il momento clou di questo secondo post, ossia fornire una possibile soluzione ad una particolare domanda del post scatenante la serie, che nel corso di quella discussione definì "complessa", ossia :
permettere all'utente di definire delle colonne individuanti i campi dati da elaborarsi per definire il file CSV.

ossia, di fare qualcosa così, in sostanza.

png


dove è stabilita una serie di colonne per l'estrazione dati con esclusa la 3a ed eliminazione degli spazi interni della 7a colonna.

Come potete arguire dal numero di "bottoni" presenti, ciò che ci si presta ad esporre è un qualcosa di una certa complessità, che probabilmente mi porterà a spezzetare questo post in più parti (il numero di caratteri inseribile in un singolo ost è alto ma, comunque, limitato). Già in fase di ideazione trovai limitato considerare le sole colonne quale discriminante : e se ci fossero dati su più righe?
Inoltre, in fase avanzata di implementazione mi trovai la necessità di estrarre alcuni consistenti brani da dei pdf da scansione ...

Alla fine, le possibilità implementate in questa seconda serie di prototipi sono giunte a poter estrarre e salvare, in tutto od in parte, testo proveniente da più pdf contemporaneamente ovvero di elaborare detto testo per produrre un csv con :
  1. dati definiti considerando i soli spazi
  2. dati definiti per incolonnamenti stabiliti dall'utente
  3. dati definiti da una griglia di celle con incolonnamenti e fusione di righe stabilite dall'utente.
... il tutto con utilizzo di un'unica classe (TextUtility), non molto complessa di per se, a dire il vero, ma piuttosto astrusa da descrivere per le interazioni possibili nelle varie fasi, questo terzo post dovrò probabilmente scriverlo in diverse sessioni, con magari qualche modifica tra l'una e l'altra, producendo ed inserendo un bel po' di immagini.

Comunque, anche in questo caso utilizzo uno specifico modulo di risorse serializzate per icone e tooltip, text_ico_and_tip.py, il codice relativo (402 righe) :
CODICE
# -*- coding: utf-8 -*-

icoclose = '''
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
'''

icoconf_col = '''
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
'''

icodef_col = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADj0lEQVR42mIgCoyCURD6n1w9lAP+3A9
gWr3263yztl+BhNQD1QSA1ML0UgQ40iEGCOd/PKZa9fkuunwRoLhygJkliaLwrBnnhWvFXsXG2rZtI8
46fObYxrNtB6v46ce43T09Z+tWtvtHda0ne5ObVp3Tp75q5AxqQcfG/kwaz+Mf1bNhjW8veFU5xMxw/
RcKbv3GwB0LHDwdcfFewfUZ0/7TURd3zHNoDB9LGtLS9efI65/Uc1EbnMC7yn5m+uvfIPATaXyPf1qP
h71Z0jOgx27+2rz3zzQ3sTE0NsBDXqtWbQg8f/8iC391/ObN2wM97l4oEAgev2bNhuO12mrU69RrsG5
lDbf+AHfdyjoajTWs1/Jeu3bjcdlEtm3bdXzDhi3weuumjfgkPuFu3bQJGzdu9ZvGCeJisQaqwcCBpm
mA08cNX6pwtCbGx8dx6tRJ3uVyQ4p09eoN8Mp1BwBMXP2ZClh9dLtdtNttTE5O0CREj0ymCCrbtqEoC
lyjgys+VmAqEzhz5gzGxsagqgoymbI0QLW6GlTD4RCWZQNDHZd/1GdebTSbTXbzSfImwqJHPJ75PbkL
wzAAq4c57/UBs8NF/X6fndeRSGSlAQqFKqg8HyIw590eYHW5Xtd1TrhUqoseK1bE/fREAdBx6RtMPFB
4gF6vB9PUEQ4npAHSaU7R9wEcXPJal3vRslI7jsMolkSPJUvCvtBLf+5LTOzym1PzNVy6NCINkEzmML
NcnPNSxw+gqioLYAVTXLRo+Syxg9CzHT8ArV+r1cLixWFPLKM4jQCYR5smQwFoGcgriCIRiMwSDxF6o
sM2Cp85PUSdTgfLlkU9sZTi9Ao9TgEsWkbS0zaY4rJlMVH8cAuAxoU0e3qVaJaCWErR87A5fvLRNJVR
XCF6RCIJUXxvk/D560/baDQJOYGI6HEPeTj0BpCeAjACARRjsZREPGAiToGvYTyeFsQCRcFjCNM0OX5
d17B8eQDFRCIjiu9qec8EMzBg2xZSqawollOkAN4HjiZCJIIplkrV46lUjt+AuphJ4I5vx13a0nE6nU
Mmk0exWJH+C+QU6RM/IArUIsVCoRxoeM4rmphUPp4oSgN4HzjHsUHjQn+lLnpDR+hvVCqVly7j9J8Uk
RxJgEqldjybLfIbUJfzadzOlpG23rlcrohyuXb8vwgwgmX8BwFG6nHhG6pMPHqPC19tT9/+bx4jr98A
ooD8AIDL8E4AAAAASUVORK5CYII=
'''

icodel_row = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFMklEQVR42u2TA3BcXxvGT23btm3bDGp
mM7GdGrGmzabcTG0zmC+pbdvadLO4a9znO/dOiptyM5P/qL+Z37z7nmf33qMl+cn+P+R7SD6yI6deGj
nkxNFSRcO+HztesWzYuYF9TpD85ljd6qmq8GXIWhyILYVJFKFsKUSiZCsXQbUoEIeqlTtC8pOd5Uuve
eHvhnuTbfHAWwT/0oVvPfB3xj1bezxxsMW2ssVWkfxiY07dULRA0j1ne1wfPwn3ZtniXJfuuGkzEOuL
FUgiHCdPniT5jbhSiXunG1ZGavkKuNS+IcSVS94jlFgqT1paGo4dO/ZT85IdOXIEt+/cwcnMTN26CWO
YO10a4RoheNCiNl/vdGuEiLIlksgXDhw4gF+xdevWPGUZp8/gfNhKw4vOrfGBEFzq0wXd6tV8fqlPZ3
yk/eserRFer2Yi4di1axc4TGazQA6JRJKnLC0zE0fmzTXJGzfA4z494Ny/XzqAjv2KFI5/NqA31K1bI
K51k6OE48tKjEaTQI61a9fmKTtKj4LyKW3+XPmG6dOuAXCgDiAU10oVomI7tUojX0jatAkceqE8UWKx
1ZmFuvvoUbDAJxOQagbiDICI1r7kO0JqVyY8WLoUei9P6F2coXem+vjQzy4wUBEUBIOnB9/z+vnx3+G
z4GAYvLygd3WF3oN+x9+f/w6Xw9+f1QcGsrqwMKVOnHRXt23bTv2u3Z5wdeXfKsBkZwfs24cvsAf2w3
LvHjgWrl4NbN+KLxjpS1i1GnyWlASEhoCDzc6GeehQsODBppMngd27LVzEcnfj8EGWNkcAdCa50TdrB
g5TQgLUeiMYM6DftAW4fAVLl68ArlyFjmaMBVAbTNCKHGH5IKXZcliuXIPOwQEMaPb2PUwdO8JoZrGF
XmxLWLhWk7zpJmNmExmZ3EO1aIkI5csSdalSRICGEHAo3n6AMToaSq0ecvoiSJIRumIFjACYjExoY2K
hNFmg1BnA0kmEhofzmTL9f9DNnAk5ANXLN0Dbtkjeuxc64KN6+cp7quRNO+R68xyFVNZbTkhZkhuGm4
BOh69IJPiCa0QEcPs2ONinT2HeuBFfcKMTAN16nkePwDo6gkenw6qDB1nkwJ47Bzx7ChZIB9CD5MZCC
LJ/opwKvlqV8ZoIYbOpsqrVFNkdO92VDR+xQ25j50EXW4nkxmvZMmgByITyY3MDAqzO1NTYzZu4+on2
6dQEqkiZ8zdkSpcmAnx9faHRmZAlUwrkxkQODlZnjFqPuLhYrkppn0FNpDorGd0A8jM83N3AaI349Fk
hkNGaMHv2LL5akynoBKIiI6HQ6KW0z6AmUp2zVdqfT8DJyQksALXWKJAbmz5tmtWZ0QyEh4fBZMEnRm
NIpyaoNAaR1mDhj8DNzY0IcHFxga3tZNjYTPpeOmYDJydnvlqTTZo4AWKxmJ0xYwZ8ff3Uy5YtfxwbF
3dwg0TiTxdbjeQmJCQEBjOgYPQCzSwwYfw4Wq3LdEZg8eJF0NNTlat06dQEuVIrUmlMfXMWTAR4e3tB
ptDgzfssgXKVHiNGDOerNVmWXI2QkGB8lmuktM+gJlKdP2Ypfn4HHB0d+R+9eivFq3e8/GeZUovBgwf
x9a8zqlTGIMDPD9JsRkqzDDqWSHV+L/3FBKZPn85vm0yhFcgdS79+fWm1LlPrQM/eGxoDpLTPoCZSnR
VqAz8BGxubHycwauRIjBSKUaNGgn6ZVuuyYcOGIioqkqsG2l+hSqheo0ePHmRnZye8hPb29uQ/RrAD/
/jH/wGPYJ6zxUytSgAAAABJRU5ErkJggg==
'''

icoedit_csv = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADfklEQVR42uzUA5BbXRwF8Oxn26xt2+a
o7qC27TZYW1nbZpw1Xta2PajbUfHvaefWeKl5Zn7Lh5MrwYf4enoudLS2VugyONVrKRAfF5dhfPIkaR
Sq6zm63DX9+vUzeKUFoqKi7Pw9PclDKqXSomKqqa4e/0oLmFtYzEtLTCTJsWOkVamppaHB45UWGDZs2
CcatbrNzsyM3F1cqL6m9qJaq/32lZaIjokxjg0PJysjI8rmsqi2unrNKy2wffv2HmkpKdeNTpyguOgY
6mhrSxe86qhUqlQPJyfydnenmsoq0nFcr1dawMfXd3WCSkUYBcrPyaXG+nqTV1pg9OjR3yQnJV20EIt
JFhtLHa1t7f379/+E5zYD+AK+AVz7nJHJZO7hgYGEk5GqKiooPz9/GvvXx/AHdLrHf/An/Azfwk/sml
/g2SIUicamp6SQ5PhxysvOIVlcvBv+3AO6wnfAl49gIvDlM/gUHsp3sbGxTU4YgYiQEKqtqgnRtzx7+
Uw2Kg/md+gFvaE7/APDYLGA5SfoD/+HhoUdVGANmAqF1Nrcell4Qvi9gD9fw3w2BbfzB5um/+HLB67v
AydhCqDJzXYsy5cv/zMlOfkqdgMVFxZQri53I8/Lu8As9pLv71kfnz1mlObCYfj3zgZgv/zCHiCQy+V
xkZgCpUyG7dhY8JgXf86GfBB0ZkV+hMflG9gGS1m5xwfrYGlhQQHJoqOppbmFUpNThzxwSQ9YCSOgJ9
8DWcGjMAD4s3Pnzs85jjujjI+nIhSpqa6xZ//6kn2CAzCQDSlfhsAeNjr6R6vV2qclJZECJTAKZ37++
efbDxoOBqBPJsDqZzqkXFxcBus4jiJDQ28dSgG+gWvYi/XNVJgHz57gwMA8Y2xHV6mUSopKwgX6ZxyM
gefLKaFwo6ebG4mPH8eWLOqYM2eOPkM5mC2258/AgQM/jomObj5x6BBlpGeQ1FG6kOeWrtD3gX3fA0a
y9fD08fHxEVsaG5Onqyvp0nU3hq+KFgViS2gZbw3EoDQTDMqmUL4CWQ6IjY2VrKuuftPe2Pj/4b0HC/
BUy3XQ7FkBLR8UQTQ8u1ICsjMy/LJSU//dun5zO5oUMzSlTwPiUmhNaAPFSgzUBP5eXpO2btp8DsaHJ
rR+aDCzMdAaGOvrcwLrhwu8vLxSQG4kEAeCLaYnqCopsU6IT5BiGE4AAM//b2y90yMPAAAAAElFTkSu
QmCC
'''

icoignore_col = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFh0lEQVR42u1UA3AsDRPcz7Zt29azbdu
MbfO/2M6zbdtWeBfrzL277f9mK9qtfT+/FF9XdfVmemZ6cmK6Euv+jb+6K8OXt+qJfj23b37k/vDOta
1PPx5+9K9ftzNdja2vv7hLGx6EJn93FN7LRFOt8B4mujnMD1o/D2x84YnNTFdi+ZOPppW7LcSVESNxz
Wkm3B677+I113m4OmoMbs0YheLHH0jssvDsVs26/67UK/PG4NyQ4bg6eRSOfvcjLozshswH705lCDt2
7GC6GinPPnT10NvPYtdTT+Hkl28j5dmHr1I9jmnFrl27sHXrVmzZskXAddu2Aenp4GQycLGx4EJCYA8
Kgp3U0xPw9sa6nTvFc/yuXbt3Y8eBA8gePxaXv30bZxkG1z58ldfL37+DyMcfSu34uqxbh9vB4uUFOJ
ZCoQAPlgVyckAwjBqF22H5qtU4FRuDim8+QR3D4PhPX2Horz/i+I9f8H/Lf/wUEW+8nMwQVq5cCQJrt
QkJwDRvHiCXg7VzYJuaYJfJwHLgYejVC6zEHKFgxXJsmz0Lynffxs2fvoPMaSkIMmcn3Pr5O+g++RDx
n763hSEUFRWBYLawQgIwjhwJ3qtvgC06BmY7x9e5jRuh//13mCXmCBkZGSDsW7gA6729QbC0Huc3eCB
iv/ig48OXm5sLgp0TkgNg6NEDMJuB6GjYAZ7IyQVqaqD74ANwEnOEpKREdEJ7HyE3Px+U6/3qswyPvL
w8xMXFQczw5GSge3eEpacjzvEcl5gIXlNT+RrefRfhaWmSs1lZWaSSpDymM5KSklo/A3YBLQB0jhAC/
7JnZMG+/yBfR0kJNI8+CovEHCEyMhK4jUd5ggPiHVeZLDaoNHohAWiefQYEfWwctHv3Q2W2QrtzN2xr
1kDFMHyPeI52hQQHk0p6lCc4ICoqCgYTixaVVkg64IEHYHW8ZOpde9BsZKHZth26mDgQlAzD94jnaJe
fny+ppEd5ggNCQkJg4wC9gRVQB0DNMMC1q9CyHCwbNsEqS4DWhvYDqEc8R7u8PD1JJT3KExwQFhYGFx
cnODsLOc/XF45OzAkIgJOfH5wcXydnd3de5wQG8t58P1/xHL8rODiYVNKjPMEBXl5eMLOAWmOCWsuTf
1a1/pe4cgWslw9UJhtURivs48YDVit5fI94jnYtXrSQVNKjPMEBLi4uaNEYUV3XLGAV23qA42K5yoDq
Zg1Mffqi+uJVEMijHvGc0rFrzpzZvIo9yqE8wQGLFy9Gs0oPRU2jgJVmOx9CqKhXwvzrb6i5cBkVdS3
A+XPk8T3iOdo1ffpUUkmP8gQHzJ49G2o9i7pGtYA1NvBfNR5ffInGSzdQ16KDOr8YcHIij+8Rz9Gu8e
PGkkp6lCc4ID0tDRER4YgIDxUwMFEGjmE4r+RkhDoYERmBEJkMYbJ/gGp2h0c94jnalZyUxJFKeZQnO
MDxnbXckCsLz9+oWd7O69XLTyutBUrmLhaK6paTN2oKSgtW79QMHHrp5M3aAti5ZiXDsNRDvZ1nHbsK
fH18QMrXhF4h5QkOCPD3O3ihpNb1xMUSzw7e8jxSp3Wpf+31S1rg8PnMwojqiVNWHa1odDu5/4SvITq
mrOHtdy9SD/V2nnXscg4Lj1CQ8jWh50p5ggMCA/yjL5fU9zp1oaTvqQulPOn5WI2up+L7HxNU5Yrkss
kzFx8vb+597sDZQRVjJjkrgWjFdz8mUI947kpJQ/eYmPj1pGKPcihPcEBwcNBIeZ352fNX5C905pkG4
/OVvQd+q9+2Y8ihasMz58/derFq7pJPj16SP6cH/qjs0e9b6hHPKerNz8TGxc8iFXuUQ3nin+JXmX8B
AFK1u/7VTGBg4CuShjgvNDRUoH8HpHdKe3dwB/8E5E6ZPNRCNx0AAAAASUVORK5CYII=
'''

icomake_csv = '''
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
'''

icomerge_rows = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAETklEQVR42sWWA5TsSBSG17Zt27Zt27Z
t27Zte7ufMWjbtjG6e/9MXk6lN9PJLPucr5Wkvr/q3mA2IvpfUf3g15rMkf8ya3YLcGSpXKZsNkvZXI
5y+TzlCwUqgGKRiqUSYTsoz6JSkahUqwpVUKv9iUazSXB0DZDN5iieSFIimaJkKk2pdIbSmdFASqjOY
HI4UBJCdoatcQj9ALk8i1MsTrM4QxlhNUBelqayeTrnOatEJl+ESL0iGtTrdf0AEGjJxRnnCkW66EUb
bXnNFImLX7Lz7MtiCcBfKwEkkKuWXFjqfLFE173ppO1umKbi+rdcVK7WeJlleLagLtA0EgAi9ZKrG/D
uDz20883TNbn3Y9+orNGghgatVks/ACSSWEP+xFd+2vP2GV156psgZqqGxaDdbusHQCNBjlIUBfnLPw
Zpv7tmqtjnToDval77JUItWSgyMDBgLEDnzN8zR+nge3sVjn/UQgfe3UMHsAzgO/4T9/l4chJCFYODg
wYCVCoq+RfT4nTEg30KV7/homi6TIfe26MikavRNW+6Vft+15sZfwA+jRT5T70JOu7Rfjr2kVGe/z7E
p9loNx/1YK8KCBrNNr34Y0TZH8ea7DlJDIaGhvQD4HyFfII9Tac+aaWTHrfQmc/Y6FdrGp0syVHfE3l
wAWWGwOzI4RgcK40x3Vs0HgDnMPrglvc8dPrTNrruLTcFEiWVHA11xlNWFRCIM41m63TDO15pjNs+8E
n/DQ8PGwuAy2aPN0ufTYlToVztlEuzveB5m4giFqm3BuirGWlyRKuQGwxQr1NFvozioqIlB5e/7BARx
RB1YjwApLiOI0h9DDlmez2fDQJjikdGRkT0A0Ba05h95+l0+3seka7ScQWAVG/24ImvgiJjicdfggZL
hdlryrHcr/0clXj9lxjoFGMf5ThMArSNXIqbcoBus8fgn05OKnw2JdUpxhg4naV7Si6Xk4glUy127KM
bQKi9phz83J+R+MWSBYocgSFOp9Pkdnuop7eXenp6yePzDx174olPs2Nx/QA6yw+muAsSUz1FgPpiP+
mZ0O/3k8lspqlTp1EgEKBAMEgnnXrqWzz+CvAYCtBt+YEtXFaBbTh9Y7EY/W4yyYHaFA6H6aQzTvuKx
14dDiMBxqy/eKFJFJrkT9UAvmN/6Tmi32KhSZMmKfIzzzvXxONuxMxuLECrJQbQnH3n+Y7/cO3A7E3m
CeT1ekX5lswcosdwAMiNBMA23D98Pq69yUx+rvuJp0nLvpEo/1cC4IVt6Hx0vcXhHDrhZKnhVheX3Wi
AA/hG1NJbeq0VKJUr5HD7Wsccf/zTWt1uNMByzKHMicwpzOnMWcy5zPnMhczFzKXMZczlAvh9JrM1s5
a8AiszyzNLM4sxCzHzimWZJZ6dmVPeuDCzOLMMs6I80DrMxnJDbc/swuzF7C+zD7MHsxOzDbM5syGzN
rMqVkQIsQAz96wQfwDZFf57mN2+ywAAAABJRU5ErkJggg==
'''

icono_cols = '''
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
'''

icono_row_merge = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFM0lEQVR42r2WA3BrTRSAn23btm3btm3
btm3bZvBsNk7tNs5tyvOfc2ezk/erbme+2ay/3e6eu6kAINF80n3Kiek/oPLY+qbqf+VK2jHbpBNGbH
2u6bH4lqbRuKOTsTBtfAR+qb7UxLRmRETEH1B5rAJDNj6bsPOOUlD7muGdMgjGbbovVOy7Y5KzAf6lR
VLzPPvtym/llx6Y9nA47JyoqKge339TeWw7sPq++qeHEcz2CJC7BcGdd55Qqc8ODzZZBaQmUhnJjORB
iiA5mFgWJMNP5ZcZADBDEKwc3IEZn75/mBGrQIfZl9SyX/7w4mcgXHrlCbfeeEC5nludAuWYQCUkE5L
bRSCNU+Dbr0/bo6Ojt1tsFg4KbH//Sb49VoHaww5OHrnujnD7nTg59J53XijZZdPU/9hynnfl47e312
JiYq5ZLCaOI9xx7dXbl9diFaADV6bH5kkVem3zoJXT5K1arUoXn0P45qP8U3RM9Cej2cBxOByfXsief
YpVAAASjfT1i2D8FwQbjMGcsDAh+NHzB8EpIvBM8hhQAEJCgziCYId7j25DisSBh0/viQJBIQEcu2CD
m3evQVLEgdTOlF29NAx+GO88uCkKBAT5cWx2K1y5cRESGwcqIRWRdEgZpBS7hvmRnEwq3fXbV1AgCvw
CfThWmwXOXz4DiY0DZRlp2eSuAjmcApeunYcoFPDx9+JYrGY4dfY4JCoOxJWzF08Bhl7w9vXgmC0mOH
riEKRIHDhx5qgo4Omj55gsRjhweC+kyDU8fOyAKODupeVgMILd+3Ykj8Dx43uLHD56YMLhw7tLUH7fw
d0oEAk6DzXHYAqFbTs3iwI7dqwrumnrhvEbN24sl6g4sGrVqnSHju6/L5G/iNZ76OH6rStRuMprO/ds
g0gU0LgrOaHGENi4ZT1s2LxWduny+RiNVgX3Ht6NXrdh9TkcJ0ec44Are/btHOPp5QFCmB1MuMXhEeG
gd9fClu0bRQG1zo0TagiGNetXgUanFttRewzPoNVpYOnyRZPiGgfKIxXYNSy9Z/8ufZgjDLdYBUrNL9
DoFSgjgA4lIiMjqYxD4ViLk1M9taMy6kf9l61a+j6uceAPgc3bNrynAT289eCm/iGi0v2mHSEBynOsN
jOVUz0vU6OIj68XzF0w+0eC4sC6DWvmfPj4Vjxgv1Xf4ZfyG0Gro8l4nggMCaBynqf2vgHecOnKeZg2
c8rYeMcBdgjzLV+1RCF7/VIcjFZFAxNqrRulHIX6J/9N7aj9s5ePYdKU8YpRo0ZlSvA1nL9qfqGFi+e
hhBQCg/zx/6vEQ6f4T6ie2j2XPIVxE8YoRk0ZVSjRcWD+/CmFZs6doZCjBB02TzwTHl66f0DlVP9S8h
xGjRlBKy+U6PcA+xSn79OnT8nJUyeq5K+kYMQz4efvDb5+XhzKU/lL6QsYNmKIqkuXNiWpH/WPz3uAw
d8DGdjzPBuSo23bZhXGjB+tlr+WiV+/4JBACAoOoFTMS+USGDSkv7pBgwYV2NczK5KJiaSObxwoR79Z
WgYpiRSoWbNm7aEjhmjkchnY8RFiNIaKqVQmgX79+2iqVatWg32+c7tIZETSxTMOcIGyTgE2cNGqtar
W6j+wr1YqfSm+BSW47X3799ZWrVq1FtYXRPK5CGThAvGJA7RlbOsysVVkZ6+iwiRXsmTJ1n369nLfu2
8PUEp5Jl2YtcvuOjmNl6D3gPMVxAbKzFZXEqlUokSJdj16dZNQSnkqZ/WZXSdO3DX8753JzLY4D0szu
x62/+IvE4Q1eXkwnqwAAAAASUVORK5CYII=
'''

icoremove_col = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAABmFBMVEUAAABVVVW4uLgAWPAAWfAAX+4
AY/kAZPEAa/8QbfITb/Efd/IgdvEhePQ0g/Q1hfVEQ0FISEdMTEtMk/dNTEtPTUxQT05UVFRVVVVWVl
ZXmPJZWFdZWVhZWVlZmPJZm/hbVEhcVEddnfldoP5fX19gWU1mpPhqaWltbW1vb290dHN0dHR3rfl4c
mp6enqEhISGhoaHuPuKiomPi4ePjo6Pj4+Tk5OUkYyUkY2Yw/uZwvuampqenp6fn5+fyf+jo6OlpaWo
qKio0f+urq60tLS1zfC5ubm5z/DI2fHK2vHS3/HT4PHX2+PZ3eTf5O3h5e3h5e7i5eri5+7i5+/k5+z
l6O/p7PPp7vfp7vjr7vPt6OXt7/Lt8PXv8fTv8fbv8vrw7enw8fHw8vrx7+zx8PHx8fHz9fnz9v319/
r29PL38u/39vT3+fz3+f749fD4+fr59vH59vP5+Pn5+fn69vD6+/z6+/37+PX7/f/8+PD8+/j9+/j9/
v/++PH++/j+////+fD/+vD/+/D//PH//vn//vz///H///9x83UFAAAAA3RSTlMAAAD6dsTeAAABm0lE
QVR42mIYEoCJoCygmXHargUIgmhs2+aNbds5tm3bnN++3cd+Tj1U9dqz1zw/YrBLFhulB4K5xSrbBxT
uiMPhIFfYpUvuULgJmUwmckGgSzd0g8Il8fv95IR4fT6fl5xm9iRFL1G4Ojw6Ojq4P0/l7PosfdwfAD
28QuGYOJ3OxB4pzl4CKDlG4VAvEAh0m3Fr3GpLB674pg6o/hCFfTWPx9OsBN4nzIp0zBPvgRUNUPU+C
jtupVIZnH3qeAt77HKF3O4Jv3U8zQaBundQ2P0H2Vps7ursGVywK+wLgz2dXc2LW4h3Udg2CIVCV+9L
x2vuh9eOl14XUMM2Cht8Op0u645+zBgV6RhnPqLdMqD8DRTWORQKRdwekRglGQGuSLsYKGcdhVUVk8m
0tCZjhUm2WoCqVlFYm5qcnFxua2rENLVktm0Z6NQaCksiBoMhndNyMdr5zM5JgYqWUJimfn190YZp0K
VLnUZh9Ofz8/O3/xe6dH9GURhhfX9/s/vY0KXLGkFhfGhgYGBoDLt0h8bhvb6uVurr/kL+A2G1wSXGp
7iQAAAAAElFTkSuQmCC
'''

icosalva = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA
DdgAAA3YBfdWCzAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAARsSURBVHjaxV
bfbxtFEJ77bcd2Yrd2EwoRIgWpou+8IPEU3vg3gGfUlwqJ8gStVPh/kIoEqiq1b0U0hCKI1CJMjJPac
bHv7LvbW2ZGHvt6d+ZCpNJN1jO7dzfz7Tff7p2ttYaX2ey5NbDrlwlAX/3k6u1Eq1258M6778HOm5fB
sSyADEuGgXixm6YB/GdI54ts8RLPxVEMez/9CPfv3gFprda5b69/fv39NADw/cnuw72HsLm5BRc6HTj
fuQDb26+D5zqFyI35D1lOnvUZhAH+LIR+/y+YBlOQNkie7mYZgCRJoFargWPb7JugeYW/90+KkmcYeX
5gpAC+1m6g0RwznasQQHOjCZ7nLW6wLAPeerXFvs4m50TiiuU5YYDHwTSkS+UAaDc0Gg3x+WELGXBtC
1bVYDUAiYnJqEPCMVO5ChnIxNcIAG+wJZpcKC6DngemhBp/0OVxrBSP0/GVUuUAEo248cGfHx9DUdOZ
kdbiMRKyPNg6VwOV5EpQDoCGET65vbn+vMg0/+foJKMZyIIN7rNQ0YpPp4F0U3ECMXUrDawAiKxekrJ
d+jEuQmn93zUQJwoi2o6RWl0CWb24OQaANaASfYYS4NNRrKB3PJapMi1I7cXl1lr3kE1VLkKdAaBwTP
Q1GxXZV/lEmWQ8u2CAHYrBydPxdSEDOQ1oFqFRsnSdBiXjpQbIYQ2k4yen0cC9u99Dr9eDwPdL6C8Wp
jBTXVuDJ789ymqgvARPfv2FevoNmLXFoFYrvqQEOPmCW8lJ+P99GRVrQL9YBs5egg8//gje2NkpquXK
Og8GA/j02jUwTRPjat7Sm53Ov4swjuNcYK/isYrH47EILyswmpMuc/xhU6lUwLIs3gmz2SwfXwAUCUN
a+gS78cWXdKzK5xvcuHmTViig6D6yyxcPgzKJb/qyovkzAJgHpfb0+BikDU9OKAkBoJWRz89L18sXgp
hTACigKJxOKXgWIFFL80Qt2yiKZOXCAmcVMJqAISPVahVq9ToMB4MCDRQgtGy7EEAURxAEgSQgK0yQ5
R4iKHsONKYYdK8BEOCiojg+HQPguhx8ig/FcSTlICvCotVLCSQ5X49CZIdFqPmFZKC1TQuejUbQWF8v
AqB6YMBW9sQK/AAcx4ExWn8yYV00m00Uoi9Jhf6FDcMQgdF8shCoOd+e7XYb+v0+GNg0NgLAg8uXLn1
Qqda+wVK10/tV6L711S0BRZ22JgXPiVD8KAxZKwLAMk2e73a7LF5sVUwbCAPGo4ODPzc2Nr7efuXiZ3
jDGoswCjmRPddCasVEvdCfPowouGgqXTLRkIxDNOexdwUA3fn3aDT6DpXf8jzvIhHw9pUr1g8PHuy4r
uu5juOiKF0E42ESh8onh4DmLzgVIpgwwjYcDn3bsQ8ODw9neNVY5mC6FTr30Jth5xJwDIzlo7sXzGaP
sTs0/Ue3m1Sq1RBPtUTQI0uAQIkVjcA01bVerxt4+hmE5wTPiP39/eTo6MidTCYUx8x/NXDyZ5T3H9+
a0pP7Q7enAAAAAElFTkSuQmCC
'''

icosave_space = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAArlBMVEUAAAAMau8Tbu9Jj/SBs/gODg4
AAAAAAAAAAAAAAAAAAAAAAAAAAAAtgPYAAAA8ifjy9fXLzNPb3OHc3eHr7e/r7fD5/Pynp7Wzsr7CxM
rDxMrS09mwsL+7u8fk5eny9PfLzNPLzdTz9ffb3OB0d3d2d3Z2d3doaGtwcXE+PkZHkflISFA4hvX//
/8Pb/UPb/YrfvU5hvVIkPhXnPxlo/1zrf6Btf+wr6+wsLD///8OwP+NAAAALnRSTlMAAAAAAAUHFyQx
PktPbXZ6gYiIiIiIiImLi4uLjY6Ojo+Pj5CrsLCysrq8weLpRxdpaAAAAK9JREFUeNrsj1OCA2EMgLM
a21zv1u0Y9z/YMLXb134/4wRWPHiC5+Z+WckoPDcGZA4gjmGLsdipkQXw0+7zurpgyjfqtUMQRnGSZt
CRpUkchQE6nM9wvofzU9zP2zkDMSvKMt/bxYxAB9L4laQD2yDRgfr+0/Sh3mztf/PzQaED/T5QXNVtt
rL1+aHRgfmSHbvD2fp8MujA+tZBfBYdOM88iMehgzCaHKSeJt+wyRMAmwYeYF9t7OIAAAAASUVORK5C
YII=
'''

icotrasf_row = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFkUlEQVR42u2UA3QmzRKG69r2/W3bNte
2bdtWrFWcLGLbWdu29zOGn+b9eyZe2885z6mufgfVE9C9TURl9aX2f4j4w7o30960/TnqL5vJj3pq+6
F0Cwig1IarGiP2VKRyRDiMJSeilDbr24ACqfRWvLzjIzmPYOzRfui39juo9F/zA8Yd7YunC58G+zID6
aayiDb8IeZXyrtxf8E7IQSV91l9P/5P+FPMbxSWbyWV3NxcuinEktkrzwsXIrggBBRDXPW1+fn5yMrK
QmZmZh3VvWvJCjIK8GzGs6C5hH97Ef44iKDyZ1b/602gWaRQIu2qHiA5ORkXY8mSJdeUDcgdAEojPJx
HeCyUoPLEQrYuIFAGM4EmVA8QFxcHFZfbXUeViIiIa8pKs0vxavmrHkoikB9h8fpFIF8CJTMz6RgR/a
bmxxUbCxWn01VHlYULF15Tlp2VDd1Rg/HjsE8MP0v5mZPUk6eRTJOphIheoYfpZzX/M0LnowJXXRnBA
V7XlGWmJoKhO3n0cHnIDP/Urj90jGrxaRP1sz9B9POfU218i4EpKTzGJwua09J4TEgRNOcWApPT2H5l
PzOjZj23CJiWXtFPYdfMqMi0Z8zKg+KT71RCy522+I3u3VnbXfEFu93D/UrxLzqX8SkeRJ/xwOWQNOO
OuLDmjAMqgV4zsXgPAJcEWZYxvEjGcbOkZcHeMzEi3wN4JBwzSqi/0M6ukVjvQWbKcoSudFgUl1x8xi
QtHBVvXyDIji/pQoxMcsIqSgg44IbIi3BKIqL2OrHOAMyYNg3rdUDARhluti+JIvpkSTjNVWQbTgJ92
IkVh4jjBhGf+trgcHoQtywWPoUOMWYVt9UpS0FGizRgRjrXkz4F/aGvnqoB0C5lsxtQHDhlEeG9xw2e
FyCLAiIOADOnjGeZguIjInzWymw4AYIgoFcOMGfqBHZYBSX7BHSL4+F2CDiqE/Cxj4SEpZGA4tZ55dj
2LVnNJThkoQ8U4dOf97B8Xfvl7ydscB2idmZkbJXhYqewCgKC9rvYmgfgwYyxA1F2FoCbx7qTAvw2SI
CrJttv9Gj92sM8ui+za9edMDmQEB0MTnDqoPD5i0tsafFr7ZO7hNom/LaPFX8ewDUmlR0n3ev+OciKx
0balL8PsCJnhxPf+Ql4czKHlyep8vjED3hzCo+XJ1bsvTOVw0sTa7LXK7NXWPb+DJ5lLGfZ+/OgfDyL
U5oFc9aeUfYdA5ZwOf8basPjo2yeJ8fY8cwY+/PUIVSwrjvs2so2seuUG51CBZzLkL5dUM1VZIuD5im
oRa8YESv2Ocpfm8Lhran8B+9Nt5cRgHHMJW+xU6l4XDKMZhv0RqsmLzrRu1cvtVbvXUlm4yT4+fqqVa
czWgp0BouPR7KwaQ0v/uAvgWoDILBegACVMwYLdEZrtRx7eJcunbXK+ivOrGwAr3nz1AH0bIBSZgizH
2B+q3Ok6/wBukdJuBhd2za6psx/zgQwlEo9TJG5ZESS+/wBpmQ64F8gg+qZQI3NtbSAekKrrL+KjNlZ
UagJq83PgNoeUv7ebffZp/psSJqZi/MGCA4sVQBFhmgzQLIbq4VHRtum32iV9VeeOXl4TR8NuASdWzA
WQDL5wGntBlheWLbp/AG+XbuqDFa7iOOnDThRS7NNRr16P2j1xFVkejOH8ePHwmDm9awvYQYz+57Smb
V/xXEb3dUv12rXrl21m46d0tfRaBXw7bdfa/VqMp3RjtGjRkJnsutZX8IMZvY9pbd8QReiffv2EB2A0
SLUUXYBn3/+GatXl/GSguHDh0GQFT3rS5jBzL5mu6wN0KJFC6pDu3btUO/HH/Ejs1491Zo1u5jVq8u+
//5bzJ07V60O1m9g++HMIfXr1/+qZcuW/6HatG7dmm4tNV/gAQ/4CURiEpbcw7InAAAAAElFTkSuQmC
C
'''

setico = {"close" : [icoclose, "Chiude la finestra, i dati non salvati saranno persi"],
         "conf_col" : [icoconf_col, "Conferma punto inizio colonna"],
         "def_col" : [icodef_col, "Attiva definizione colonne dati"],
         "del_row" : [icodel_row, "Elimina testo righe selezionate"],
         "edit_csv" : [icoedit_csv, "Attiva la modalità di definizione dati CSV"],
         "ignore_col" : [icoignore_col, "Ignora la colonna selezionata nella  definizione dati"],
         "make_csv" : [icomake_csv, "Trasforma le righe selezionate in dati CSV"],
         "merge_rows" : [icomerge_rows, "Associa le righe selezionate alla prima"],
         "no_cols" : [icono_cols, "Azzera le impostazioni di colonna effettuate"],
         "no_row_merge" : [icono_row_merge, "Scioglie una associazione di righe"],
         "remove_col" : [icoremove_col, "Rimuove la colonna corrente dalle definizioni"],
         "salva" : [icosalva, "Salva il testo corrente"],
         "save_space" : [icosave_space, "Conserva/elimina spazi interni alla colonna"],
         "trasf_row" : [icotrasf_row, "Trasferisce righe direttamente, senza elaborarle"]
         }


class IcoDispencer:
   def __init__(self):
       self.icoset = setico

   def getIco(self, chiave):
       return self.icoset[chiave][0]

   def getDescr(self, chiave):
       try:
           descr = self.icoset[chiave][1]
           return descr
       except:
           return None

   def printData(self):
       for chiave in self.icoset:
           print(chiave, ':', self.icoset[chiave][1])

   def getData(self):
       return self.icoset


La classe TextUtility


A parte gli import, la classe TextUtility è l'unico elemento al momento presente nel modulo "textutility.py", ben 625 righe tutte sue. È un oggetto tkinter.Toplevel che a parte la miriade di oggetti presenti nel paio di tool-bars visibili sopra in figura presenta solo un controllo tkinter.Text ed un paio di scroll-bar.

TextUtility può essere utilizzata in due differenti modalità, da definire al momento dello istanziamento, la prima modalità "text", valore di default nel parametro type di inizializzazione, viene data all'utente la possibilità di editare e salvare il testo come tale, ovvero di avviare una impostazione per trasformare il testo in una tabella dati di tipo CSV.
La seconda modalità è, appunto, "csv", in tale modalità la maggior parte dei comandi saranno disabilitati, l'utente avrà ancora la possibilità di editare il testo per eventuali finiture e salvarlo, potrà inoltre stabilire di utilizzare il carattere di tabulazione in vece del separatore impostato ... e niente altro. L'area di testo avrà un colore giallino, il file prodotto sarà codificato utf-8, non in ascii.

Si è sfruttata la capacità dell'oggetto tkinter.Text di definire alcune caratteristiche di gruppi di caratteri, tramite dei tags, per costruire una interfaccia visuale per l'utente finalizzata a facilitare l'individuazione delle impostazioni dallo stesso effettuate per successiva elaborazione, cosa non sempre semplice.
Detti tags si limitano a definire i colori di background da applicarsi a delle "aree" di caratteri del controllo, vengono definiti alla inizializzazione della finestra
CODICE
class TextUtility(tk.Toplevel):
   colours = ['white', '#f4ef93', '#D5E711', 'lightgray', '#92BDFA', '#FA92A5']
   def __init__(self, master, type='text'):
   ...
       self.pdf_text = tk.Text(p_text, font='courier 10',
                               selectbackground="yellow",
                               wrap='none', padx=5, pady=5)
       self.pdf_text.tag_config('even', background=self.colours[0])
       self.pdf_text.tag_config('odd', background=self.colours[1])
       self.pdf_text.tag_config('r_merge', background=self.colours[4])
       self.pdf_text.tag_config('not', background=self.colours[3])
       self.pdf_text.tag_config('no_space', background=self.colours[5])
       self.pdf_text.tag_config('defcol', background=self.colours[2])
       self.pdf_text.tag_raise('sel')
       if self.type == 'csv':
           self.pdf_text.configure(bg='#f4ef93')
   ...

Lo stralcio di codice vi renderà evidente che self.pdf_text è l'oggetto "Text" destinato alla manipolazione/definizione dati da estrarsi dal file pdf originario. Non indulgerò qui a spiegare come saranno utilizzati i vari tags configurati, se me ne ricorderò li citerò nei momenti opportuni, altrimenti basterà valutare il sorgente, salterà agli occhi.

L'approccio utente ipotizzato


Questo è un aspetto che mi ha fatto perdere un po' di tempo nella definizione della user-interface (sarà giusto come termine?) che si sta trattando, per esperienza so che nessuna metodologia ipotizzabile può mettere freno alle corbellerie combinate dall'utonto comune. Alla fine ho optato di stabilire una serie di "step" logici cui attenermi nello sviluppo.

Detti "step's" possono essere così riassunti :
  • è presente del testo : l'utente può editare e salvare il testo, inoltre, potrà stabilire di passare ad una editazione CSV, cosa che renderà disponibili quasi tutti i controlli nella toolbar superiore.
  • è stato stabilito di produrre un CSV : viene aperta una nuova istanza di TextUtility di tipo "csv", destinazione del testo elaborato, l'utente può ancora :
    • editare e salvare il testo;
    • selezionare una serie di righe e cancellarle;
    • selezionare una serie di righe e stabilirne la fusione (senza immediato effetto);
    • potrà selezionare delle righe ed ordinarne l'elaborazione diretta;
    • Attivare la definizione delle colonne di elaborazione.
  • si è deciso di definire delle colonne dati : vengono attivati i controlli nella seconda toolbar, dedicati alla definizione delle colonne dati, l'utente potrà :
    • definire un punto di divisione colonna, che creerà due nuove colonne sstituenti la precedente;
    • eliminare una colonna già definita;
    • definire una colonna come "ignorata" nella elaborazione;
    • Stabilire l'eliminazione degli spazi interni al testo in una colonna.

I passi sopra possono sussistere contemporaneamente (tranne l'elaborazione diretta di righe quando attivata la definizione di colonne) e sono controllati tanto dalla presenza o meno di testo nel controllo self.pdf_text, che potrebbe anche essere cancellato, quanto da una serie di variabili di istanza per controllo di stato o contenzione dati
CODICE
self.type = type
       self.state = 'no_work'
       self.edit = False
       self.defcol = False
       self.maximo = 0
       self.columns = []
       self.merge_rows = []
       self.del_spaces = []
       self.csv_win = None

variabili che vengono manipolate nei vari processi e valutate in un metodo di "auto-valutazione" della classe che provvede ad adattare i comandi disponibili nel contesto corrente
CODICE
def _evaluate_context(self):
       self.bt_save.configure(state='disabled')
       self.bt_edit.configure(state='disabled')
       self.e_sep.configure(state='disabled')
       self.chk_tab.configure(state='disabled')
       self.bt_defcol.configure(state='disabled')
       self.bt_merge.configure(state='disabled')
       self.bt_spaces.configure(state='disabled')
       self.bt_delrows.configure(state='disabled')
       self.bt_trafrows.configure(state='disabled')
       self.bt_makecsv.configure(state='disabled')
       self.sc_columns.configure(state='disabled')
       self.bt_confcol.configure(state='disabled')
       self.cmb_col.configure(state='disabled')
       self.bt_ignorecol.configure(state='disabled')
       self.bt_removecol.configure(state='disabled')
       self.bt_nocols.configure(state='disabled')
       self.update()
       if len(self.pdf_text.get('1.0', 'end-1c')) == 0:
           self.state = 'no_work'
       else:
           self.bt_save.configure(state='normal')
           self.state = 'work'
       if self.state == 'no_work':
           return
       if self.state == 'work' and self.type == 'text':
           if self.edit:
               self.e_sep.configure(state='normal')
               self.bt_delrows.configure(state='normal')
               self.bt_trafrows.configure(state='normal')
               self.bt_merge.configure(state='normal')
               self.bt_makecsv.configure(state='normal')
               self.bt_close.configure(state='normal')
               if self.defcol:
                   self.sc_columns.configure(state='normal')
                   self.bt_confcol.configure(state='normal')
                   self.cmb_col.configure(state='normal')
                   if len(self.cmb_col['values']) > 0:
                       self.bt_spaces.configure(state='normal')
                       self.bt_ignorecol.configure(state='normal')
                       self.bt_removecol.configure(state='normal')
                       self.bt_nocols.configure(state='normal')
               else:
                   self.bt_defcol.configure(state='normal')
           else:
               self.bt_edit.configure(state='normal')
       elif self.type == 'csv' and self.state == 'work':
               self.bt_save.configure(state='normal')
               self.chk_tab.configure(state='normal')


Volendo visualizzare la seguenza di attivazione :

Stato iniziale dei pulsanti
png


Testo presente, attivi solo il pulsante di salvataggio ed avvio impostazioni CSV

Impostazioni CSV attive, definizione colonne non avviato
png


Manipolatori di riga attivati, è possibile trasferire direttamente singole righe, p.e. di intestazione, impostare carattere separatore ed elaborare intrvalli di righe, avviare la definizione di colonne

Definizione colonne attivata, almeno una colonna definita
png


L'attivazione dei controlli di manipolazione è completa, sono disattivi i controlli che permettono di avviare le varie "fasi"


... certo, quanto sopra è molto schematico, solo la punta di un iceberg, sarà bene vedere singolarmente le funzioni dei vari controlli disponibili e, quindi, come sarebbe bene procedere nel loro uso (magari è spunto per una futura "guida utente") ed infine come interagiscano nel loro contesto.

Intanto vediamo i controlli interessanti

Controlli e loro funzione
ControlloFunzione
pngPulsante Salva dati disponibile nelle modalità "text" e "csv" se vi è del testo, permette di procedere al salvataggio del testo in un file
pngPulsante Attiva CSV disponibile in modalità "text" se non è attiva una sessione di definizione dati csv ed è presente del testo. Avvia una sessione di definizione dati csv, il testo presente viene "regolarizzato" al numero di caratteri presenti nella riga con più testo
pngEntry Separatore disponibile in modalità "text" permette di inserire il separatore dati da utilizzare
pngCheck-Button Tabulazione disponibile in modalità "csv" se selezionato il carattere separatore verrà convertito in carattere di tabulazione formato UTF-32
pngPulsante Attiva colonne disponibile in modalità "text" con attivata una sessione di definizione dati csv e modalità di definizione colonne non ancora attivata, avvia la modalità di definizione colonne dati
pngPulsante Cancella righe disponibile in modalità "text" con attivata una sessione di definizione dati csv, cancella le righe selezionate, o la riga corrente, senza chiedere alcuna conferma
pngPulsante Trasferisci righe disponibile in modalità "text" con attivata una sessione di definizione dati csv, trasferisce le righe selezionate, o la riga corrente, senza alcuna elaborazione, le righe trasferite vengono rimosse dalla finestra di definizione dati.Da utilizzarsi principalmente per le intestazioni con spazi interni quando i dati sono semplici e regolari
pngPulsante Unisci righe disponibile in modalità "text" con attivata una sessione di definizione dati csv, accorpa un gruppo di righe contigue e selezionate il cui contenuto verrà fuso come unico dato, ha effetto solo in concomitanza di una definizione di colonne
pngPulsante Dividi righe disponibile in modalità "text" con attivata una sessione di definizione dati csv, scioglie una unione di righe già definita
pngPulsante Elabora CSV disponibile in modalità "text" con attivata una sessione di definizione dati csv, esegue l'elaborazione dei dati per impostarli in modalità CSV in due modalità differenti:
  1. con modalità "definizione colonne" non attivata vengono elaborate le righe selezionate considerando gli spazi quali punti di divisione dati, le righe elaborate vengono rimosse dall'area di testo;
  2. con modalità "definizione colonne" attiva vengono elaborati i dati, per celle costituite da singola riga o per gruppi di righe ed i limiti di colonna come impostati; gli spazi interni vengono conservati, gli esterni eliminati, in caso di unione di righe viene inserito uno spazio.Il testo elaborato viene conservato per eventuali verifiche
pngPulsante Chiudi finestra sempre disponibile, chiude la finestra senza alcuna richiesta di conferma
pngPulsante Inizio colonna disponibile in modalità "text" con attivata una sessione di definizione dati csv ed attiva la definizione di colonne, fissa un punto di divisione colonne corrispondente al valore nello "scale" limitrofo, segnala errore in caso valore corrisponda al primo od all'ultimo carattere
pngPulsante Attiva/Disattiva conservazione spazi interni disponibile in modalità "text" con attivata una sessione di definizione dati csv, attiva la definizione di colonne ed almeno una colonna definita, è uno switch, vengono indicate con una barra rossiccia le colonne in cui i caratteri interni verranno eliminati (anche nelle intestazioni)
pngPulsante Ignora colonna disponibile in modalità "text" con attivata una sessione di definizione dati csv, attiva la definizione di colonne ed almeno una colonna definita, la colonna su cui è posizionato il cursore viene segnalata come da ignorare nella elaborazione, detta colonna assume colorazione grigia
pngPulsante Rimuovi colonna disponibile in modalità "text" con attivata una sessione di definizione dati csv, attiva la definizione di colonne ed almeno una colonna definita, la colonna su cui è posizionato il cursore viene eliminata ed accorpata alla precedente
pngPulsante Azzera impostazioni disponibile in modalità "text" con attivata una sessione di definizione dati csv, attiva la definizione di colonne ed almeno una colonna definita, Tutte le impostazioni di colonna definite vengono annullate ... in caso di pasticci per ricominciare ;)


... beh, direi che questo post è già molto lungo, interrompiamo qui, nel prossimo riprenderemo da quella che "sarebbe" la modaità di utilizzo.

Ciao :)

Edited by nuzzopippo - 2/7/2021, 08:14
 
Web  Top
view post Posted on 5/7/2021, 08:36
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,226

Status:


TextUtility - 2a parte


Riprendiamo il post da dove l'avevamo lasciato, cioè ai metodi di utilizzo di quanto realizzato più coerenti con le impostazioni dati, che abbiamo visto sintetizzata nella tabella di controlli ed uso precedente (un paio sono stati omessi, vedremo poi).

Dal post sul viewer abbiamo visto che, a discrezione dell'utilizzatore, può essere estratto il testo da una o più pagine alla volta e da più documenti in successione aggiungendolo al contenuto del controllo self.pdf_text, di una istanza formato "text" di TextUtility, editato e salvato, oppure manipolato per produrre dati in formato CSV.
La prima casistica (salvataggio testo) è decisamente banale, l'utente carica il testo che gli serve, con operazioni di edithing ordinario cancella eventuali parti non interessanti, corregge eventuali svarioni quindi lo salva come testo, oppure seleziona le parti interessanti per copiarle con i comandi del sistema in uso per poiincollarli nel word-processor preferito, e chiude li ... metodologia presumibilmente utile per estrazioni OCR, i PDF non da immagini permettono di farlo direttamente.

... Non è detto sia così banale organizzare i dati derivanti dall'estrazione di testo di uno o più file pdf in quelle tabelle "piatte" che caratterizzano la codifica CSV, tali tabelle, per essere lette, devono avere un numero di campi costante, con magari una intestazione, una estrazione OCR è certo che non darà tale "costanza", inoltre, dato l'alto numero di "spurie" che si ottengo è inadatta a tale compito, salvo casi eccezionali, ma anche parlando di "estrazione diretta" del testo (con pdftotext per ora) detta "costanza" non è garantita per niente: la lunghezza del testo si fermerà all'ultimo dato utile, con conseguenti possibili problemi di indicizzazione in fase di "lettura dati", inoltre gli allineamenti, tanto per riga quanto per colonna, non sempre sono perfettamente coerenti.
Pertanto, la prima operazione da effettuare è l'estrazione del testo da trattare, magari con contestuale eliminazione delle parti di testo ridondanti, tipo intestazione di colonna ripeture su varie pagine, eliminazione di righe testo non pertinenti ai dati, etc., solo dopo aver ottenuto il testo che si vuol effettivamente trattare dare avvio al una sessione di definizione dei dati csv premendo il pulsante "Attiva CSV" (ovvero self.bt_edit) nel cui callback (_on_edit(self)) verrà avviata una ulteriore istanza di TextUtility di tipo "csv", memorizzata nella variabile di istanza "self.csv_win"*, verificato che il corrente carattere separatore non sia presente nel testo, in tal caso si ricerà un warning, ed elaborato il testo presente in self.pdf_text valutando il numero di caratteri* presenti nella riga di testo più lunga ed aggiungendo degli spazi alle altre righe in modo che raggiungano la stessa dimensione.

* In questo prototipo non si è sottilizzato a equilibrare completamente il codice con accorgimenti quali verificare l'esistenza di self.csv_win al momento del trasferimento dati, ovvero ricalibrazione del testo ad eventuali aggiunte successive all'avvio di una sessione di definizione dati. Accorgimenti facilmente implementabili ma ridondanti per un semplice prototipo.

Una volta estratto il testo che vogliamo trattare, il "cosa fare" dipenderà tanto dal nostro obiettivo quanto dalla "qualità" di ciò che abbiamo ottenuto, se tutto ciò che vogliamo è "ottenere" quel testo ci sarà sufficiente salvarlo, direttamente o dopo qualche correzione di eventuali spurie (lo OCR ne da). Se, invece, ciò che vogliamo è definire il testo estratto quale "dati", da inserire in un foglio elettronico, database o altro, apriremo la nostra sessione di definizione dati, le caratteristiche del testo estratto ci forniranno due possibili vie di approccio.

La via "Facile"


In molte circostanze, probabilmente, ci si troverà a voler estrarre blocchi di dati numerici separati tra loro da spazi, magari con spazi interni presenti solo in una riga di intestazione, o neanche. In tal caso, è prevista una forma semplificata di elaborazione, all'occorrenza può essere editata la riga rappresentante l'intestazione eliminando gli spazi eccedenti ed inserendo il carattere di separazione campo stabilito

png


quindi premere il pulsante "Trasferisci Righe", la riga di intestazione verrà inviata "così com'é" alla finestra per il csv ed eliminata dalla casella di testo di definizione.

A questo punto non resta altro da fare che selezionare l'insieme di righe da elaborare quali dati CSV

png


e premere il pulsante "Elabora CSV", il callback relativo valuterà la variabile di stato "self.defcol" che memorizza la condizione di attivazione della definizione di colonne dati e, non trovandola attiva, invocherà il metodo di classe "self._elab_minor()" che elaborerà ed eliminerà le righe selezionate dall'indice maggiore al minore, per poi trasferire il tutto alla finestra per il csv
CODICE
def _make_csv(self):
       try:
           self._verify_sep()
       except ValueError as e:
           message = 'Errore separatore : {0}'.format(e)
           msg = mydialog.Message(self).show_error(message,
                                                   title='Separatore incongruo')
           mydialog.center_win(msg)
           return
       sep = self.e_sep.get()
       self.csv_win.set_separator(sep)
       if not self.defcol:
           self._elab_minor()
       else:
           self._elab_major()
   ...
   def _elab_minor(self):
       if self.csv_win == None or not self.csv_win.winfo_exists():
           return
       rows = self._get_rows_intervall()
       if not rows: return
       sep = self.e_sep.get()
       if len(sep) > 1: return
       if not sep: return
       for n in rows:
           text = self.pdf_text.get(str(n)+'.0', str(n)+'.end')
           words = text.split()
           text = sep.join(words)
           self.csv_win.add_text(text)
       rows.reverse()
       for n in rows:
           self.pdf_text.delete(str(n)+'.0', str(n)+'.end + 1 char')

Tale elaborazione leggerà le singole righe di testo selezionare estraendone le singole "parole" con le quali produrrà una nuova stringa unendo dette "parole" tramite il separatore, la stringa prodotta verrà inviata alla finestra per il CSV quale aggiunta, alla fine le righe di testo elaborate verranno eliminate, partendo da quella di indice maggiore.
La procedura può essere effettuata in più "fasi", una volta completata non resterà altro da fare che salvare i dati dalla finestra del CSV.

... Da notare che nel callback ("_make_csv(self)") viene nuovamente verificata la eventuale presenza del carattere separatore nel testo (TUTTO il testo), questa volta, però, non vi sarà un avvertimento ma un messaggio di errore : o si cambia il carattere di separazione (ovvero lo si elimina dal testo) o i dati non verranno processati.

Ok : complichiamoci la vita!


Non necessariamente i dati da estrarre sono "ragionevoli", nel senso di blocchi numerici ben strutturati con etichette brevi e concise, tal volta i dati da trattare sono stringhe multilinea o, peggio, sono dati numerici ma con romanzi al posto delle etichette (vezzo diffusissimo tra i miei colleghi burocrati) ... giusto per dare un esempio, un tipico caso :

png


Si tratta di un PDF in formato A3 orizzontale con etichette orizzontali e verticali multi-riga e variamente giustificate ... il nostro pdftotext (con lo OCR è intrattabile) è in grado di estrarre correttamente il testo, anche di tenere un incolonnamento approssimativamente congruo ma per le righe ... beh, non sono proprio perfette!

png



Un caso del genere comporta una certa attenzione in ciò che si fa, i posizionamenti di riga sono in genere attendibili ma non sempre i dati sono sulle stesse righe che contengono le etichette. Comunque, ci si trova in una condizione nella quale occorre indicare alla applicazione che alcune righe vanno considerate quale un unico elemento. Pur potendo effettuare tale operazione con la definizione delle colonne attivata (e senza le quali non avrebbe effetto), trovo che tale concomitanza confonde, principalmente trattando fogli dati piuttosto ampi.
La progressione migliore, una volta definito il testo da trattare è in primo luogo individuare le righe extra-dati da eliminarsi, selezionarle e cancellarle utilizzando il pulsante "Cancella righe, quindi passare alla

Definizione delle unioni di righe


Effettuare una unione di righe, per l'utente, è una operazione piuttosto semplice, gli basterà selezionare le righe da unire presenti nel controllo di testo e premere il pulsante "Unisci righe", prendiamo ad esempio l'immagine soprastante, per definire la seconda riga dati bisognerà cliccare sulla 2a e, tenendo il pulsante premuto, trascinare la selezione sino alla 21a

png


quindi premere il pulsante "Unisci righe", ripetendo poi l'operazione per le altre righe interessate

png


Le righe "unite" avranno l'intera prima riga e la prima colonna di tutte le altre righe interessate con un celestino quale colore di background, per facilitare all'utente la visualizzazione di quanto fatto.

Dal nostro punto di vista di programmatori, o aspiranti tali quale lo scrivente :D , la pressione del pulsante "Unisci righe" (self.bt_merge nel codice) avvierà il callback "_merge_rows(self)" che provvederà ad estrarre l'intervallo di righe selezionate in self.pdf_text, se vi sono più righe selezionate controllerà che nessuna di esse ricada in un intervallo di unione già definito, se tutto è "in ordine" verrà formata una lista degli indici di riga delle righe comprese nella unione, detta lista verrà aggiunta alla variabile di istanza self.merge_rows (anch'essa una lista) ed invocherà il metodo di classe _repaint_row_columns() che, tra le altre cose, applicherà il tag "r_merge" a tutti i caratteri della prima riga dell'unione ed al primo carattere delle altre.

Ovviamente, è prevedibile avvengano errori nella impostazione delle unioni di righe, in tal caso sarà sufficiente posizionare il cursore in una delle righe appartenenti all'unione errata e premere il pulsante "Dividi righe" (self.bt_nomerge nel codice) il cui callback (_no_merge_rows(self)) provvederà ad identificare ed eliminare l'elemento interessato di self.merge_rows ed invocare il ridisegno dell'area di testo.

i callbacks
CODICE
def _merge_rows(self):
       if self.pdf_text.tag_ranges(tk.SEL):
           init_index = self.pdf_text.index(tk.SEL_FIRST)
           end_index = self.pdf_text.index(tk.SEL_LAST)
       else:
           init_index = self.pdf_text.index(tk.INSERT)
           end_index = init_index
       first = int(init_index.split('.')[0])
       last = int(end_index.split('.')[0])
       if first == last: return
       for r in range(first, last+1):
           for mr in self.merge_rows:
               if r in mr:
                   message = 'Riga %d già compresa in precedente accorpamento' % r
                   msg = mydialog.Message(self).show_warning(message,
                                                             title='Posizione incongrua')
                   mydialog.center_win(msg)
                   return
       self.merge_rows.append([x for x in range(first, last+1)])
       self._repaint_row_columns()

   def _no_merge_rows(self):
       curr_index = int(self.pdf_text.index(tk.INSERT).split('.')[0])
       for i in range(len(self.merge_rows)):
           if curr_index in self.merge_rows[i]:
               del(self.merge_rows[i])
               break
       self._repaint_row_columns()


Come detto, sarebbe cosa buona e giusta eseguire separatamente le fasi di definizione delle unioni di righe e di definizione delle colonne, comunque sono processi logicamente "separati" che possono essere eseguiti contemporaneamente.
La definizione di unioni di righe, come già detto, non ha effetti se non vi è una contemporanea definizione di colonne, per contro una definizione di colonne può agire anche in assenza di definizione di unioni di righe : considererà le singole righe per definire una griglia di celle da elaborare per l'estrazione dati.
In assenza di unioni di righe l'elaborazione considererà il testo della singola riga compreso tra le ordinate di inizio e fine di una colonna, ripulito dei caratteri "spazio" a sinistra e destra, quale singolo dato.
In presenza di unioni di righe l'elaborazione considererà il testo delle singole righe facenti parte dell'unione e compreso tra le ordinate di inizio e fine di una colonna, ripulito dei caratteri "spazio" a sinistra e destra e sommato con inserzione di uno spazio intermedio, quale singolo dato.

Definizione delle colonne


Una cosa non ancora detta è che l'area di testo contenente i dati da impostare è configurata con un font a passo fisso ed ha inibito il ritorno a capo "automatico" (proprietà "wrap='none allo instanziamento di self.pdf_text), ciò per rendere il più lineare possibile il lavoro di un user ... e poi, lavoriamo sulle righe ;)
Per altro, una volta avviata la modalità di impostazione per un CSV, viene immediatamente eseguita una valutazione delle righe di testo (ripetuta ad ogni eventuale ulteriore aggiunta di testo) che pareggerà la lunghezza di tutte le righe-dati alla dimensione maggiore, tale operazione viene effettuata con chiamate al metodo "_def_max_len(self)" di TextUtility
CODICE
def _def_max_len(self):
       text = self.pdf_text.get('1.0', 'end-1c')
       rows = text.split('\n')
       maximo = 0
       for r in rows:
           if len(r) > maximo: maximo = len(r)
       if maximo > self.maximo:
           self.maximo = maximo
           self.sc_columns.configure(from_=0, to=self.maximo,
                                     tickinterval=self.maximo//10)
       # ricodifica il testo aggiungendo spazi per raggiungere sempre il massimo
       self.pdf_text.delete("1.0", "end-1c")
       for r in rows:
           r += ' ' * (self.maximo-len(r))
           self.pdf_text.insert('end', r + '\n')


Gli accorgimenti sopra esposti hanno la ben precisa finalità di ottenere una dimensione di testo visivamente "regolare", ossia che tutti i caratteri di indice "x" delle varie righe di testo presenti abbiano le stesse ordinate visuali e che comunque esistano su ogni riga caratteri in corrispondenza sino alla più alta "x" esistente ... cosa non garantita dalla estrazione di pdftotext (provare per credere).
Quanto sopra è realizzato per far visualizzare all'user il punto di inizio della definizione di una colonna, tale "punto di inizio" è costituito dalla colorazione verde-giallino di background per i caratteri con una determinata posizione "x" in tutte le righe, la colorazione è effettuata applicando il tag "defcol" ai caratteri.

Riguardo alla definizione della "posizione x" da parte dell'utente sono state implementate due possibili modalità.
Una volta avviata la modalità di definizione delle colonne dati l'utente potrà cliccare, col tasto sinistro del mouse, nel punto voluto dell'area di testo

png


vedrà comparire la barra verde-giallo indicate il punto di inizio di una nuova colonna, si noti come in figura si estenda un paio di righe oltre il testo, basterà che prema il pulsante "Inizio colonna" e su quella ordinata verrà impostato l'inizio di una nuova colonna dati.

Operativamente ho trovato tale metodo molto efficace e semplice, semplicità che, naturalmente, comporta un certo "lavoro" di implementazione, per ottenere tale "effetto" si è eseguito il binding per l'evento "<button-1>" per self.pdf_text collegandolo al callback "_on_locate(self, evt)" il quale, verificato se è il giusto contesto, provoca il refresh del controllo, in maniera da avere la "giusta" posizione del cursore, e dopo una attesa di 10 millisecondi (per il refresh) invoca il metodo di classe "_get_text_coords(self)" che provvede a leggere la posizione (riga/colonna, ovvero y/x) del cursore, che esporrà nella label in fondo alla finestra ("Cursore : 1, 8" in figura) ed imposterà l'ordinata quale valore di un controllo tk.Scale (self.sc_columns), è il binding di quest'ultimo, _column_evidence(self, evt=None), in realtà, a "fare il lavoro" di colorazione, che è indipendente dal cursore, come potete vedere nella sottostante figura

png


ed è il valore (value) corrente nel controllo scale che sarà valutato nel callback del pulsante "Inizio colonna" (ovvero di self.bt_confcol nel codice) che provvederà a valutare la "liceità" della nuova posizione e, nel caso, ad aggiungere "il nuovo" a self.columns, aggiornare il combo-box e la finestra in generale
CODICE
def _def_new_column(self):
       pos = self.sc_columns.get()
       if pos == 0 or pos == self.maximo:
           message = 'Una separazione di colonne non può\nsussistere sui caratteri estremi.'
           msg = mydialog.Message(self).show_error(message,
                                                   title='Posizione incongrua')
           mydialog.center_win(msg)
           return
       if self.columns:
           for i in range(len(self.columns)):
               if pos == self.columns[i][0] or pos == self.columns[i][1]:
                   message = "Una separazione di colonne non può\ngiacere all'inizio o alla fine di una colonna esistente"
                   msg = mydialog.Message(self).show_warning(message,
                                                             title='Posizione incongrua')
                   mydialog.center_win(msg)
                   return
               if pos in range(self.columns[i][0]+2,self.columns[i][1]-1):
                   old1 = self.columns[:i]
                   old2 = self.columns[i+1:]
                   new = [(self.columns[i][0], pos-1, True), (pos, self.columns[i][1], True)]
                   self.columns = old1 + new + old2
                   break
       else:
           new = [(0, pos-1, True), (pos, self.maximo, True)]
           self.columns = new
       self._repaint_row_columns()
       self._update_combo()
       self._evaluate_context()

Si noti, nel codice sopra, che i singoli valori di self.columns siano in realtà una tupla formata da due interi, costituenti la posizione di inizio e fine di una colonna, ed un valore booleano, inizialmente "Vero", che indica se la colonna è da valutarsi o meno nella elaborazione dei dati.
Una volta definita una colonna, sulla stessa potranno applicarsi un paio di proprietà, una di esse è la proprietà di essere "ignorata" nel corso della definizione dei dati CSV, la definiremo premendo il pulsante "Ignora colonna" (self.bt_ignorecol nel codice), la colonna verrà colorata di grigio applicando il rag "not" ... e se abbiamo cliccato "a menga" lo ripremeremo (è uno switch), il callback (_ignore_col(self)) si limita a ricreare la tupla utilizzando le stesse ordinate e la negazione del precedente stato del valore per "ignorata".
Analogamente, si comporta come uno switch il pulsante "Attiva/Disattica conservazione spazi interni", ovvero self.bt_spaces nel codice, il cui callback (_switch_i_spaces(self)) provvederà ad aggiungere la colonna interessata nella lista self.del_spaces, se non vi è contenuta, la eliminerà altrimenti, la colonna avrà applicato il tag "no_space" ai caratteri della seconda riga, che apparirà con sfondo rossastro, nel caso sia attiva l'eliminazione degli spazi interni ... si tenga presente che, se presente, l'indice di una colonna verrà eliminato da self.del_spaces nell caso sulla stessa si traffichi con il pulsante "Ignora colonna".
Per altro, al fine di distinguerle, le varie colonne saranno alternativamente di colore bianco o giallo-verdino, per applicazioni ai caratteri in esse ricadenti dei tags "even" ed "odd".
Naturalmente si è previsto il caso di una impostazione errata di colonna, basterà posizionarsi sulla colonna "sbagliata" e premere il pulsante "Rimuovi colonna", verrà eliminata e, se proprio avete combinato un pasticcio premete il pulsante "Azzera impostazioni", saranno rimosse tutte le impostazioni di colonna e potrete ricominciare da capo.

Si rimanda al codice completo del modulo per approfondimenti sui callback non esposti. Forse, più interessante è la procedura _elab_major richiamata da _make_csv, callback di self.bt_makecsv, quando self.defcol è "Vero"
CODICE
def _elab_major(self):
       if self.csv_win == None or not self.csv_win.winfo_exists():
           return
       sep = self.e_sep.get()
       if len(sep) > 1: return
       text_rows = self.pdf_text.get('1.0', tk.END).splitlines()
       if not text_rows: return
       index = 0
       while index < len(text_rows):
           # verifica se l'indice corrente ricade in una fusione di righe
           merge_index = None
           index += 1
           cell_text = []
           for i in range(len(self.merge_rows)):
               if index in self.merge_rows[i]:
                   merge_index = i
                   break
           if merge_index != None:
               cell_x = self.merge_rows[merge_index]
               index = self.merge_rows[merge_index][-1]
           else:
               cell_x = [index]
           for y in range(len(self.columns)):
               cell_text.append(self._get_cell_text(cell_x, y))
           for item in cell_text:
               if item == None:
                   cell_text.remove(item)
           csv_text = sep.join(cell_text)
           self.csv_win.add_text(csv_text)

   def _get_cell_text(self, cell_x, y):
       response = ''
       for r in cell_x:
           text = self.pdf_text.get(str(r)+'.0', str(r)+'.end')
           if self.columns[y][2]:
               text = text[self.columns[y][0]:self.columns[y][1]+1]
               text = text.lstrip()
               text = text.rstrip()
           else:
               return None
           response += ' ' + text
       response = response.lstrip()
       response = response.rstrip()
       if y in self.del_spaces:
           response = response.replace(' ', '')
       return response


In tale processo viene scandito il testo presente nell'area di testo da elaborare, in primo luogo, è definita una lista di indici cell_x* di riga costituita dalla singola riga in esame se non presente in una fusione di riga, ovvero dagli elementi di self.merge_rows che la comprendono (in tal caso la riga corrente è riposizionata all'indice più alto dell'elemento in self.merge_rows), viene, quindi, invocato il metodo _get_cell_text(self, cell_x, y)* su ogni singolo elemento di self.columns definito.

* la scelta dei nomi x, y e cell_x è infelice e fuorviante dato che x tratta numeri di riga (quindi ascisse) ed y tratta numeri di colonna dei caratteri (quindi ordinate), in futuro vi sarà indicazione più congrua

_get_cell_text estrarrà il testo compreso nei limiti di colonna (y[0] ed y[1]) di ogni riga memorizzata in cell_x e lo memorizzerà in una singola stringa che, ripulito degli spazi se la colonna è compresa in self.del_spaces, verrà restituito se la colonna non è da ignorare, in tal caso verrà restituito "None".
Il valore restituito viene memorizzato nella lista cell_text, scandite tutte le colonne verranno, quindi, eliminati i valori "None" e quindi costruita la limea dati CSV, unendo i vari elementi con il carattere separatore, che verrà inviata alla finestra per i dati CSV.

In pratica, supponendo di aver impostato in tal modo i dati da elaborare :

png



Otterremo i dati CSV sotto riportati

png



Non ci rimarrà altro che spuntare la casella Sostituisci con tabulazione, nel caso volessimo un tab UTF-32 quale separatore, altrimenti sarà il carattere separatore di elaborazione, e preme il pulsante "Salva" per ottenere il nostro file dati CSV.

Ora il codice completo di textutility.py (626 righe)
CODICE
#-*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk
import tkinter.filedialog as fdlg
from text_ico_and_tip import IcoDispencer
from my_tk_object import CreaToolTip as ctt
import mydialog

class TextUtility(tk.Toplevel):
   colours = ['white', '#f4ef93', '#D5E711', 'lightgray', '#92BDFA', '#FA92A5']
   def __init__(self, master, type='text'):
       super().__init__(master)
       self.master = master
       self.type = type
       self.state = 'no_work'
       self.edit = False
       self.defcol = False
       self.maximo = 0
       self.columns = []
       self.merge_rows = []
       self.del_spaces = []
       self.csv_win = None
       self._populate()
       self._evaluate_context()

   def _populate(self):
       if self.type == 'text':
           self.title('Testo dal PDF')
       elif self.type == 'csv':
           self.title('CSV dal testo')
       else:
           self.title('%s : Stato non definito' % self.type)
       ids = IcoDispencer()
       # toolbar
       p_tools = tk.Frame(self)
       p_tools.grid(row=0, column=0, sticky='ew')
       self.ico_save = tk.PhotoImage(data=ids.getIco('salva'))
       self.bt_save = tk.Button(p_tools, image=self.ico_save,
                                command=self._on_save)
       bt_save_ttp = ctt(self.bt_save, ids.getDescr('salva'))
       self.bt_save.grid(row=0, column=0, padx=5, pady=5)
       self.ico_edit = tk.PhotoImage(data=ids.getIco('edit_csv'))
       self.bt_edit = tk.Button(p_tools, image=self.ico_edit,
                                command=self._on_edit)
       bt_edit_ttp = ctt(self.bt_edit, ids.getDescr('edit_csv'))
       self.bt_edit.grid(row=0, column=2, padx=5, pady=5)
       lbl = tk.Label(p_tools, text='Car. separatore:', anchor='w',
                      padx=5, pady=5)
       lbl.grid(row=0, column=4)
       self.e_sep = tk.Entry(p_tools, width=3)
       self.e_sep.insert(tk.END, ';')
       self.e_sep.grid(row=0, column=5, padx=5)
       message = 'Carattere utilizzato come separatore dati (solo modalità testo)'
       e_sep_ttp = ctt(self.e_sep, message)
       self.chk_var = tk.BooleanVar()
       self.chk_var.set(False)
       self.chk_tab = tk.Checkbutton(p_tools, text='Sostituisci con tabulazione',
                                     var= self.chk_var, padx=5, pady=5,
                                     justify='left')
       message = 'Sostituisce il carattere di separazione con la codifica di tabulazione (solo modalità CSV)'
       chk_tab_ttp = ctt(self.chk_tab, message)
       self.chk_tab.grid(row=0, column=6, padx=5, pady=5)
       self.ico_defcol = tk.PhotoImage(data=ids.getIco('def_col'))
       self.bt_defcol = tk.Button(p_tools, image=self.ico_defcol,
                                  command=self._on_defcol)
       bt_defcol_ttp = ctt(self.bt_defcol, ids.getDescr('def_col'))
       self.bt_defcol.grid(row=0, column=7, padx=5, pady=5)
       self.ico_delrows = tk.PhotoImage(data=ids.getIco('del_row'))
       self.bt_delrows = tk.Button(p_tools, image=self.ico_delrows,
                                   command=self._del_select_rows)
       bt_delrows_ttp = ctt(self.bt_delrows, ids.getDescr('del_row'))
       self.bt_delrows.grid(row=0, column=9, padx=5, pady=5)
       self.ico_trafrows = tk.PhotoImage(data=ids.getIco('trasf_row'))
       self.bt_trafrows = tk.Button(p_tools, image=self.ico_trafrows,
                                    command=self._trasf_rows)
       bt_trafrows_ttp = ctt(self.bt_trafrows, ids.getDescr('trasf_row'))
       self.bt_trafrows.grid(row=0, column=10, padx=5, pady=5)
       self.ico_merge = tk.PhotoImage(data=ids.getIco('merge_rows'))
       self.bt_merge = tk.Button(p_tools, image=self.ico_merge,
                                 command=self._merge_rows)
       bt_merge_ttp = ctt(self.bt_merge, ids.getDescr('merge_rows'))
       self.bt_merge.grid(row=0, column=11, padx=5, pady=5)
       self.ico_nomerge = tk.PhotoImage(data=ids.getIco('no_row_merge'))
       self.bt_nomerge = tk.Button(p_tools, image=self.ico_nomerge,
                                   command=self._no_merge_rows)
       bt_nomerge_ttp = ctt(self.bt_nomerge, ids.getDescr('no_row_merge'))
       self.bt_nomerge.grid(row=0, column=12, padx=5, pady=5)
       self.ico_makecsv = tk.PhotoImage(data=ids.getIco('make_csv'))
       self.bt_makecsv = tk.Button(p_tools, image=self.ico_makecsv,
                                   command=self._make_csv)
       bt_makecsv_ttp = ctt(self.bt_makecsv, ids.getDescr('make_csv'))
       self.bt_makecsv.grid(row=0, column=14, padx=5, pady=5)
       self.ico_close = tk.PhotoImage(data=ids.getIco('close'))
       self.bt_close = tk.Button(p_tools, image=self.ico_close,
                                 command=self.destroy)
       bt_close_ttp = ctt(self.bt_close, ids.getDescr('close'))
       self.bt_close.grid(row=0, column=16, padx=5, pady=5)
       p_tools.grid_columnconfigure(1, weight=1)
       p_tools.grid_columnconfigure(3, weight=1)
       p_tools.grid_columnconfigure(8, weight=1)
       p_tools.grid_columnconfigure(13, weight=1)
       p_tools.grid_columnconfigure(15, weight=1)
       # gestione colonne
       p_cols = tk.Frame(self)
       p_cols.grid(row=1, column=0, sticky='ew')
       self.sc_columns = tk.Scale(p_cols, from_=0, to=0, orient=tk.HORIZONTAL,
                                  command=self._column_evidence)
       message = 'imposta il punto iniziale per una nuova colonna'
       sc_columns_ttp = ctt(self.sc_columns, message)
       self.sc_columns.grid(row=0, column=0, padx=5, pady=5, sticky='ew')
       self.ico_confcol = tk.PhotoImage(data=ids.getIco('conf_col'))
       self.bt_confcol = tk.Button(p_cols, image=self.ico_confcol,
                                   command=self._def_new_column)
       bt_confcol_ttp = ctt(self.bt_confcol, ids.getDescr('conf_col'))
       self.bt_confcol.grid(row=0, column=1, padx=5, pady=5)
       self.cmb_col = ttk.Combobox(p_cols, values=[], width=10, state='readonly')
       message = 'Elenco delle colonne dati definite (formato : num_inizio num_fine)'
       cmb_col_tpp = ctt(self.cmb_col, message)
       self.cmb_col.grid(row=0, column=2, padx=5, pady=5)
       self.ico_spaces = tk.PhotoImage(data=ids.getIco('save_space'))
       self.bt_spaces = tk.Button(p_cols, image=self.ico_spaces,
                                  command=self._switch_i_spaces)
       bt_spaces_ttp = ctt(self.bt_spaces, ids.getDescr('save_space'))
       self.bt_spaces.grid(row=0, column=3, padx=5, pady=5)
       self.ico_ignorecol = tk.PhotoImage(data=ids.getIco('ignore_col'))
       self.bt_ignorecol = tk.Button(p_cols, image=self.ico_ignorecol,
                                     command=self._ignore_col)
       bt_ignorecol_ttp = ctt(self.bt_ignorecol, ids.getDescr('ignore_col'))
       self.bt_ignorecol.grid(row=0, column=4, padx=5, pady=5)
       self.ico_removecol = tk.PhotoImage(data=ids.getIco('remove_col'))
       self.bt_removecol = tk.Button(p_cols, image=self.ico_removecol,
                                     command=self._remove_column)
       bt_removecol_ttp = ctt(self.bt_removecol, ids.getDescr('remove_col'))
       self.bt_removecol.grid(row=0, column=5, padx=5, pady=5)
       self.ico_nocols = tk.PhotoImage(data=ids.getIco('no_cols'))
       self.bt_nocols = tk.Button(p_cols, image=self.ico_nocols,
                                  command=self._restart_cols)
       bt_nocols_ttp = ctt(self.bt_nocols, ids.getDescr('no_cols'))
       self.bt_nocols.grid(row=0, column=6, padx=5, pady=5)
       p_cols.grid_columnconfigure(0, weight=1)
       p_text = tk.Frame(self)
       p_text.grid(row=2, column=0, sticky='nsew')
       self.pdf_text = tk.Text(p_text, font='courier 10',
                               selectbackground="yellow",
                               wrap='none', padx=5, pady=5)
       self.pdf_text.tag_config('even', background=self.colours[0])
       self.pdf_text.tag_config('odd', background=self.colours[1])
       self.pdf_text.tag_config('r_merge', background=self.colours[4])
       self.pdf_text.tag_config('not', background=self.colours[3])
       self.pdf_text.tag_config('no_space', background=self.colours[5])
       self.pdf_text.tag_config('defcol', background=self.colours[2])
       self.pdf_text.tag_raise('sel')
       if self.type == 'csv':
           self.pdf_text.configure(bg='#f4ef93')
       self.pdf_text.grid(row=0, column=0, sticky='nsew')
       v_scr_1 = tk.Scrollbar(p_text, orient=tk.VERTICAL,
                               command=self.pdf_text.yview)
       self.pdf_text.configure(yscrollcommand=v_scr_1.set)
       v_scr_1.grid(row=0, column=1, sticky='ns')
       h_scr_1 = tk.Scrollbar(p_text, orient=tk.HORIZONTAL,
                               command=self.pdf_text.xview)
       self.pdf_text.configure(xscrollcommand=h_scr_1.set)
       h_scr_1.grid(row=1, column=0, sticky='ew')
       self.lbl_message = tk.Label(p_text, text='Cursore : ', justify='left')
       self.lbl_message.grid(row=2, column=0, columnspan=2, sticky='w')
       p_text.grid_columnconfigure(0, weight=1)
       p_text.grid_rowconfigure(0, weight=1)
       self.grid_columnconfigure(0, weight=1)
       self.grid_rowconfigure(2, weight=1)
       # bindings

       self.pdf_text.bind('<Button-1>', self._on_locate)

   # ***************************
   # ***     CALLBACK'S      ***
   # ***************************

   def _on_save(self):
       if self.type == 'csv':
           self._write_csv()
       else:
           self._write_text()
       self._evaluate_context()

   def _on_edit(self):
       if self.csv_win == None or not self.csv_win.winfo_exists():
           self.csv_win = TextUtility(self, type='csv')
       try:
           self._verify_sep()
       except ValueError as e:
           message = 'Errore separatore : {0}'.format(e)
           msg = mydialog.Message(self).show_warning(message,
                                                     title='Separatore incongruo')
           mydialog.center_win(msg)
       self.edit = True
       self._def_max_len()
       self._evaluate_context()
   
   def _on_defcol(self):
       self.defcol = True
       self._evaluate_context()
   
   def _on_locate(self, evt):
       if self.type != 'text': return
       if not self.defcol: return
       self.pdf_text.update()
       self.after(10, self._get_text_coords)
   
   def _get_text_coords(self):
       y, x = self.pdf_text.index(tk.INSERT).split('.')
       msg = 'Cursore : ' + y + ', ' + x
       self.lbl_message.configure(text=msg)
       self.sc_columns.set(int(x))

   def _del_select_rows(self):
       rows = self._get_rows_intervall()
       if not rows: return
       rows.reverse()
       for n in rows:
           self.pdf_text.delete(str(n)+'.0', str(n)+'.end + 1 char')
   
   def _trasf_rows(self):
       rows = self._get_rows_intervall()
       if not rows: return
       for row in rows:
           text = self.pdf_text.get(str(row )+ '.0', str(row) + '.end')
           self.csv_win.add_text(text)
       rows.reverse()
       for n in rows:
           self.pdf_text.delete(str(n)+'.0', str(n)+'.end + 1 char')

   def _make_csv(self):
       try:
           self._verify_sep()
       except ValueError as e:
           message = 'Errore separatore : {0}'.format(e)
           msg = mydialog.Message(self).show_error(message,
                                                   title='Separatore incongruo')
           mydialog.center_win(msg)
           return
       sep = self.e_sep.get()
       self.csv_win.set_separator(sep)
       if not self.defcol:
           self._elab_minor()
       else:
           self._elab_major()
   
   def _column_evidence(self, evt=None):
       self.pdf_text.tag_remove('defcol', '1.0', 'end')
       last_row = int(self.pdf_text.index('end').split('.')[0])
       pos = self.sc_columns.get()
       for r in range(1, last_row+1):
           self.pdf_text.tag_add('defcol', '%d.%d' % (r, pos),
                                 '%d.%d' %(r, pos+1))
   
   def _def_new_column(self):
       pos = self.sc_columns.get()
       if pos == 0 or pos == self.maximo:
           message = 'Una separazione di colonne non può\nsussistere sui caratteri estremi.'
           msg = mydialog.Message(self).show_error(message,
                                                   title='Posizione incongrua')
           mydialog.center_win(msg)
           return
       if self.columns:
           for i in range(len(self.columns)):
               if pos == self.columns[i][0] or pos == self.columns[i][1]:
                   message = "Una separazione di colonne non può\ngiacere all'inizio o alla fine di una colonna esistente"
                   msg = mydialog.Message(self).show_warning(message,
                                                             title='Posizione incongrua')
                   mydialog.center_win(msg)
                   return
               if pos in range(self.columns[i][0]+2,self.columns[i][1]-1):
                   old1 = self.columns[:i]
                   old2 = self.columns[i+1:]
                   new = [(self.columns[i][0], pos-1, True), (pos, self.columns[i][1], True)]
                   self.columns = old1 + new + old2
                   break
       else:
           new = [(0, pos-1, True), (pos, self.maximo, True)]
           self.columns = new
       self._repaint_row_columns()
       self._update_combo()
       self._evaluate_context()
   
   def _switch_i_spaces(self):
       if not self.columns: return
       pos = self.sc_columns.get()
       for i in range(len(self.columns)):
           start = self.columns[i][0]
           end = self.columns[i][1]
           if pos in range(start, end+1):
               if i in self.del_spaces:
                   self.del_spaces.remove(i)
               elif self.columns[i][2]:
                   self.del_spaces.append(i)
               break
       self._repaint_row_columns()
   
   def _ignore_col(self):
       if not self.columns: return
       pos = self.sc_columns.get()
       for i in range(len(self.columns)):
           start = self.columns[i][0]
           end = self.columns[i][1]
           if pos in range(start, end+1):
               col = i
               break
       data = (self.columns[col][0], self.columns[col][1], not self.columns[col][2])
       self.columns[col] = data
       if col in self.del_spaces:
           self.del_spaces.remove(col)
       self._repaint_row_columns()
   
   def _remove_column(self):
       if not self.columns: return
       pos = self.sc_columns.get()
       for i in range(len(self.columns)):
           start = self.columns[i][0]
           end = self.columns[i][1]
           if pos in range(start, end+1):
               col = i
               break
       if col == 0: return
       data = (self.columns[col-1][0], self.columns[col][1], True)
       self.columns[col-1] = data
       del(self.columns[col])
       for c in self.del_spaces:
           if c >= col: self.del_spaces.remove(c)
       self._repaint_row_columns()

   def _restart_cols(self):
       self.columns = []
       self.del_spaces = []
       self._repaint_row_columns()
   
   def _merge_rows(self):
       if self.pdf_text.tag_ranges(tk.SEL):
           init_index = self.pdf_text.index(tk.SEL_FIRST)
           end_index = self.pdf_text.index(tk.SEL_LAST)
       else:
           init_index = self.pdf_text.index(tk.INSERT)
           end_index = init_index
       first = int(init_index.split('.')[0])
       last = int(end_index.split('.')[0])
       if first == last: return
       for r in range(first, last+1):
           for mr in self.merge_rows:
               if r in mr:
                   message = 'Riga %d già compresa in precedente accorpamento' % r
                   msg = mydialog.Message(self).show_warning(message,
                                                             title='Posizione incongrua')
                   mydialog.center_win(msg)
                   return
       self.merge_rows.append([x for x in range(first, last+1)])
       self._repaint_row_columns()

   def _no_merge_rows(self):
       curr_index = int(self.pdf_text.index(tk.INSERT).split('.')[0])
       for i in range(len(self.merge_rows)):
           if curr_index in self.merge_rows[i]:
               del(self.merge_rows[i])
               break
       self._repaint_row_columns()

   # ***************************
   # *** METODI DELLA CLASSE ***
   # ***************************

   def set_text(self, text):
       '''Imposta il testo nell'area di visualizzazione cancellando il precedente.
       e tutte le impostazioni effettuate'''
       self.edit = False
       self.defcol = False
       self.maximo = 0
       self.columns = []
       self.merge_rows = []
       self.del_spaces = []
       if self.csv_win and self.csv_win.winfo_exists():
           self.csv_win.close()
       self.pdf_text.delete("1.0", "end-1c")
       data = text.split('\n')
       for row in data:
           self.pdf_text.insert('end', row +'\n')
       self._evaluate_context()

   def add_text(self, text):
       '''Aggiunge del testo nell'area di visualizzazione.'''
       data = text.split('\n')
       for row in data:
           self.pdf_text.insert('end', row +'\n')
       if self.edit: self._def_max_len()
       self._evaluate_context()
   
   def _def_max_len(self):
       text = self.pdf_text.get('1.0', 'end-1c')
       rows = text.split('\n')
       maximo = 0
       for r in rows:
           if len(r) > maximo: maximo = len(r)
       if maximo > self.maximo:
           self.maximo = maximo
           self.sc_columns.configure(from_=0, to=self.maximo,
                                     tickinterval=self.maximo//10)
       # ricodifica il testo aggiungendo spazi per raggiungere sempre il massimo
       self.pdf_text.delete("1.0", "end-1c")
       for r in rows:
           r += ' ' * (self.maximo-len(r))
           self.pdf_text.insert('end', r + '\n')
           
   def _verify_sep(self):
       text = self.pdf_text.get("1.0", "end-1c")
       sep = self.e_sep.get()
       if len(sep) > 1: raise ValueError('Carattere separatore composto da più caratteri')
       if not sep: raise ValueError('Carattere separatore nullo')
       if sep in text: raise ValueError('Carattere separatore contenuto nel testo')


   def _evaluate_context(self):
       self.bt_save.configure(state='disabled')
       self.bt_edit.configure(state='disabled')
       self.e_sep.configure(state='disabled')
       self.chk_tab.configure(state='disabled')
       self.bt_defcol.configure(state='disabled')
       self.bt_merge.configure(state='disabled')
       self.bt_spaces.configure(state='disabled')
       self.bt_delrows.configure(state='disabled')
       self.bt_trafrows.configure(state='disabled')
       self.bt_makecsv.configure(state='disabled')
       self.sc_columns.configure(state='disabled')
       self.bt_confcol.configure(state='disabled')
       self.cmb_col.configure(state='disabled')
       self.bt_ignorecol.configure(state='disabled')
       self.bt_removecol.configure(state='disabled')
       self.bt_nocols.configure(state='disabled')
       self.update()
       if len(self.pdf_text.get('1.0', 'end-1c')) == 0:
           self.state = 'no_work'
       else:
           self.bt_save.configure(state='normal')
           self.state = 'work'
       if self.state == 'no_work':
           return
       if self.state == 'work' and self.type == 'text':
           if self.edit:
               self.e_sep.configure(state='normal')
               self.bt_delrows.configure(state='normal')
               self.bt_trafrows.configure(state='normal')
               self.bt_merge.configure(state='normal')
               self.bt_makecsv.configure(state='normal')
               self.bt_close.configure(state='normal')
               if self.defcol:
                   self.sc_columns.configure(state='normal')
                   self.bt_confcol.configure(state='normal')
                   self.cmb_col.configure(state='normal')
                   if len(self.cmb_col['values']) > 0:
                       self.bt_spaces.configure(state='normal')
                       self.bt_ignorecol.configure(state='normal')
                       self.bt_removecol.configure(state='normal')
                       self.bt_nocols.configure(state='normal')
               else:
                   self.bt_defcol.configure(state='normal')
           else:
               self.bt_edit.configure(state='normal')
       elif self.type == 'csv' and self.state == 'work':
               self.bt_save.configure(state='normal')
               self.chk_tab.configure(state='normal')
   
   def _get_rows_intervall(self):
       if self.pdf_text.tag_ranges(tk.SEL):
           init_index = self.pdf_text.index(tk.SEL_FIRST)
           end_index = self.pdf_text.index(tk.SEL_LAST)
       else:
           init_index = self.pdf_text.index(tk.INSERT)
           end_index = init_index
       first = int(init_index.split('.')[0])
       last = int(end_index.split('.')[0])
       return [x for x in range(first, last+1)]
   
   def _repaint_row_columns(self):
       self.pdf_text.tag_remove('even', '1.0', 'end')
       self.pdf_text.tag_remove('odd', '1.0', 'end')
       self.pdf_text.tag_remove('r_merge', '1.0', 'end')
       self.pdf_text.tag_remove('not', '1.0', 'end')
       self.pdf_text.tag_remove('no_space', '1.0', 'end')
       self.pdf_text.tag_remove('defcol', '1.0', 'end')
       last_row = int(self.pdf_text.index('end').split('.')[0])
       for i in range(len(self.columns)):
           start = self.columns[i][0]
           end = self.columns[i][1] + 1
           for r in range(1, last_row+1):
               if self.columns[i][2]:
                   if i % 2:
                       self.pdf_text.tag_add('even', '%d.%d' % (r, start), '%d.%d' %(r, end))
                   else:
                       self.pdf_text.tag_add('odd', '%d.%d' % (r, start), '%d.%d' %(r, end))
               else:
                   self.pdf_text.tag_add('not', '%d.%d' % (r, start), '%d.%d' %(r, end))
       for c in self.del_spaces:
           start = self.columns[c][0]
           end = self.columns[c][1] + 1
           self.pdf_text.tag_add('no_space', '%d.%d' % (2, start), '%d.%d' %(2, end))
       for coll in self.merge_rows:
           self.pdf_text.tag_add('r_merge', '%d.0' % coll[0], '%d.end' % coll[0])
           for i in range(1, len(coll)):
               r = coll[i]
               self.pdf_text.tag_add('r_merge', '%d.0' % r, '%d.1' % r)

   def _update_combo(self):
       self.cmb_col.delete(0, tk.END)
       col_list = []
       for c in self.columns:
           item = str(c[0]) + ' ' + str(c[1])
           col_list.append(item)
       self.cmb_col['values'] = col_list

   def _elab_minor(self):
       if self.csv_win == None or not self.csv_win.winfo_exists():
           return
       rows = self._get_rows_intervall()
       if not rows: return
       sep = self.e_sep.get()
       if len(sep) > 1: return
       if not sep: return
       for n in rows:
           text = self.pdf_text.get(str(n)+'.0', str(n)+'.end')
           words = text.split()
           text = sep.join(words)
           self.csv_win.add_text(text)
       rows.reverse()
       for n in rows:
           self.pdf_text.delete(str(n)+'.0', str(n)+'.end + 1 char')

   def _elab_major(self):
       if self.csv_win == None or not self.csv_win.winfo_exists():
           return
       sep = self.e_sep.get()
       if len(sep) > 1: return
       text_rows = self.pdf_text.get('1.0', tk.END).splitlines()
       if not text_rows: return
       index = 0
       while index < len(text_rows):
           # verifica se l'indice corrente ricade in una fusione di righe
           merge_index = None
           index += 1
           cell_text = []
           for i in range(len(self.merge_rows)):
               if index in self.merge_rows[i]:
                   merge_index = i
                   break
           if merge_index != None:
               cell_x = self.merge_rows[merge_index]
               index = self.merge_rows[merge_index][-1]
           else:
               cell_x = [index]
           for y in range(len(self.columns)):
               cell_text.append(self._get_cell_text(cell_x, y))
           for item in cell_text:
               if item == None:
                   cell_text.remove(item)
           csv_text = sep.join(cell_text)
           self.csv_win.add_text(csv_text)

   def _get_cell_text(self, cell_x, y):
       response = ''
       for r in cell_x:
           text = self.pdf_text.get(str(r)+'.0', str(r)+'.end')
           if self.columns[y][2]:
               text = text[self.columns[y][0]:self.columns[y][1]+1]
               text = text.lstrip()
               text = text.rstrip()
           else:
               return None
           response += ' ' + text
       response = response.lstrip()
       response = response.rstrip()
       if y in self.del_spaces:
           response = response.replace(' ', '')
       return response
   
   def set_separator(self, sep):
       old_state = self.e_sep['state']
       self.e_sep.configure(state='normal')
       self.e_sep.delete(0, tk.END)
       self.e_sep.insert(0, sep)
       self.e_sep.configure(state=old_state)
   
   def _write_text(self):
       f_types = [('file testo', '*.txt')]
       f_name = fdlg.asksaveasfilename(parent=self,
                                       title='Registrazione testo',
                                       confirmoverwrite=False,
                                       filetypes=f_types)
       if not f_name: return
       text = self.pdf_text.get('1.0', 'end-1c')
       try:
           with open(f_name, 'w') as f:
               f.write(text)
       except OSError:
           message = 'Errore scrittura file'
           msg = mydialog.Message(self).show_warning(message,
                                  title='Salvataggio fallito')
           mydialog.center_win(msg)

   def _write_csv(self):
       f_types = [('Comma separed values', '*.csv')]
       f_name = fdlg.asksaveasfilename(parent=self,
                                       title='Creazione CSV',
                                       confirmoverwrite=False,
                                       filetypes=f_types)
       if not f_name: return
       text = self.pdf_text.get('1.0', 'end-1c')
       if self.chk_var.get():
           sep = self.e_sep.get()
           text = text.replace(sep, '\U00000009')
       try:
           with open(f_name, 'w') as f:
               f.write(text)
       except OSError:
           message = 'Errore scrittura file'
           msg = mydialog.Message(self).show_warning(message,
                                  title='Salvataggio fallito')
           mydialog.center_win(msg)

   def close(self):
       self.destroy()


La parte tkinter finisce qui ... ora provo a vedere cosa mi riesce di fare con le wxPython! ... sarà in prossimo post ;)

Ciao

Edited by nuzzopippo - 9/7/2021, 12:30
 
Web  Top
3 replies since 14/6/2021, 10:20   293 views
  Share