DEV Community

Cover image for Come trasformare la foto di una firma in un timbro PDF trasparente con Python

Come trasformare la foto di una firma in un timbro PDF trasparente con Python

La foto di una firma ha due problemi prima di poter finire in un PDF. La carta non è bianca e non è illuminata in modo uniforme, quindi incollare il JPEG così com'è lascia un rettangolo grigio sulla riga della firma. E qualunque cosa si incolli, il PDF la memorizza in due modi molto diversi: come annotazione che qualsiasi lettore può trascinare via o cancellare, oppure come parte della pagina. La scheda Scan e il comando Flatten comments dell'editor PDF dell'app desktop di osFoundry fanno entrambi i lavori. Qui sotto ogni passaggio è ricostruito con Python, numpy, Pillow e PyMuPDF, con numeri misurati su questo Mac. Il codice è una mia versione dell'idea, non il codice sorgente dell'app.

La foto di prova è generata, non è una vera foto da telefono: un JPEG da 1280 × 940 con una firma in corsivo su carta, con una caduta di luminosità da un angolo e grana della carta. Così il problema è riproducibile, ma è un esempio, non una misura di telefoni reali.

La foto di esempio della firma usata per tutti i numeri

Perché una sola soglia non basta

Il modo ovvio per togliere la carta è luminanza < T. Nella foto di esempio la luminanza della carta va da circa 202 (l'1 % più scuro) a 235, e l'inchiostro è molto più scuro, quindi un T qualsiasi nel mezzo sembra andare bene:

Livello di luce Carta, 1 % più scuro T=160, pixel di carta scambiati per inchiostro T=190, pixel di carta scambiati per inchiostro Stima locale della carta
1.0 (originale) 201.9 0 0 0
0.9 181.7 0 91,293 0
0.8 161.6 2,663 917,309 0
0.7 141.4 669,328 917,666 0

Il livello di luce è la foto moltiplicata per 0.9, 0.8 e 0.7, come se fosse stata scattata in una stanza più buia. «Pixel di carta scambiati per inchiostro» conta i pixel fuori da un riquadro più largo di 40 px per lato rispetto alla firma, su 917,666 pixel in quell'area. T=190 è pulito a 1.0 e a 0.9 ha segnalato 91,293 pixel di carta bianca. Qualsiasi T fisso funziona con la luce per cui è stato regolato e fallisce con la foto successiva.

Stimare la carta e misurare lo scuro rispetto a essa

Invece di confrontare con una costante, si stima la luminosità della carta in ogni pixel e si misura quanto il pixel è più scuro. Un filtro massimo su una copia piccola dell'immagine toglie l'inchiostro (l'inchiostro è scuro, quindi il massimo lo ignora) e una sfocatura leviga il risultato:

def paper_est(L, small=160):
    h, w = L.shape
    im = Image.fromarray(L.astype(np.uint8)).resize((small, int(small * h / w)))
    im = im.filter(ImageFilter.MaxFilter(15)).filter(ImageFilter.GaussianBlur(6))
    return np.asarray(im.resize((w, h), Image.BILINEAR), np.float32)

def extract(img, sens=50):
    a = np.asarray(img.convert("RGB"), np.float32)
    L = a @ np.array([.299, .587, .114])
    P = paper_est(L)
    d = np.clip((P - L) / np.maximum(P, 1), 0, 1)   # how much darker than the local paper
    t0 = 0.03 + (100 - sens) / 100 * 0.12           # dead zone for grain
    alpha = np.clip((d - t0) / 0.35, 0, 1)
    return alpha, a
Enter fullscreen mode Exit fullscreen mode

Il pixel di uscita mantiene il colore della foto e usa alpha come trasparenza, quindi una penna blu resta blu e un sigillo rosso resta rosso. Nell'ultima colonna della tabella qui sopra la stessa funzione ha segnalato 0 pixel di carta a ogni livello di luce, e la firma stessa risulta di 26,526 pixel con alpha superiore a 0.05 a 1.0 e tra 26,511 e 26,544 agli altri tre livelli.

Il parametro sens fa la parte del cursore Ink sensitivity dell'app (che parte da 50), ma come si traduce in una soglia qui è una mia scelta. Sulla stessa foto:

sens Pixel con alpha > 0 Di cui fuori dal riquadro della firma
0 25,004 0
25 26,005 0
50 27,325 0
75 30,478 13
100 149,111 15,519

A 100 la zona morta è così piccola che la grana della carta comincia a contare come inchiostro, lo stesso problema di macchioline che il cursore dell'app serve a correggere. L'intera estrazione ha richiesto da 61 a 65 ms per una foto da 1280 × 940 su questo Mac, quindi si può rifare a ogni spostamento del cursore.

Annotazione o contenuto della pagina

Scrivere il risultato in un PNG con canale alfa e metterlo su un PDF dà due esiti possibili, a seconda della chiamata PyMuPDF usata. Con l'accordo di esempio e un riquadro di firma di 150 × 28.7 pt:

rect = pymupdf.Rect(196.6, 575.5, 346.6, 604.2)
pg.add_stamp_annot(rect, stamp="ink-en.png")       # an annotation
doc.bake(annots=True, widgets=False)               # draw annotations into the page
Enter fullscreen mode Exit fullscreen mode

Dopo add_stamp_annot la pagina ha 1 annotazione (tipo Stamp) e nessuna immagine. Cancellare quell'annotazione con page.delete_annot porta via la firma: il numero di pixel scuri nel rettangolo della firma passa da 1,453 a 0. Dopo doc.bake la pagina ha 0 annotazioni e 1 immagine, il bake ha richiesto 2.0 ms, e cancellare le annotazioni dopo non toglie nulla (1,440 pixel scuri prima e dopo). Il testo della pagina resta selezionabile ed estraibile, perché bake aggiunge solo l'immagine.

Il file salvato dall'app dopo Flatten comments, letto con PyMuPDF, è identico: 0 annotazioni, 1 immagine con maschera alfa, di 1032 × 198 px, disegnata in un rettangolo di 150 × 28.7 pt, circa 495 dpi sulla pagina. L'originale di 87,708 byte è passato a 101,785 byte. Il mio file con timbro è venuto molto più grande (922,521 byte, 922,681 dopo il bake), perché ho scritto un PNG a dimensione piena e ho lasciato che PyMuPDF lo salvasse com'è: in uno script conviene ridurlo prima alla dimensione di stampa.

Dove finisce

Il risultato è un'immagine sulla pagina. Non dimostra chi l'ha messa e nulla nel file rileva una modifica successiva. Per questo serve una vera firma digitale (una firma PKCS#7 sul file), che è un meccanismo diverso. Per i moduli che chiedono solo una firma sulla riga, un PNG alfa incorporato nella pagina equivale a una firma stampata e scansionata, senza la carta.

Tutto quanto sopra si esegue con uvx --with pymupdf --with numpy --with pillow python -I script.py. L'app è gratuita dalla pagina di download di osFoundry, e l'editor lavora su file presenti sul computer, in linea con il suo approccio local-first.

Top comments (0)