Article technique

Filtres couleur PDF pour malvoyants en Delphi avec PDFium

Un lecteur malvoyant ne distingue pas du texte noir sur une page blanche au contraste par défaut : il demande donc un mode sombre. La réponse naïve consiste à inverser chaque pixel de la page rendue. Elle est livrée en une semaine et casse le lendemain : les photographies numérisées reviennent avec l'allure de négatifs, les marques du surligneur jaune du lecteur deviennent une bouillie bleue illisible, et quelqu'un demande pourquoi l'impression est sortie entièrement noire. La fonction vaut réellement la peine d'être construite et se rate réellement à moitié sans effort, et l'écart entre ces deux issues tient à une idée : chaque décision de couleur a sa place à un point précis de la chaîne de rendu, et l'inversion est le mauvais outil appliqué à la mauvaise étape. Le code présenté ici utilise PDFium Component, le visualiseur fondé sur PDFium pour Delphi, C++Builder et Lazarus, dont l'API de rendu expose ces étapes séparément

Les filtres sont un état de présentation, jamais un état du document

Une règle prévient ici la pire catégorie de bug : un mode de lecture change la façon dont le bitmap est produit ou post-traité, et rien d'autre. Les octets du PDF restent intacts, chaque mode est réversible par un nouveau rendu, et « enregistrer » ne réécrit jamais une apparence filtrée dans le fichier. Cela semble évident jusqu'à ce qu'un juriste imprime un contrat sous un filtre actif et classe la version inversée. À ce moment-là, la question « l'impression utilise-t-elle l'apparence propre au document ou celle de l'écran » mérite une réponse explicite dans votre spécification, pas un accident de chemin de code. Gardez le réglage du filtre dans l'état du visualiseur, appliquez-le au rendu, et faites déclarer à chaque chemin d'export quelle apparence il utilise

La règle se paie deux fois. La réversibilité vient gratuitement, car changer de mode refait le rendu depuis la source inchangée : il n'y a pas de pile d'annulation à tenir et aucun moyen pour une série de changements de mode de dégrader la page. Les scénarios multifenêtres restent cohérents pour la même raison. Deux vues d'un même document peuvent tourner dans des modes différents, puisque chaque vue possède son état de présentation tandis que l'objet document reste partagé

Rendre d'abord, transformer ensuite

Le schéma pris en charge est le traitement de bitmap après rendu : RenderPage produit la trame de la page, puis une passe de transformation l'ajuste. Le composant livre trois transformations comme opérations de bitmap sur place, InvertPdfBitmap, DuotonePdfBitmap et GrayscalePdfBitmap, ce qui fait du changement de mode une fonction propre à deux étages :

Schéma de la chaîne des modes de lecture d'un visualiseur PDF Delphi où un seul appel RenderPage de PDFium alimente quatre modes de lecture, chacun étant une transformation de bitmap sur place comme InvertPdfBitmap ou DuotonePdfBitmap
RenderPage produit la trame une seule fois et le mode de lecture actif choisit une transformation de bitmap sur place
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);  // fond sombre, texte ambre
    rmGrayscale:    GrayscalePdfBitmap(Result);
  end;
  // rmNormal passe au travers : le document garde ses propres couleurs
end;

Deux choses découlent de cette conception. D'abord, le coût de la transformation est proportionnel à la taille du bitmap : le travail a donc sa place là où vos résultats de rendu sont mis en cache, en filtrant le bitmap mis en cache une fois, pas à chaque dessin. Ensuite, comme la transformation s'exécute sur la trame finie, elle touche le texte, le dessin vectoriel, les images et les apparences d'annotations de la même façon. Cette uniformité est exactement ce que l'inversion simple rate sur les photographies. C'est la raison pour laquelle la transformation duotone fait un meilleur défaut pour les documents chargés de texte, puisqu'elle projette la luminance sur une rampe de couleurs choisie du sombre au clair au lieu de nier les teintes ; l'inversion reste disponible comme choix explicite pour les lecteurs qui la veulent. Des bords de glyphes plus nets sont un levier distinct. L'option de rendu reNoSmoothText coupe l'anticrénelage du texte au moment du rendu et s'accorde bien avec le mode contraste élevé à fort zoom

Deux niveaux de gris qui se contredisent

Les options de rendu comprennent reGrayscale, qui ressemble à un raccourci évitant l'étape de post-traitement. Ce n'est pas la même opération :

Schéma comparant l'option de rendu reGrayscale de PDFium, qui désature les images mais laisse des titres colorés, au post-traitement Delphi GrayscalePdfBitmap qui convertit la page entière
L'option du moteur désature le contenu image tandis que le post-traitement convertit chaque pixel du bitmap fini
// Niveau moteur : niveaux de gris appliqués pendant la rastérisation
GrayA := Pdf.RenderPage(0, 0, W, H, ro0, [reGrayscale]);

// Post-traitement : rendu en couleur, conversion du bitmap fini
GrayB := Pdf.RenderPage(0, 0, W, H);
GrayscalePdfBitmap(GrayB);

L'option au niveau du moteur s'applique à la sortie matricielle du contenu image mais n'atteint ni les aplats vectoriels ni les couleurs de texte : une page à titres colorés peut donc revenir avec des photographies grises et des titres obstinément bleus. GrayscalePdfBitmap sur le bitmap fini convertit tout, sans condition. L'option de rendu garde sa place quand vous voulez des images désaturées tout en gardant la couleur du texte comme signal, ce que certains lecteurs malvoyants préfèrent explicitement. Mais si l'exigence dit « page en niveaux de gris », c'est le post-traitement qui la satisfait. Quel que soit le chemin retenu, gardez en tête les deux styles de surcharge de RenderPage. La forme fonction renvoie un bitmap dont l'appelant est propriétaire et qu'il doit libérer, et cela compte dès que les filtres multiplient le nombre de bitmaps rendus en circulation

Fonds, marques de sélection et le piège de PageColor

Tous les réglages de confort ne sont pas des transformations. Remplacer le fond blanc de la page par une teinte chaude suffit souvent à lui seul pour les lecteurs sensibles à l'éblouissement, et cela dispose d'une propriété dédiée. Cette propriété porte une règle de portée qui piège du monde :

Schéma du piège de portée de PageColor dans un visualiseur PDF Delphi : la teinte apparaît à l'écran tandis que la sortie de RenderPage reste blanche si la couleur n'est pas passée explicitement
PageColor teinte uniquement la vue à l'écran et RenderPage garde une page blanche si la couleur n'est pas passée explicitement
// Affecte uniquement la vue à l'écran
PdfView.PageColor := $00D9EDF2;  // teinte papier chaude derrière le contenu de page

// La sortie de RenderPage ignore PageColor ; passez la couleur explicitement
Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [], $00D9EDF2);

PageColor change ce qu'affiche TPdfView, mais les bitmaps produits par RenderPage gardent le blanc par défaut tant que le paramètre Color ne dit pas autre chose. Le symptôme est fiable : l'écran montre la page teintée, l'utilisateur exporte ou imprime, et la sortie revient au blanc. Classez cela sous la même décision de politique d'export vue dans la première section

Les propriétés de couleur restantes définissent les marques de surcouche : HighlightColor pour les occurrences de recherche, SelectionColor pour la sélection de texte par l'utilisateur, ReadingWordColor pour le curseur du mot lu à voix haute. Chacune d'elles doit être revérifiée sous chaque filtre que vous proposez. Un curseur de lecture ambre qui fonctionne sur blanc s'évanouit après inversion ; une sélection bleu pâle disparaît dans un fond à contraste élevé. Tenez des palettes de surcouche par mode plutôt qu'un seul jeu global, et testez les combinaisons délibérément. Filtres plus synthèse vocale est une configuration normale pour les lecteurs que sert cette fonction, pas un cas limite. La mécanique des surcouches elle-même est couverte dans l'article sur le lecteur accessible

Chiffres, vérification et la question de l'impression

WCAG 2.1 fait de cette fonction quelque chose de mesurable. Le critère de succès 1.4.3 demande un rapport de contraste de 4,5:1 pour le texte courant, et le 1.4.6 le porte à 7:1 pour un contraste renforcé. Contrôlez par sondage votre mode contraste élevé face à ces rapports avec un analyseur de contraste lancé sur la sortie réellement rendue. Le texte sur images et le texte dans les champs de formulaire sont les endroits où les rapports échouent discrètement alors même que le texte courant passe

L'impression mérite sa propre décision, et le défaut défendable est l'apparence propre au document, avec « imprimer tel qu'affiché » proposé comme choix explicite de l'utilisateur. Une page imprimée fait office de preuve dans plus de flux de travail que les auteurs de visualiseurs ne l'imaginent, et l'impression inversée d'un contrat est un incident de support à saveur juridique. Un autre appariement compte pour la performance : le rendu filtré double le travail de bitmap à chaque changement de mode, alors n'appliquez pas de transformation à chaque message de dessin. Mettez en cache le bitmap filtré et ne relancez la transformation que lorsque la page, le zoom ou le mode changent réellement. La stratégie de cache qui rend cela bon marché vit dans l'article sur le cache de rendu et la performance du zoom

Une chose à trancher dans votre interface plutôt que dans votre code : quel mode doit être le défaut. Il n'y a pas de réponse unique, alors proposez l'ensemble et laissez le lecteur choisir. Le contraste élevé convient à la plupart des lectures chargées de texte, l'inversion va aux lecteurs qui veulent spécifiquement du clair sur sombre, les niveaux de gris coupent le bruit chromatique, et une teinte de fond traite la sensibilité à l'éblouissement. Persistez le choix par utilisateur, restaurez-le au démarrage, et gardez un retour au normal en une seule touche, puisqu'un lecteur qui atterrit dans un mode qu'il ne peut pas lire a besoin d'une sortie rapide

Les options de rendu, les transformations de bitmap et les propriétés de couleur de la vue utilisées ici sont livrées avec PDFium Component pour Delphi, C++Builder et Lazarus/FPC, avec le source complet afin que les implémentations de transformation puissent être auditées ou étendues