Articolo tecnico

Filtri colore PDF per ipovisione in Delphi con PDFium

La richiesta di funzionalità era semplice: "Le pagine bianche fanno male agli occhi: aggiungete un dark mode". La prima implementazione invertì ogni pixel della pagina renderizzata, uscì in una settimana e generò un secondo ticket dopo pochi giorni: le fotografie scansionate ora sembravano negativi, le evidenziazioni gialle del cliente erano diventate una macchia blu illeggibile, e un utente chiese perché la stampa uscisse nera. Il supporto visuale per ipovisione in un viewer PDF è davvero prezioso ed è davvero facile implementarlo a metà. La differenza tra i due risultati sta nel capire in quale punto della pipeline appartiene ogni decisione sul colore. L'implementazione sotto usa PDFium Component, il componente viewer basato su PDFium per Delphi, C++Builder e Lazarus, la cui API di rendering espone separatamente ciascuno di questi punti decisionali.

I filtri sono stato di presentazione, mai stato del documento

La regola architetturale che evita la peggior categoria di bug: una modalità di lettura cambia il modo in cui il bitmap viene prodotto o post-processato, e nient'altro. I byte del PDF restano intatti, ogni modalità è reversibile renderizzando di nuovo, e "salva" non persiste mai nel file un aspetto filtrato. Sembra ovvio finché un revisore legale stampa un contratto con un filtro attivo e archivia la versione invertita: a quel punto la domanda "la stampa usa l'aspetto proprio del documento o quello dello schermo" merita una risposta esplicita nella specifica, non un accidente del percorso di codice. Mantieni l'impostazione del filtro nello stato del viewer, applicala al render time, e fai dichiarare a ogni percorso di esportazione quale aspetto usa.

La regola si ripaga due volte. La reversibilità arriva gratis: cambiare modalità renderizza di nuovo dalla sorgente invariata, quindi non c'è uno stack di undo da mantenere e nessuna sequenza di cambi di modalità può degradare la pagina. E gli scenari multi-window restano coerenti: due viste dello stesso documento possono usare modalità diverse, perché ogni vista possiede il proprio stato di presentazione mentre l'oggetto documento resta condiviso.

Prima renderizza, poi trasforma

Il pattern supportato è il post-processing del bitmap renderizzato: RenderPage produce il raster della pagina, poi un passaggio di trasformazione lo regola. Il componente distribuisce tre trasformazioni, InvertPdfBitmap, DuotonePdfBitmap e GrayscalePdfBitmap, come operazioni in-place sui bitmap, quindi il cambio modalità diventa una funzione pulita in due fasi:

function TViewerForm.RenderWithMode(W, H: Integer): TBitmap;
begin
  Result := Pdf.RenderPage(0, 0, W, H, ro0, [reAnnotations]);
  case FReadingMode of
    rmInverted:     InvertPdfBitmap(Result);
    rmHighContrast: DuotonePdfBitmap(Result, clBlack, $0000C8FF);  // dark bg, amber text
    rmGrayscale:    GrayscalePdfBitmap(Result);
  end;
  // rmNormal falls through: the document keeps its own colors
end;

Vale la pena interiorizzare due conseguenze di questo disegno. I costi di trasformazione sono proporzionali alla dimensione del bitmap, quindi appartengono dove vengono memorizzati in cache i risultati del rendering: filtra il bitmap in cache una volta, non a ogni paint. E poiché la trasformazione gira sul raster finito, si applica uniformemente a testo, grafica vettoriale, immagini e apparenze delle annotazioni; l'uniformità è proprio ciò che la semplice inversione sbaglia sulle fotografie, per questo la trasformazione duotone, che mappa la luminanza su una rampa colore scuro-chiaro scelta invece di negare le tinte, è il default migliore per documenti ricchi di testo, con l'inversione offerta come scelta esplicita. I lettori che chiedono bordi dei glifi più netti hanno una leva separata: l'opzione di rendering reNoSmoothText disabilita l'anti-aliasing del testo al render time e si abbina bene alla modalità ad alto contrasto con zoom elevato.

Due scale di grigi che non sono d'accordo

Le opzioni di rendering includono reGrayscale, che sembra una scorciatoia per saltare il post-processing. Non è la stessa operazione:

// Engine-level: grayscale applied during rasterization
GrayA := Pdf.RenderPage(0, 0, W, H, ro0, [reGrayscale]);

// Post-process: render in color, convert the finished bitmap
GrayB := Pdf.RenderPage(0, 0, W, H);
GrayscalePdfBitmap(GrayB);

L'opzione a livello di motore si applica all'output raster del contenuto immagine ma non raggiunge riempimenti vettoriali o colori del testo, quindi una pagina con titoli colorati può tornare con fotografie grigie e titoli ostinatamente blu. GrayscalePdfBitmap sul bitmap finito converte tutto, senza condizioni. L'opzione di rendering resta utile quando vuoi specificamente desaturare le immagini preservando il colore del testo come segnale, cosa che alcuni utenti ipovedenti preferiscono, ma se il requisito dice "pagina in scala di grigi", il post-processing è la versione che lo soddisfa. Qualunque percorso tu scelga, ricorda che esistono entrambi gli stili di overload di RenderPage: la forma funzione restituisce un bitmap posseduto dal chiamante e da liberare, aspetto che conta appena i filtri moltiplicano il numero di bitmap renderizzati in circolazione.

Sfondi, segni di selezione e la trappola di PageColor

Non ogni regolazione di comfort è una trasformazione. Sostituire lo sfondo bianco della pagina con una tinta calda è spesso sufficiente per lettori sensibili all'abbagliamento, e ha una proprietà dedicata, con una regola di ambito che sorprende:

// Affects the on-screen view only
PdfView.PageColor := $00D9EDF2;  // warm paper tone behind page content

// RenderPage output ignores PageColor; pass the color explicitly
Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [], $00D9EDF2);

PageColor cambia ciò che TPdfView mostra, ma i bitmap prodotti tramite RenderPage restano bianchi per default a meno che il parametro Color dica diversamente. Sintomo pratico: lo schermo mostra la pagina tinta, l'utente esporta o stampa, e l'output torna bianco; va archiviato nella stessa decisione di policy sull'esportazione della prima sezione.

Le altre proprietà colore, HighlightColor per i risultati di ricerca, SelectionColor per la selezione testo dell'utente, ReadingWordColor per il cursore della parola pronunciata, definiscono segni overlay, e ognuna va ricontrollata sotto ogni filtro offerto. Un cursore di lettura ambra che funziona sul bianco scompare dopo l'inversione; una selezione azzurra pallida sparisce dentro uno sfondo ad alto contrasto. Mantieni palette overlay per modalità invece di un set globale, e testa deliberatamente le combinazioni: filtri più text-to-speech è una configurazione normale per gli utenti serviti da questa funzione, non un caso limite. La meccanica overlay è trattata nell'articolo sul reader accessibile.

Numeri, verifica e domanda sulla stampa

WCAG 2.1 dà a questa funzione target misurabili: il criterio di successo 1.4.3 richiede un rapporto di contrasto 4,5:1 per il testo del corpo, e 1.4.6 lo alza a 7:1 per il contrasto avanzato. Controlla a campione la modalità ad alto contrasto rispetto a quei rapporti con un analizzatore di contrasto sull'output renderizzato reale: testo sopra immagini e testo nei campi modulo sono i punti in cui i rapporti falliscono in silenzio anche quando il corpo passa.

Per la stampa, il default difendibile è l'aspetto proprio del documento, con "stampa come visualizzato" come scelta utente esplicita; una pagina stampata è prova in più workflow di quanto si aspettino gli autori di viewer, e una stampa invertita di un contratto è un incidente di supporto con sapore legale. Poiché il rendering filtrato raddoppia il lavoro sui bitmap durante i cambi di modalità, la strategia di caching dell'articolo su render cache e prestazioni dello zoom è la lettura complementare naturale.

FAQ

Il dark mode modifica il file PDF?

Non in questo disegno: le trasformazioni girano sui bitmap renderizzati e i byte del documento non cambiano mai. Fai la stessa promessa nel testo della UI, perché revisori e auditor devono sapere esplicitamente che il file sorgente non viene toccato dalle impostazioni di visualizzazione.

Perché l'immagine esportata è bianca quando lo schermo mostra una pagina tinta?

La tinta viene da PageColor, che influisce solo sulla visualizzazione di TPdfView. Le esportazioni passano da RenderPage, che usa il proprio parametro Color: passa lì la tinta, oppure accetta l'aspetto default del documento per le esportazioni e dichiaralo nella UI.

Quale modalità dovrebbe essere il default per utenti ipovedenti?

Offri scelte invece di eleggerne una: alto contrasto per la maggior parte della lettura ricca di testo, inversione per chi vuole specificamente chiaro su scuro, scala di grigi per ridurre il rumore cromatico, e una tinta di sfondo per la sensibilità all'abbagliamento. Persiste la scelta per utente, ripristinala all'avvio e mantieni un percorso a un solo tasto per tornare alla normalità.

I filtri influenzano le prestazioni di rendering?

Le trasformazioni sono passaggi lineari sul bitmap finito, quindi il costo scala con il numero di pixel invece che con la complessità del documento, e alle risoluzioni di schermo il passaggio è molto più economico del rendering stesso. L'ottimizzazione pratica è memorizzare in cache il bitmap filtrato e rieseguire la trasformazione solo quando cambiano pagina, zoom o modalità, non a ogni messaggio di paint.

Le opzioni di rendering, le trasformazioni bitmap e le proprietà colore della vista usate in questo articolo sono distribuite con PDFium Component per Delphi, C++Builder e Lazarus/FPC, con sorgente completo così le implementazioni delle trasformazioni possono essere auditate o estese.