[Python] pdfminer => otd: fontname, una domanda

Linguaggi di programmazione: php, perl, python, C, bash e tutti gli altri.
Avatar utente
nuzzopippo
Entusiasta Emergente
Entusiasta Emergente
Messaggi: 1627
Iscrizione: giovedì 12 ottobre 2006, 11:34

[Python] pdfminer => otd: fontname, una domanda

Messaggio da nuzzopippo »

Signori, i miei saluti

Devo, necessariamente, premettere che in questo periodo, e per qualche mese, non dispongo della rete internet, probabilmente non avrei neanche posto la domanda, avendola.
Un problema che mi è dato da detta carenza è la non comprensione della documentazione in inglese reperita (nello specifico quella di "0MQ") che di solito uso affrontare con l'ausilio del traduttore di google e la redazione di maccheronici documenti con libreoffice writer secondo modelli a me comodi.
Marcando il passo per il problema "inglese" (da me mai studiato e che non comprendo :( ) mi son messo a studiare la fattibilità di realizzare un traduttore off-line a mio consumo che "dovrebbe" acquisire font e testo tramite pdfminer, tradurre parti del testo tramite argos-translate (le parti di codice ovviamente no) ed infine inserire il testo in un template libreoffice writer, che definirò, oppure con l'utilizzo della formattazione testo originale.

Al momento sto studiandomi i sorgenti di pdfminer e, per quanto riesco a capirla, la scarna documentazione trovata al fine di estrarre i singoli caratteri corredati dal font utilizzato e le loro coordinate nel documento per poi organizzarli per i successivi trattamenti, cosa che mi è riuscito di fare ... ma qui mi sorge un primo problema, probabilmente banale : in alcuni casi, il font-name ha un tipo di indicazione che non so bene come trattare in seguito.
Vi pongo, ad esempio, uno dei codici "di studio" realizzati che tramite il modulo "layout" di pdfminer estrae e stampa i dati interessati del testo:

Codice: Seleziona tutto

# -*- coding: utf-8 -*-

import sys
from io import StringIO

from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.layout import LAParams
from pdfminer.converter import PDFPageAggregator
import pdfminer

def createPDFDoc(f_name):
    fp = open(f_name, 'rb')
    parser = PDFParser(fp)
    document = PDFDocument(parser, password='')
    #  check if the document allow text extraction. If not abort
    if not document.is_extractable:
        raise "Not extractable"
    else:
        return document

def createDeviceInterpreter():
    rsrcmgr = PDFResourceManager()
    laparams = LAParams()
    device = PDFPageAggregator(rsrcmgr, laparams=laparams)
    interpreter = PDFPageInterpreter(rsrcmgr, device)
    return device, interpreter


# Bisogna procedere ad una migliore assegnazione dei fonts al testo
def parse_obj(objs):
    old_name = ""
    old_size = 0
    for obj in objs:
        if isinstance(obj, pdfminer.layout.LTTextBox):
            for o in obj._objs:
                if isinstance(o, pdfminer.layout.LTTextLine):
                    print()
                    for c in o._objs:
                        if isinstance(c, pdfminer.layout.LTChar):
                            if c.fontname != old_name or c.size != old_size:
                                old_name = c.fontname
                                old_size = c.size
                                print('\nfontname %s %d x = %d, y = %d' % (c.fontname, c.size, c.bbox[0], c.bbox[1]))
                            print(c.get_text(), end=' - ')
                            coords = [x for x in c.bbox]
                            for c in coords:
                                print(c, end=', ')
                            print()
        else:
            pass

if __name__ == '__main__':
    arg = sys.argv
    if len(arg) < 2:
        text = 'Utilizzo : python pdfm_get_fonts_var_03.py file_name [start_page] [end_page]'
        print(text)
        exit()
    f_name = arg[1]
    try:
        start_page = None if len(arg) < 3 else int(arg[2])
        end_page = None if len(arg) < 4 else int(arg[3])
    except ValueError:
        print('Valori di pagina non validi, esco')
        exit()
    if not start_page is None:
        if end_page is None:
            sel_pages = range(start_page-1, start_page)
        else:
            self_pages = range(start_page-1, end_page)
    document = createPDFDoc(f_name)
    device, interpreter = createDeviceInterpreter()
    pages = list(PDFPage.create_pages(document))
    for p in sel_pages:
        interpreter.process_page(pages[p])
        layout = device.get_result()
        print(repr(layout))
        parse_obj(layout._objs)
N.B - Non gestisce la condizione di pagina iniziale non indicata, da per scontato ci sia
Applicando tale codice su questo semplice pdf realizzato con libreoffice writer
Immagine
ho l'output come nello stralcio che segue

Codice: Seleziona tutto

Python 3.8.0 (default, Dec  9 2021, 17:53:27) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license()" for more information.
>>> 
==== RESTART: /media/nuzzo/STORE N GO/pdf/prove/pdfm_get_fonts_01_var_04.py ====
<LTPage(1) 0.000,0.000,595.276,841.861 rotate=0>


fontname BAAAAA+LiberationSerif 15 x = 56, y = 771
A - 56.8, 771.308, 65.464, 786.716, 
p - 65.488, 771.308, 71.488, 786.716, 
e - 71.488, 771.308, 76.804, 786.716, 
l - 76.792, 771.308, 80.116, 786.716,
...
Ora, non so bene cosa sia quel "BAAAAA" presente nel font-name, di solito non è presente in vari altri documenti visti ma ogni tanto c'è, suppongo che sia qualcosa inerente la codifica utf del set di caratteri utilizzati ma non ho idea se occorra affrontarla o meno od anche ignorarla proprio.
Qualcuno saprebbe darmi consigli in merito? Sarebbero graditi.

Grazie dell'attenzione.
Fatti non foste a viver come bruti ...
Avatar utente
vaeVictis
Imperturbabile Insigne
Imperturbabile Insigne
Messaggi: 4703
Iscrizione: venerdì 27 luglio 2012, 17:58
Desktop: Gnome
Distribuzione: Ubuntu 20.04 64bit

Re: [Python] pdfminer => otd: fontname, una domanda

Messaggio da vaeVictis »

Ciao caro @nuzzopippo, una domanda.
La stessa informazione ti viene data se controlli i metadati con altri programmi?
Pirates arrrrrrrrrrr awesome!!!
«I fear not the man who has practiced 10000 kicks once,
but I fear the man who has practiced one kick 10000 times.»
Avatar utente
nuzzopippo
Entusiasta Emergente
Entusiasta Emergente
Messaggi: 1627
Iscrizione: giovedì 12 ottobre 2006, 11:34

Re: [Python] pdfminer => otd: fontname, una domanda

Messaggio da nuzzopippo »

Grazie dell'intervento @vaeVictis

Al momento non posso fare prove estese, rispondo da cellulare e sul portatile (stravecchio) non dispongo di gran che.
Nelle informazioni disponibili col visualizzatore di documenti i font elencati sono "normali", non hanno, cioè quelle codifiche esadecimali che mi hanno fatto porre la questione.
Penso che l'informazione, estratta direttamente dai dati dei singoli caratteri, sia legata a questo ultimo e, forse, un "di più" aggiuntivo di writer (creatore del PDF), dato che i documenti vari altri in genere tale dato non è presente.

Comunque la Tua domanda mi ha dato idea per una ulteriore verifica tramite il catalogo del PDF, interpretando i vari oggetti "PDFObjRef" contenuti ... eccede, probabilmente, le mie necessità ma, forse, farà inquadrare meglio la faccenda.

Farò sapere appena posso, intanto grazie :)
Fatti non foste a viver come bruti ...
Avatar utente
vaeVictis
Imperturbabile Insigne
Imperturbabile Insigne
Messaggi: 4703
Iscrizione: venerdì 27 luglio 2012, 17:58
Desktop: Gnome
Distribuzione: Ubuntu 20.04 64bit

Re: [Python] pdfminer => otd: fontname, una domanda

Messaggio da vaeVictis »

Tienimi aggiornato sugli sviluppi. Purtroppo sono in terra straniera e il portatile si è rotto, quindi non posso scrivere codice.
Pirates arrrrrrrrrrr awesome!!!
«I fear not the man who has practiced 10000 kicks once,
but I fear the man who has practiced one kick 10000 times.»
Avatar utente
vaeVictis
Imperturbabile Insigne
Imperturbabile Insigne
Messaggi: 4703
Iscrizione: venerdì 27 luglio 2012, 17:58
Desktop: Gnome
Distribuzione: Ubuntu 20.04 64bit

Re: [Python] pdfminer => otd: fontname, una domanda

Messaggio da vaeVictis »

Tienimi aggiornato sugli sviluppi. Purtroppo sono in terra straniera e il portatile si è rotto, quindi non posso scrivere codice.
Pirates arrrrrrrrrrr awesome!!!
«I fear not the man who has practiced 10000 kicks once,
but I fear the man who has practiced one kick 10000 times.»
Avatar utente
crap0101
Rampante Reduce
Rampante Reduce
Messaggi: 8242
Iscrizione: martedì 30 ottobre 2007, 6:33
Desktop: LXDE
Distribuzione: Ubuntu 18.04.1 LTS
Sesso: Maschile
Località: TO
Contatti:

Re: [Python] pdfminer => otd: fontname, una domanda

Messaggio da crap0101 »

se non ti da problemi tipo che ti vengono fuori font diversi da quelli che ti aspetti, puoi pure ignorarli.
Preso direttamente dalle specifiche pdf 1.7:
9.6.4 Font Subsets
PDF documents may include subsets of Type 1 and TrueType fonts. The font and font descriptor that describe
a font subset are slightly different from those of ordinary fonts. These differences allow a conforming reader to
recognize font subsets and to merge documents containing different subsets of the same font. (For more
information on font descriptors, see 9.8, "Font Descriptors".)
For a font subset, the PostScript name of the font—the value of the font’s BaseFont entry and the font
descriptor’s FontName entry— shall begin with a tag followed by a plus sign (+). The tag shall consist of
exactly six uppercase letters; the choice of letters is arbitrary, but different subsets in the same PDF file shall
have different tags.
EXAMPLE EOODIA+Poetica is the name of a subset of Poetica®, a Type 1 font.
http://www.gnu.org/ http://boinc.berkeley.edu/ http://www.python-it.org/
- Ricorda le ultime parole di suo padre: «Sta' alla larga dalle chiese, figlio. La sola cosa per cui hanno la chiave è il merdaio. E giurami che non porterai mai un distintivo della legge» - W.S. Burroughs
Avatar utente
nuzzopippo
Entusiasta Emergente
Entusiasta Emergente
Messaggi: 1627
Iscrizione: giovedì 12 ottobre 2006, 11:34

Re: [Python] pdfminer => otd: fontname, una domanda

Messaggio da nuzzopippo »

Grazie della indicazione @crap0101, chiarisce la natura del "problema", forse una guardata a fonttools potrebbe essermi sufficiente, comunque al momento è meglio post-porre la faccenda all'autunno ed affrontarla dopo aver sgrossato la problematica.

@VaeVictis, se con "gli sviluppi" intendi la stretta problematica posta in quesito, personalmente considero l'indicazione di @crap chiarificatrice della faccenda, devo perfezionare l'interpretazione diretta del catalogo che, al momento, si rivela sorprendentemente (anzi TROPPO) ampio per l'esempio minimale in esame, devo rivedermi bene i sorgenti di pdfminer.pdftypes ed anche la logica applicata, una volta che mi riterrò soddisfatto o che non saprò "che pesci prendere" aggiornerò il post circa quel punto.

Qualora Tu intenda l'intero sviluppo della "idea" ... beh, sto agli studi preliminari ma non mi dispiacerebbe certo discuterne ogni tanto :)

[Edit] Proseguo "in calce" dato che l'ultimo post è mio.

Allora : per superare l'impasse relativo alla consultazione del catalogo sovra-dimensionata ho sviluppato, per prove consecutive uno script (ben poco efficiente) adattandolo per successione al tipo di oggetti trovati, nelle varie prove mi son reso conto che :
1 - esiste un complesso intreccio di riferimenti incrociati che può facilmente portare un esame "alla garibaldina" ad un loop infinito o comunque esagerato;
2 - o si sa esattamente cosa si vuol trovare o è meglio evitare di utilizzare il catalogo : in pratica, bisogna conoscere il protocollo "PDF" per sfruttarlo;
3 - i sub-set dei caratteri (quei codici che ho ritenuto esadecimali) esistono già a livello di catalogo, esempi di stralci di output ottenuti :

Codice: Seleziona tutto

==================================================
Esame oggetto <PDFObjRef:24> livello 3
Type                 (<class 'pdfminer.psparser.PSLiteral'>) /'Font'
Subtype              (<class 'pdfminer.psparser.PSLiteral'>) /'TrueType'
BaseFont             (<class 'pdfminer.psparser.PSLiteral'>) /'BAAAAA+LiberationSerif'
FirstChar            (<class 'int'>) 0
LastChar             (<class 'int'>) 28
Widths               (<class 'list'>) [365, 722, 500, 443, 277, 250, 333, 277, 500, 500, 500, 443, 277, 777, 500, 443, 443, 277, 500, 333, 500, 389, 500, 500, 333, 722, 250, 722, 250]
FontDescriptor       (<class 'pdfminer.pdftypes.PDFObjRef'>) <PDFObjRef:22>
ToUnicode            (<class 'pdfminer.pdftypes.PDFObjRef'>) <PDFObjRef:23>
==================================================
Esame oggetto <PDFObjRef:22> livello 4
Type                 (<class 'pdfminer.psparser.PSLiteral'>) /'FontDescriptor'
FontName             (<class 'pdfminer.psparser.PSLiteral'>) /'BAAAAA+LiberationSerif'
Flags                (<class 'int'>) 4
FontBBox             (<class 'list'>) [-176, -303, 1005, 981]
ItalicAngle          (<class 'int'>) 0
Ascent               (<class 'int'>) 891
Descent              (<class 'int'>) -216
CapHeight            (<class 'int'>) 981
StemV                (<class 'int'>) 80
FontFile2            (<class 'pdfminer.pdftypes.PDFObjRef'>) <PDFObjRef:20>
==================================================
Applicati sul semplice esempio riportato in figura nel primo post ... per altro, bisogna dire che l'output ottenuto nell'ultima prova è stato di circa 650 righe, eseguire questo script

Codice: Seleziona tutto

# -*- coding: utf-8 -*-

import sys
from io import StringIO

from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdftypes import resolve1, resolve_all, PDFObjRef

oggetti_esaminati = []

def parse_c_obj(obj, level=0):
    global oggetti_esaminati
    if level == 0:
        print('\n\nInizio esame Catalogo...')
    if isinstance(obj, dict):
        for key in obj:
            print(key + ' ' * (20-len(key)), "(" + repr(type(obj[key])) +")", repr(obj[key]))
        for key in obj:
            try:
                for value in [x for x in obj[key]]:
                    if isinstance(value, PDFObjRef):
                        if repr(value) in oggetti_esaminati:
                            continue
                        else:
                            oggetti_esaminati.append(repr(value))
                        print('='*50)
                        print('Esame oggetto', value, 'livello %d' % level)
                        try:
                            ob = resolve1(value)
                            parse_c_obj(ob, level+1)
                        except Exception as e:
                            print('Risoluzione fallita, eccezione :', repr(e))
            except TypeError:
                value = obj[key]
                if isinstance(value, PDFObjRef):
                    print('='*50)
                    print('Esame oggetto', value, 'livello %d' % level)
                    try:
                        ob = resolve1(value)
                        parse_c_obj(ob, level+1)
                    except Exception as e:
                        print('Risoluzione fallita, eccezione :', repr(e))
    elif isinstance(obj, list):
        for value in obj:
            if isinstance(value, PDFObjRef):
                if repr(value) in oggetti_esaminati:
                    continue
                else:
                    oggetti_esaminati.append(repr(value))
                print('='*50)
                print('Esame oggetto', value, 'livello %d' % level)
                try:
                    ob = resolve1(value)
                    parse_c_obj(ob, level+1)
                except Exception as e:
                    print('Risoluzione fallita, eccezione :', repr(e))
    else:
        if isinstance(obj, PDFObjRef):
            if not repr(obj) in oggetti_esaminati:
                oggetti_esaminati.append(repr(obj))
                print('='*50)
                print('Esame oggetto', obj, 'livello %d' % level)
                try:
                    ob = resolve1(obj)
                    parse_c_obj(ob, level+1)
                except Exception as e:
                    print('Risoluzione fallita, eccezione :', repr(e))
        else:
            print(type(obj), ' - ', repr(obj))
            if 'PDFStream' in repr(type(obj)):
                print('Attributi :')
                param = obj.attrs
                try:
                    parse_c_obj(param, level)
                except Exception as e:
                    print('Risoluzione fallita, eccezione :', repr(e))
                

arg = sys.argv
if len(arg) < 2:
    text = 'Utilizzo : python pdfm_doc_info_catalog.py file_name'
    print(text)
    exit()
f_name = arg[1]
fp = open(f_name, 'rb')
parser = PDFParser(fp)
doc = PDFDocument(parser)
parser.set_document(doc)

print('Informazioni generali documento:')
for i in doc.info:
    for key in i.keys():
        try:
            value = i[key].decode(encoding='utf-8', errors='ignore').replace('x00', '')
            if ''.join(value[:2]) == 'D:':
                value = (value[8:10] + '/' + value[6:8]+ '/' + value[2:6] + ' ' +
                         value[10:12] + ':' + value[12:14] + ':' + value[14:16])
        except:
            value = i[key]
        print(key + ' ' * (20-len(key)), ':', value)
print()

parse_c_obj(doc.catalog)
Su di un intero libro può facilmente portare ad un crash di python, comunque un eventuale output sarebbe enorme.
Da notare la necessità (appurata) di memorizzare riferimenti agli oggetti "PDFObjRef" eaminati per evitare di ripeterne l'esame, i riferimenti incrociati sono sicuramente molto numerosi, interrotto un test senza tale accorgimento ho visto che ero giunto a circa 1.800 livelli di annidamento!
Comunque, l'esperimento è stato interessante, intanto ha dato indicazione riguardo ai sub-set dei font, comunque presenti, poi indizio di numerosi particolari da andarsi a guardare per comprendere meglio alcuni aspetti.
In ogni caso, non credo che al momento mi serva procedere ancora su questa linea.

Ciò che tali prove mi hanno fatto concludere è che allo stato mi è necessario "volare basso", escludo di approfondire la sintassi dei PDF, sarebbe un orpello eccessivo per il mio scopo, applicare i metodi usati nel primo post una pagina alla volta (limando quando serve) è forse il metodo più immediato disponibile, per altro voler escludere parte dei dati ottenibili da alcuni trattamenti successivi mi implica dover prevedere interventi manuali sugli stessi, dato che non riesco neppure ad immaginare come distinguere nel testo esposizioni di codice o di output, da trattarsi separatamente dal resto del testo.

Ciao :)
Fatti non foste a viver come bruti ...
Scrivi risposta

Ritorna a “Programmazione”

Chi c’è in linea

Visualizzano questa sezione: 0 utenti iscritti e 19 ospiti