[Python] Estrazione font inclusi in un pdf: consigli

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

[Python] Estrazione font inclusi in un pdf: consigli

Messaggio da nuzzopippo »

I miei saluti.

Intrigato da questo post del buon @rai, da qualche giorno dedico i ritagli di tempo agli studi preliminari per riprodurre i Suoi risultati in una applicazione ad interfaccia grafica (tkinter), con posizionamento visuale di eventuali inserti di testo o immagini.

Tra le varie "utility" che mi è venuto in mente di porre in essere ho preso a studiarmi la possibilità di estrarre da un file pdf gli eventuali files di font inclusi nel pdf per provare a riutilizzarli nelle fasi di inserimento di testo aggiuntivo, per cercare di conservare i caratteri in uso. Tale estrazione è stato facile farla con le funzioni ad alto livello di pdfreader ma dato che già utilizzo pdfminer per estrarre diverse altre informazioni dalle pagine (marginatura generale del documento per tipo di pagina, rilegature, etc.) ho deciso di replicare tali operazioni con pdfminer, in maniera da ridurre i tempi di analisi del documento, che possono essere notevoli.
Pur se con qualche difficoltà (nessuna docs o discussione trovata per l'argomento specifico) mi è riuscito di risolvere la faccenda relativa alla estrazione e scrittura su disco di detti fonts inclusi in un pdf valutando i sorgenti di pdfminer e preparando il codice (dall'output molto verboso) di test ed estrazione che segue:

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
from pdfminer.pdftypes import resolve1

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

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


def extract_font_reference(page: PDFPage, font_dict: dict) -> None:
    if not isinstance(page, PDFPage):
        print('Passato oggetto', repr(type(page)), 'invece di PDFPage')
        return
    if not 'Resources' in page.attrs.keys(): return
    if not 'Font' in page.attrs['Resources']: return
    fonts = page.attrs['Resources']['Font']
    if not fonts: return
    for key in fonts:
        print(f'Pagina {page.pageid:>3}: rif. {key:<5} = {repr(fonts[key])}')
        if not repr(fonts[key]) in font_dict.keys():
            font_dict[repr(fonts[key])] = fonts[key]

def font_reference_exame(ref: dict) -> None:
    if not isinstance(ref, dict):
        print('Referenza non valida')
        return
    for key in ref.keys():
        o = ref[key]
        if isinstance(o, pdfminer.pdftypes.PDFObjRef):
            print(f'{key:<20} : oggetto {type(ref[key])}')
        else:
            print(f'{key:<20} : {resolve1(o)}')
        if key == 'FontDescriptor':
            font_descriptor_exame(resolve1(o), ref['Subtype'])
            
def font_descriptor_exame(des: dict, typ: object) -> None:
    if not isinstance(des, dict):
        print('Descrittore non valido')
        return
    space = ' ' * 21
    for key in des.keys():
        o = des[key]
        if key == 'CharSet':
            data = resolve1(o)
            tmp = repr(data)[1:].split('/')
            print(f'{space}- {key:<20}: {len(tmp)} codifiche')
        elif 'FontFile' in key:
            ob = resolve1(o)  # provo a memorizzarlo
            data = ob.get_data()
            f_name = str(resolve1(des['FontName']))[2:-1].split('+')[-1].split(',')[0]
            f_name += '.' + str(resolve1(typ))[2:-1]
            with open(f_name, 'wb') as f_d:
                f_d.write(data)
            print(f'{space}- {key:<20}: uno stream => {f_name}')
        else:
            if isinstance(o, pdfminer.pdftypes.PDFObjRef):
                print(f'{space}- {key:<20}: {repr(o)}')
            else:
                print(f'{space}- {key:<20}: {resolve1(o)}')
    

if __name__ == '__main__':
    arg = sys.argv
    if len(arg) < 2:
        text = 'Utilizzo : python pdfm_get_fonts.py file_name'
        print(text)
        exit()
    f_name = arg[1]
    document = createPDFDoc(f_name)
    device, interpreter = createDeviceInterpreter()
    pages = PDFPage.create_pages(document)
    fnt_dict = {}
    for page in pages:
        extract_font_reference(page, fnt_dict)
    print(f'Rilevate {len(fnt_dict)} definizioni di fonts')
    for f in fnt_dict.keys():
        print(repr(fnt_dict[f]))
    print('\nEsame delle risorse trovate')
    for k in sorted(fnt_dict.keys()):
        print()
        value = resolve1(fnt_dict[k])
        font_reference_exame(value)
il cui risultato, oltre all'abbondante output a video, sono dei files dei caratteri inclusi nel pdf esaminato, visualizzabili con l'applicazione "Caratteri" correntemente in uso in Ubuntu.
Naturalmente, i caratteri estratti a volte sono dei ridotti sub-set ed in alcuni casi, non ho idea perché, vengono visualizzati in maniera un po' "pasticciata"
Immagine
ma in genere sono abbastanza completi per un uso ordinario e chiari, tipo questo
Immagine
il cui relativo output è il seguente stralcio:

Codice: Seleziona tutto

BaseFont             : /'IBBBXZ+HelveticaNeue-MediumCond'
Encoding             : oggetto <class 'pdfminer.pdftypes.PDFObjRef'>
FirstChar            : 32
FontDescriptor       : oggetto <class 'pdfminer.pdftypes.PDFObjRef'>
                     - Ascent              : 951
                     - CapHeight           : 714
                     - CharSet             : 72 codifiche
                     - Descent             : -216
                     - Flags               : 32
                     - FontBBox            : [-164, -216, 1031, 951]
                     - FontFamily          : b'HelveticaNeue MediumCond'
                     - FontFile3           : uno stream => HelveticaNeue-MediumCond.Type1
                     - FontName            : /'IBBBXZ+HelveticaNeue-MediumCond'
                     - FontStretch         : /'Condensed'
                     - FontWeight          : 500
                     - ItalicAngle         : 0
                     - StemV               : 108
                     - Type                : /'FontDescriptor'
                     - XHeight             : 538
LastChar             : 121
Subtype              : /'Type1'
ToUnicode            : oggetto <class 'pdfminer.pdftypes.PDFObjRef'>
Type                 : /'Font'
Widths               : [240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 352, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 519, 537, 519, 556, 481, 463, 240, 556, 222, 240, 519, 444, 240, 574, 556, 500, 556, 240, 519, 463, 537, 240, 741, 240, 240, 240, 240, 240, 240, 240, 240, 240, 463, 481, 444, 481, 463, 278, 481, 481, 222, 240, 240, 222, 740, 481, 463, 481, 240, 315, 426, 278, 481, 426, 685, 426, 426]
Ora, qui mi son bloccato, non riuscendo a comprendere in primo luogo le unità di misura ricavate in che unità siano (punti, inch, altro?), poi anche il significato di alcune di esse (tipo widths, StemV, CapHeight) o di chiavi non sempre presenti, come il caso di "ToUnicode" o l'uso di dati tipo "CharSet", una stringa binaria abbastanza voluminosa che ho ridotto.

Ho cercato di ricorrere alle specifiche pdf 1.7 ma mi ci son perso (l'inglese mi mette ko) ... qualcuno ha conoscenze dell'argomento (fonts-pdf) sufficienti per darmi indicazioni su cosa consultare o, magari, consigliarmi su cosa utilizzare e cosa ignorare? [Edit] (Naturalmente per una presentazione allo user dei font e successivo uso per la definizione di nuovo testo)[/Edit]

Grazie dell'attenzione :)
Fatti non foste a viver come bruti ...
rai
Imperturbabile Insigne
Imperturbabile Insigne
Messaggi: 2842
Iscrizione: domenica 11 maggio 2008, 18:03
Desktop: plasma
Distribuzione: 22.04
Località: Palermo

Re: [Python] Estrazione font inclusi in un pdf: consigli

Messaggio da rai »

Hola nuzzopippo :),
ho visto ora questa discussione. Bello estrarre i font embedded nei pdf, appunto per poterli riusare in caso sia richiesto di aggiungere del testo al documento.
Non ho nessun consiglio dato che non ho ancora letto attentamente il tuo codice ma l'ho provato su un pdf creato con LibreOffice e contenente una o più righe con un font per riga. Sorprendentemente si è bloccato con un errore la cui causa non ho ancora approfondito.

Codice: Seleziona tutto

$ python3 nuzzopippo.py porta_font.pdf 
Traceback (most recent call last):
  File "nuzzopippo.py", line 98, in <module>
    extract_font_reference(page, fnt_dict)
  File "nuzzopippo.py", line 39, in extract_font_reference
    if not 'Font' in page.attrs['Resources']: return
TypeError: argument of type 'PDFObjRef' is not iterable
$
Intanto volevo segnalarti questo aspetto collaterale:
se faccio come scrivi nella riga 22

Codice: Seleziona tutto

>>> raise "Not extractable"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: exceptions must derive from BaseException
>>> 
Quindi credo che prima di sollevare e catturare la tua eccezione personalizzata dovresti crearne la classe più o meno così:

Codice: Seleziona tutto

class NoTextExtractable(Exception):
    
    def __init__(self, message="Text extraction not allowed."):
        self.message = message

[...]
try:
    [...]
    if foo:
        raise NoTextExtractable
    [...]
except NoTextExtractable as e:
    print(e.message)
    [...]
o forse più semplicemente si può importare e usare l'eccezione già esistente

Codice: Seleziona tutto

from pdfminer.pdfpage import PDFTextExtractionNotAllowed
Avatar utente
nuzzopippo
Entusiasta Emergente
Entusiasta Emergente
Messaggi: 1624
Iscrizione: giovedì 12 ottobre 2006, 11:34

Re: [Python] Estrazione font inclusi in un pdf: consigli

Messaggio da nuzzopippo »

Ciao @rai, grazie del Tuo intervento

Come certo Ti accorgerai una volta che avrai valutato quel codice, si tratta di un prototipo di studio dei dati ottenibili relativamente alla finalità "Estrarre i font", non è molto curata sotto molti aspetti.
L'errore che segnali :
rai ha scritto:
giovedì 19 gennaio 2023, 21:51
.... Sorprendentemente si è bloccato con un errore la cui causa non ho ancora approfondito...
È dovuto a differenze nella realizzazione dei PDF che sussistono nelle varie realizzazioni che si incontrano in giro, mi ci ero imbattuto a mia volta e mi sono accorto che in alcuni pdf il dato "Resource" è un dizionario, in altre è un "PDFObjRef" ... come detto in altro post, la codifica dei pdf è un po' un far-west, nel caso specifico della riga interessata, ho modificato il codice con in questo stralcio:

Codice: Seleziona tutto

    if isinstance(page.attrs['Resources'], pdfminer.pdftypes.PDFObjRef):
        res = resolve1(page.attrs['Resources'])
    elif isinstance(page.attrs['Resources'], dict):
        res = page.attrs['Resources']
    else:
        raise RuntimeError(f'{repr(page.attrs["Resources"])} : tipologia non prevista')
Analoghi accorgimenti sono stati necessari in diversi altri punti del codice, la casistica prima indicata si ripete anche nei sotto-insiemi di dati dipendenti, il codice completo di valutazione allo stato corrente è questo che segue:

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
from pdfminer.pdftypes import resolve1

FONT_NAMES = []

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

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


def extract_font_reference(page: PDFPage, font_dict: dict) -> None:
    if not isinstance(page, PDFPage):
        print('Passato oggetto', repr(type(page)), 'invece di PDFPage')
        return
    if not 'Resources' in page.attrs.keys(): return
    if isinstance(page.attrs['Resources'], pdfminer.pdftypes.PDFObjRef):
        res = resolve1(page.attrs['Resources'])
    elif isinstance(page.attrs['Resources'], dict):
        res = page.attrs['Resources']
    else:
        raise RuntimeError(f'{repr(page.attrs["Resources"])} : tipologia non prevista')
    if not 'Font' in res: return
    if isinstance(res['Font'], pdfminer.pdftypes.PDFObjRef):
        fonts = resolve1(res['Font'])
    elif isinstance(res['Font'], dict):
        fonts = resolve1(res['Font'])
    else:
        raise RuntimeError(f'{repr(res["Font"])}: tipologia non prevista')
    if not fonts: return
    for key in fonts:
        print(f'Pagina {page.pageid:>3}: rif. {key:<5} = {repr(fonts[key])}')
        if not repr(fonts[key]) in font_dict.keys():
            font_dict[repr(fonts[key])] = fonts[key]

def font_reference_exame(ref: dict) -> None:
    if not isinstance(ref, dict):
        print('Referenza non valida')
        return
    for key in ref.keys():
        o = ref[key]
        if isinstance(o, pdfminer.pdftypes.PDFObjRef):
            print(f'{key:<20} : oggetto {type(ref[key])}')
        else:
            print(f'{key:<20} : {resolve1(o)}')
        if key == 'FontDescriptor':
            font_descriptor_exame(resolve1(o), ref['Subtype'])
            
def font_descriptor_exame(des: dict, typ: object) -> None:
    if not isinstance(des, dict):
        print('Descrittore non valido')
        return
    global FONT_NAMES
    space = ' ' * 21
    for key in des.keys():
        o = des[key]
        if key == 'CharSet':
            data = resolve1(o)
            tmp = repr(data)[1:].split('/')
            print(f'{space}- {key:<20}: {len(tmp)} codifiche')
        elif 'FontFile' in key:
            ob = resolve1(o)  # provo a memorizzarlo
            data = ob.get_data()
            f_name = str(resolve1(des['FontName']))[2:-1].split('+')[-1].split(',')[0]
            f_name += '.' + str(resolve1(typ))[2:-1]
            with open(f_name, 'wb') as f_d:
                f_d.write(data)
            print(f'{space}- {key:<20}: uno stream => {f_name}')
            FONT_NAMES.append(f_name)
        else:
            if isinstance(o, pdfminer.pdftypes.PDFObjRef):
                print(f'{space}- {key:<20}: {repr(o)}')
            else:
                print(f'{space}- {key:<20}: {resolve1(o)}')
    

if __name__ == '__main__':
    arg = sys.argv
    if len(arg) < 2:
        text = 'Utilizzo : python pdfm_get_fonts.py file_name'
        print(text)
        exit()
    f_name = arg[1]
    document = createPDFDoc(f_name)
    device, interpreter = createDeviceInterpreter()
    pages = PDFPage.create_pages(document)
    fnt_dict = {}
    for page in pages:
        extract_font_reference(page, fnt_dict)
    print(f'Rilevate {len(fnt_dict)} definizioni di fonts')
    for f in fnt_dict.keys():
        print(repr(fnt_dict[f]))
    print('\nEsame delle risorse trovate')
    for k in sorted(fnt_dict.keys()):
        print()
        value = resolve1(fnt_dict[k])
        font_reference_exame(value)
    if FONT_NAMES:
        f_out = 'names_fonts'
        with open(f_out, 'w') as f:
            f.write('\n'.join(FONT_NAMES))
Riguardo alla Tua segnalazione riguardo al raise, giusta osservazione, non ci ho pensato a correggere gli esempi base della docs, in fondo questo è un semplice codice preliminare di valutazione, comunque è sufficiente invocare un "RuntimeError" già messo a disposizione da python apposta per eccezioni nel corso della esecuzione. ... anzi, correggo subito!

Il codice appena esposto lo ho testato con diversi pdf da libri interi e pdf della documentazione sino a pagine fatte da me, non ho trovato altre problematiche da tener presenti ma non escludo possano verificarsi ... al momento pare funzionare bene, i dati voluti vengono estratti e sono utilizzabili.
Riguardo al fonts estraibili, bisogna tener presente che spesso si è in presenza di sub-set dei caratteri del font originario, in alcuni casi limitato a pochissimi caratteri e la cui codifica, probabilmente, è contenuta il quel "- CharSet : 72 codifiche" nello stralcio di output che ho postato nel mio primo post.

Credo, anche, di aver capito "a soldoni" cosa siano le unità di misura che non mi erano chiare, dovrebbero essere delle unità di misura espresse su "em" sulla base della dimensione del font arbitrariamente definita nel suo instanziamento, tali unità sono astratte ed occorrono per il disegno vettoriale dei glifi, il font stesso definisce la quantità di unità che entrano in gioco ... ammesso che io abbia capito correttamente il significato di tali "misure", ritengo che debbano essere trasparenti per obiettivi ordinari di rappresentazione.

Se interessa il discorso, al momento mi sto dedicando all'aspetto parallelo della rappresentazione dei fonts in una interfaccia grafica (mia finalità), tkinter nello specifico, anche se credo sarebbe più semplice farlo con framework più evoluti, tipo le wxPython, non ho ancora cominciato a definire delle classi operative di lavorazione dati dei pdf perciò c'è ampio margine di confronto sull'argomento "estrazione fonts", ogni intervento e suggerimento è più che gradito
:)
Fatti non foste a viver come bruti ...
rai
Imperturbabile Insigne
Imperturbabile Insigne
Messaggi: 2842
Iscrizione: domenica 11 maggio 2008, 18:03
Desktop: plasma
Distribuzione: 22.04
Località: Palermo

Re: [Python] Estrazione font inclusi in un pdf: consigli

Messaggio da rai »

Provo a starti dietro e magari essere di aiuto nel task di estrazione font.
Riguardo l'interfaccia grafica, ormai di tkinter non uso più nemmeno i messagebox prefabbricati: troppo spesso l'ho trovato limitativo e comunque sto imparando a usare le Qt ed é un mondo che ti consiglio, a parte una curva di apprendimento inizialmente un po' ripida
Avatar utente
nuzzopippo
Entusiasta Emergente
Entusiasta Emergente
Messaggi: 1624
Iscrizione: giovedì 12 ottobre 2006, 11:34

Re: [Python] Estrazione font inclusi in un pdf: consigli

Messaggio da nuzzopippo »

rai ha scritto:
venerdì 20 gennaio 2023, 16:33
Riguardo l'interfaccia grafica, ormai di tkinter non uso più nemmeno i messagebox prefabbricati: troppo spesso l'ho trovato limitativo e comunque sto imparando a usare le Qt ed é un mondo che ti consiglio, a parte una curva di apprendimento inizialmente un po' ripida
Comprendo benissimo, in effetti il mio frequente utilizzo di tkinter, nonostante neanche io lo apprezzi molto, ha due motivi di fondo : il primo è dato dal fatto che non ho più motivo di "produrre" qualcosa, entrato in pensione non ho motivo di creare qualcosa di utile, quindi faccio giocattoli e tkinter ci può stare.
Il secondo, forse più motivante, è dato dalla constatazione che vedo la maggior parte delle new-entry approcciarsi direttamente a tkinter, magari senza nemmeno la conoscenza delle basi di python e senza alcuna idea di OOP, gestione degli eventi, etc. ... ogni tanto mi diletto a scrivere un qualche appunto per essi, anche se non sono un gran che e credo che nessuno leggerà mai ciò che scrivo ma magari capita che a qualcosa serva ... tutto qua.
In ogni caso, apprezzo sempre i consigli di chi reputo in gamba, darò 'na guardata alle QT6, magari, se mi riesce, posto qui una piccola prova circa l'estrazione e visualizzazione dei font tanto in tkinter quanto in QT :)
rai ha scritto:
venerdì 20 gennaio 2023, 16:33
Provo a starti dietro e magari essere di aiuto nel task di estrazione font.
Ti ringrazio della disponibilità, non mi aspetto grosse difficoltà sull'argomento salvo qualche approccio "particolare" in qualche pdf sparso, dopo tutto le specifiche 1.7 prevedono esplicitamente un dizionario per le risorse, non "PDFObjRef" ma tant'è ... in ogni caso, sono certo Tu abbia idee più chiare delle mie su molti argomenti e saprai certo suggerire approcci migliori.

La mia idea di base, scritta solo per esemplificarla e non ancora testata, è di utilizzare un thread, lanciato in background, per la reattività nella elaborazione di grossi documenti, che estragga i fonts e crei i file relativi e poi comunichi l'elenco dei file prodotti, ho scritto un esempio "volante" questa mattina, giusto per darne una idea dello schema, lo chiamo "pdfm_fonts.py"

Codice: Seleziona tutto

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

from typing import Any

import os
import glob
from threading import Thread

from pdfminer.high_level import extract_pages
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

from utility import Delivery


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

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


class FontsPredator(Thread):
    ''' Estrattore di font embedded da pdf '''
    def __init__(self, cache: str, f_name: str, pwd: str='', noimg: bool=False) -> None:
        super().__init__()
        self.document = None
        self.f_name = f_name
        self.pwd = pwd
        self.cache = cache
        self.fonts = []

    def run(self):
        self._clear_cache()
        self._recognize_document(self.f_name, self.pwd)
        msg = ['FONTS-EXTRACT', self.fonts]
        Delivery().send_message('FONT-USER', msg)

    def _clear_cache(self):
        files = glob.glob(os.path.join(self.cache, '*.*'))
        for f in files:
            try:
                os.remove(f)
            except OSError:
                pass

    def _recognize_document(self, f_name: str, pwd):
        ''' Esamina le pagine per trovarne tipo, marginatura e rilegatura '''
        self.document = createPDFDoc(f_name, pwd)
        device, interpreter = createDeviceInterpreter()
        pages = list(PDFPage.create_pages(self.document))
        # Crea oggetti "pagina"
        fnt_dict = {}
        for i in range(len(pages)):
            interpreter.process_page(pages[i])
            page = device.get_result()
            self.extract_font_reference(page, fnt_dict)
        for k in sorted(fnt_dict.keys()):
            value = resolve1(fnt_dict[k])
            font_reference_exame(value)
            
    def extract_font_reference(self, page: PDFPage, font_dict: dict) -> None:
        if not isinstance(page, PDFPage):
            return
        if not 'Resources' in page.attrs.keys(): return
        if isinstance(page.attrs['Resources'], pdfminer.pdftypes.PDFObjRef):
            res = resolve1(page.attrs['Resources'])
        elif isinstance(page.attrs['Resources'], dict):
            res = page.attrs['Resources']
        else:
            raise RuntimeError(f'{repr(page.attrs["Resources"])} : tipologia non prevista')
        if not 'Font' in res: return
        if isinstance(res['Font'], pdfminer.pdftypes.PDFObjRef):
            fonts = resolve1(res['Font'])
        elif isinstance(res['Font'], dict):
            fonts = resolve1(res['Font'])
        else:
            raise RuntimeError(f'{repr(res["Font"])}: tipologia non prevista')
        if not fonts: return
        for key in fonts:
            if not repr(fonts[key]) in font_dict.keys():
                font_dict[repr(fonts[key])] = fonts[key]

    def font_reference_exame(self, ref: dict) -> None:
        if not isinstance(ref, dict):
            return
        for key in ref.keys():
            o = ref[key]
            if key == 'FontDescriptor':
                self.font_descriptor_exame(resolve1(o), ref['Subtype'])
                
    def font_descriptor_exame(des: dict, typ: object) -> None:
        if not isinstance(des, dict):
            return
        for key in des.keys():
            o = des[key]
            if 'FontFile' in key:
                ob = resolve1(o)  # provo a memorizzarlo
                data = ob.get_data()
                f_name = str(resolve1(des['FontName']))[2:-1].split('+')[-1].split(',')[0]
                f_name += '.' + str(resolve1(typ))[2:-1]
                f_name = os.path.join(self.cache, f_name)
                with open(f_name, 'wb') as f_d:
                    f_d.write(data)
                self.fonts.append(f_name)
Per le comunicazioni, ho idea di adottare uno schema tipo "publisher/subscriber" implementando un singleton (il "Delivery" importato da "utils") per lo smistamento dei messaggi ... stralci di codice relativi :

Codice: Seleziona tutto

from functools import wraps

...

def singleton(o_cls):
    ''' Funzione-decoratore per definizione di classi singleton '''
    orig_new = o_cls.__new__
    inst_field = '__instance'  # attributo aggiuntivo

    @wraps(o_cls.__new__)
    def __new__(cls, *args, **kwargs):
        ''' Intercetta la nuova istanza di un oggetto '''
        # verifica se la classe possiede il nuovo attributo
        if (instance := getattr(cls, inst_field, None)) is None:
            # non lo possiede, è la prima instanza
            instance = orig_new(cls)  # recupera l'instanza dell'oggetto
            # verifica vi sia un processo di inizializzazione
            if hasattr(cls, '__init__'):
                cls.__init__(instance, *args, **kwargs)  # inizializza
                delattr(cls, '__init__')  # ed elimina la inizializzazione
            setattr(cls, inst_fiels, instance)  # ed imposta l'attributo
        return instance

    o_cls.__new__ = __new__  # sostituisce il processo di nuova instanza
    return o_cls  # restituisce la classe modificata

...

@singleton
class Delivery:
    ''' Un "postino" tra oggetti '''
    
    def __init__(self):
        self.subscr = {}
    
    def subscribe(self, group: str, method: any) -> None:
        ''' Aggiunge un subscriber ad un dato gruppo '''
        if not group in self.subscr.keys():
            self.subscr[group] = []
        if method in self.subscr[group]:
            raise RuntimeError('Metodo già acquisito nel gruppo %s' % group)
        self.subscr[group].append(method)

    def unsubscribe(self, group: str, method: any) -> None:
        ''' Rimuove un subscriber ad un dato gruppo, i gruppi vuoti vengono rimossi'''
        if not group in self.subscr.keys():
            raise RuntimeError('Gruppo %s non registrato' % group)
        if method not in self.subscr[group]:
            raise RuntimeError('Metodo non registrato nel gruppo %s' % group)
        self.subscr[group].remove(method)
        if not self.subscr[group]:
            del self.subscr[group]
            
    def send_message(self, group:str, message: list) -> None:
        ''' Distribuisce un messaggio ai subscriber di un gruppo '''
        if not group in self.subscr.keys():
            raise RuntimeError('Gruppo %s non registrato' % group)
        for s_func in self.subscr[group]:
            s_func(message)
... occhio, NON Ti sto chiedendo supporto, quanto su funzionerà ed eventuali sviste le troverò, Ti sto solo proponendo lo schema di ciò che avrei in mente, se hai tempo e Ti va leggilo con tranquillità a se avrai qualche altro schema d'approccio che vorrai suggerire ben venga e se ne parla volentieri.

Poi, magari, se mi riesce di concludere proporrò un test con GUI tkinter/QT e li magari si vede altro :) :ciao:
Fatti non foste a viver come bruti ...
Avatar utente
nuzzopippo
Entusiasta Emergente
Entusiasta Emergente
Messaggi: 1624
Iscrizione: giovedì 12 ottobre 2006, 11:34

Re: [Python] Estrazione font inclusi in un pdf: consigli

Messaggio da nuzzopippo »

Non edito il post precedente per lasciare inalterato quanto scritto.

Cominciando a testare i processi abbozzati mi sono accorto di una corbelleria commessa in FontsPredator._recognize_document(self, f_name: str, pwd): ove nel ciclo di scansione delle pagine ho inserito le istruzioni :

Codice: Seleziona tutto

            interpreter.process_page(pages[i])
            page = device.get_result()
che, naturalmente, danno il contenuto della pagina, privo delle risorse ... in sostanza : mai postare codice senza testarlo prima! :shy:

il codice funzionale della classe FontsPredator è il seguente:

Codice: Seleziona tutto

class FontsPredator(Thread):
    ''' Estrattore di font embedded da pdf '''
    def __init__(self, cache: str, f_name: str, pwd: str='') -> None:
        super().__init__()
        self.document = None
        self.f_name = f_name
        self.pwd = pwd
        self.cache = cache
        self.fonts = []

    def run(self):
        self._clear_cache()
        self._recognize_document(self.f_name, self.pwd)
        msg = ['FONTS-EXTRACT', self.fonts]
        Delivery().send_message('FONT-USER', msg)

    def _clear_cache(self):
        files = glob.glob(os.path.join(self.cache, '*.*'))
        for f in files:
            try:
                os.remove(f)
            except OSError:
                pass

    def _recognize_document(self, f_name: str, pwd):
        ''' Esamina le pagine per trovarne tipo, marginatura e rilegatura '''
        self.document = createPDFDoc(f_name, pwd)
        device, interpreter = createDeviceInterpreter()
        pages = list(PDFPage.create_pages(self.document))
        # Crea oggetti "pagina"
        fnt_dict = {}
        for i in range(len(pages)):
            self.extract_font_reference(pages[i], fnt_dict)
        for k in sorted(fnt_dict.keys()):
            value = resolve1(fnt_dict[k])
            self.font_reference_exame(value)
            
    def extract_font_reference(self, page: PDFPage, font_dict: dict) -> None:
        if not isinstance(page, PDFPage):
            return
        if not 'Resources' in page.attrs.keys(): return
        if isinstance(page.attrs['Resources'], pdfminer.pdftypes.PDFObjRef):
            res = resolve1(page.attrs['Resources'])
        elif isinstance(page.attrs['Resources'], dict):
            res = page.attrs['Resources']
        else:
            raise RuntimeError(f'{repr(page.attrs["Resources"])} : tipologia non prevista')
        if not 'Font' in res.keys(): return
        if isinstance(res['Font'], pdfminer.pdftypes.PDFObjRef):
            fonts = resolve1(res['Font'])
        elif isinstance(res['Font'], dict):
            fonts = res['Font']
        else:
            raise RuntimeError(f'{repr(res["Font"])}: tipologia non prevista')
        if not fonts: return
        for key in fonts:
            if not repr(fonts[key]) in font_dict.keys():
                font_dict[repr(fonts[key])] = fonts[key]

    def font_reference_exame(self, ref: dict) -> None:
        if not isinstance(ref, dict):
            return
        for key in ref.keys():
            o = ref[key]
            if key == 'FontDescriptor':
                self.font_descriptor_exame(resolve1(o), ref['Subtype'])
                
    def font_descriptor_exame(self, des: dict, typ: object) -> None:
        if not isinstance(des, dict):
            return
        for key in des.keys():
            o = des[key]
            if 'FontFile' in key:
                ob = resolve1(o)  # provo a memorizzarlo
                data = ob.get_data()
                f_name = str(resolve1(des['FontName']))[2:-1].split('+')[-1].split(',')[0]
                f_name += '.' + str(resolve1(typ))[2:-1]
                f_name = os.path.join(self.cache, f_name)
                with open(f_name, 'wb') as f_d:
                    f_d.write(data)
                self.fonts.append(f_name)
Per altro, i primi test sui fonts estratti dai PDF non promettono bene, i sub-set di caratteri ottenuti danno sorprese, tipo quello in figura ottenuto dal un libro in pdf che ho acquistato
Immagine
ove la mancanza di glifo per lo spazio è un problema, ben peggio avviene con documenti prodotti con libreoffice writer ove nessun carattere viene identificato per la produzione dell'immagine di testo ... è una problematica che richiederà un bel po' di studio mi sa.

[OT] Qualcuno ha provato QTCreator in linux/Ubuntu? È possibile utilizzarlo quale editor python? [/OT]
Fatti non foste a viver come bruti ...
rai
Imperturbabile Insigne
Imperturbabile Insigne
Messaggi: 2842
Iscrizione: domenica 11 maggio 2008, 18:03
Desktop: plasma
Distribuzione: 22.04
Località: Palermo

Re: [Python] Estrazione font inclusi in un pdf: consigli

Messaggio da rai »

[OT] Qualcuno ha provato QTCreator in linux/Ubuntu? È possibile utilizzarlo quale editor python? [/OT]
Certamente sì, è un IDE e per quello che ho visto ha estensioni per riconoscimento e auto-completamento della sintassi Python. Però prendi con le pinze quello che ti dico perché non uso QtCreator come editor di codice (uso Kate) né come supporto di progettazione grafica: per questa uso il più semplice QtDesigner.
Ma credo che ognuno debba provare e scegliere secondo le proprie preferenze|attitudini|abitudini.
Quello che ho provato con le Qt5 si installava con

Codice: Seleziona tutto

sudo apt install qtcreator pyqt5-dev-tools
Avatar utente
nuzzopippo
Entusiasta Emergente
Entusiasta Emergente
Messaggi: 1624
Iscrizione: giovedì 12 ottobre 2006, 11:34

Re: [Python] Estrazione font inclusi in un pdf: consigli

Messaggio da nuzzopippo »

Grazie della indicazione, probabilmente lo proverò, magari mi scarico la versione per QT6 non mi sembra il caso di usare idle con le qt.

Riguardo alla "estrazione dei font" mi ci sto accanendo con modesti risultati, alcuni dei fonts che vengono memorizzati nei pdf sembrano fatti in modo da non poter essere utilizzati : charset non utf e charmap's che non lo contemplano, oltre a docs poco istruttiva ed esempi prevalentemente in "C" :(

Mi sa che mi ci vorrà un bel po' per venirne a capo, sempre ammesso mi ci riesca- :)
Fatti non foste a viver come bruti ...
Avatar utente
nuzzopippo
Entusiasta Emergente
Entusiasta Emergente
Messaggi: 1624
Iscrizione: giovedì 12 ottobre 2006, 11:34

Re: [Python] Estrazione font inclusi in un pdf: consigli

Messaggio da nuzzopippo »

... Discorso rimasto sospeso, questo, l'ho rivisto stamattina ed ho pensato che, anche se improbabile, magari potesse interessare a qualcuno ciò che (con molta fatica) son riuscito a concludere sin ora sull'argomento.

In primo luogo, l'estrazione dei font memorizzati in un pdf è possibile farla, come già è evidente nei precedenti post, ma ho verificato che è soggetta a varie problematiche che ne limitano l'utilità :
1° la più significativa, vengono memorizzati solo i caratteri utilizzati in un documento, ciò implica che per documenti di dimensioni ridotte, ovvero per font usati per casi particolari (titoli, codice, etc.) possono esserci forti limitazioni nella disponibilità.
2° fastidiosa, i fonts embedded di numerosi documenti non contengono il carattere " " (spazio), ciò perché memorizzano nel pdf le singole "parole" per poter applicare spaziatura variabile in caso di giustificazione dei paragrafi.
3° seccante, In alcuni pdf la registrazione dei fonts embedded viene appositamente realizzata in modo tale da impedire la riproducibilità dei font, probabilmente per ragioni di rispetto del copyright dei fonts, non è "universale" ma, dolorosamente, il nostro libreoffice produce i pdf in tal maniera, vedi sotto
Immagine
Il font "Arial - grassetto" memorizzato in un pdf creato con libreoffice NON HA alcun riferimento a caratteri, essendovi memorizzato solo il null ("\x00"), HA incoding APPLE pur essendo realizzata in ambiente linux (quindi utf-8), possiede, però, 27 immagini (glifi) di carattere associati a valori (probabilmente casuali) per la rappresentazione nel pdf ... ciò succede anche per tutti gli altri fonts utilizzati nel documento.
4° provando su pdf trovati in giro ho visto anche altre caratteristiche possibili (tipo l'assoluta mancanza di encoding).

Le problematiche sopra mi hanno fatto pensare a vari tipi di possibili approcci, primo tra tutti sfruttare (se c'è) il dato sulla "famiglia" del font per l'uso in tkinter o per il rintracciamento dei files di font registrati nel sistema (linux ovviamente) e la selezione per famiglia degli stessi.
Quando quanto sopra non fosse possibile (spesso ho trovato fonts "esotici") ho pensato di prevedere la possibilità di utilizzo di fonts non registrati nel sistema (files di fonts scaricati) ovvero alla costruzione di immagini del testo voluto costruito direttamente dai glifi ... quest'ultima opzione ho provato a realizzarla tramite il modulo freetype, faticaccia boia farlo ma riuscita, anche se un po' rozza
Immagine
Per il "rozzo" si guardi la scritta in rosso nella casella giallo chiaro in basso a destra e la stessa scritta in nero su sfondo bianco in alto a sinistra, quest'ultima è creata con uso diretto dei fonts, mentre quella in rosso è una immagine creata direttamente dai glifi; il maggiore spessore della scritta è dovuto alla conversione dei "colori", freetype fornisce caratteri bianchi su sfondo nero, mentre lo stacco tra la "k" e la "a", maggiore nella immagine, è dovuto principalmente al fatto di aver usato bitmap dei singoli glifi di carattere (che sono vettoriali) quanto ad approssimazioni che diventano eccessive per piccole dimensioni.

Non credo interessi a molti il discorso, mirato alla definizione di "strumenti" per la modifica grafica di pdf indicata nel post iniziale, ma nel caso allego un file zip ("bozza_strumenti_x_font_tk.zip") contenente 4 files python, vanno salvati nella stessa directory, il file da lanciare è "test_render_sys_fonts.py" e richiede i moduli "pillow", "freetype" e "pdfminer.six", nel caso qualcuno volesse provarlo suggerisco di creare un venv.
Si tratta di un prototipo di studio non legato a niente, non curato sotto diversi aspetti, è solo una verifica delle possibilità sopra dette, permette solo di crearsi una immagine od una stringa derivanti dai caratteri presenti in un pdf direttamente, o per famiglia nei fonts di sistema o per fonts su file custom.

Ciao
Allegati
bozza_strumenti_x_font_tk.zip
(14.77 KiB) Scaricato 19 volte
Fatti non foste a viver come bruti ...
rai
Imperturbabile Insigne
Imperturbabile Insigne
Messaggi: 2842
Iscrizione: domenica 11 maggio 2008, 18:03
Desktop: plasma
Distribuzione: 22.04
Località: Palermo

Re: [Python] Estrazione font inclusi in un pdf: consigli

Messaggio da rai »

, l'estrazione dei font memorizzati in un pdf è possibile farla, come già è evidente nei precedenti post, ma ho verificato che è soggetta a varie problematiche che ne limitano l'utilità
. . .
Un po' demoralizzante :(
Mi sono scaricato il tuo zip, sicuramente imparo qualcosa sui pdf :ciao:
Avatar utente
nuzzopippo
Entusiasta Emergente
Entusiasta Emergente
Messaggi: 1624
Iscrizione: giovedì 12 ottobre 2006, 11:34

Re: [Python] Estrazione font inclusi in un pdf: consigli

Messaggio da nuzzopippo »

rai ha scritto:
giovedì 9 marzo 2023, 13:45
Mi sono scaricato il tuo zip, sicuramente imparo qualcosa sui pdf :ciao:
Sui pdf temo ben poco @rai, quelli che hai scaricato sono solo strumenti mirati ai fonts con l'idea di utilizzare font estratti da un pdf, anche creando, se il caso, immagini di testo invece che vero e proprio test oppure individuare i fonts utilizzati nel pdf con eventuali fonts registrati nel sistema ovvero reperiti su internet. Dei prototipi per un successivo sviluppo più ampio, in pratica-

Le conclusioni che riporto circa i PDF sono frutto di qualche decina di script, basati su pdfminer, che ho steso per verificare vari aspetti dei file pdf, approfondendo in successione, che ho applicato su numerosi documenti che ho raccolto nel tempo e miranti ad altri obiettivi ... le mie conclusioni, riguardo ai files pdf in giro è che sono un farwest, le scappatoie e personalizzazioni lasciate per il formato dalla Adobe nel tempo sono state ampiamente utilizzate per personalizzare e, in qualche modo privatizzare, i pdf prodotti, spesso in maniera irragionevole, la nota di interi libri in pdf senza carattere "spazio" utilizzato non è un estremo, ho constatato che un intero libro di centinaia di pagine non è presente neanche una LTTextBox ma per ogni singolo carattere vi è definita una LTTextLine a se stante, tale estremo è solo una delle tante assurdità che ho riscontrato esaminando a livello quanto più basso mi riusciva i file pdf al fine di estrarne e interpretare testo formattato per obiettivi miei (traduzione offline inglese => italiano di testi pdf di documentazione) che dopo numerose riscritture marca tutt'ora il passo.
Fatti non foste a viver come bruti ...
Scrivi risposta

Ritorna a “Programmazione”

Chi c’è in linea

Visualizzano questa sezione: 0 utenti iscritti e 7 ospiti