La demande disait simplement : « Les pages blanches sont douloureuses à lire, ajoutez un mode sombre. » La première implémentation a inversé chaque pixel de la page rendue, a été livrée en une semaine, et a généré un second ticket quelques jours plus tard : les photographies scannées ressemblaient à des négatifs, les marques jaunes du client étaient devenues une tache bleue illisible, et un utilisateur demandait pourquoi l'impression sortait noire. Le support d'affichage basse vision dans un lecteur PDF est réellement utile et réellement facile à réussir à moitié. La différence tient à la compréhension de l'endroit du pipeline où chaque décision de couleur appartient. L'implémentation ci-dessous utilise PDFium Component, composant de visualisation basé sur PDFium pour Delphi, C++Builder et Lazarus, dont l'API de rendu expose séparément ces points de décision.
Les filtres sont un état de présentation, jamais un état du document
La règle d'architecture qui évite la pire catégorie de bugs : un mode de lecture change la manière dont le bitmap est produit ou post-traité, rien d'autre. Les octets PDF restent intacts, chaque mode est réversible par un nouveau rendu, et « enregistrer » ne persiste jamais une apparence filtrée dans le fichier. Cela semble évident jusqu'à ce qu'un juriste imprime un contrat avec un filtre actif et classe la version inversée ; la question « l'impression utilise-t-elle l'apparence du document ou celle de l'écran » mérite alors une réponse explicite dans la spécification, pas un accident de chemin de code. Gardez le réglage de 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 rapporte deux fois. La réversibilité est gratuite : changer de mode re-rend depuis la source inchangée, sans pile undo à maintenir ni dégradation possible après une suite de modes. Et les scénarios multi-fenêtres restent cohérents : deux vues du même document peuvent utiliser des modes différents, car chaque vue possède son état de présentation tandis que l'objet document reste partagé.
Rendre d'abord, transformer ensuite
Le modèle pris en charge est le post-traitement de bitmap : RenderPage produit le raster de page, puis une passe de transformation l'ajuste. Le composant livre trois transformations, InvertPdfBitmap, DuotonePdfBitmap et GrayscalePdfBitmap, comme opérations en place, ce qui fait du changement de mode une fonction en deux étapes nette :
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;
Deux conséquences valent d'être intégrées. Les coûts de transformation sont proportionnels à la taille du bitmap ; ils appartiennent donc à l'endroit où vous mettez en cache les rendus, filtrez le bitmap mis en cache une fois, pas à chaque paint. Et comme la transformation travaille sur le raster fini, elle s'applique uniformément au texte, aux vecteurs, aux images et aux apparences d'annotation ; cette uniformité est précisément ce que l'inversion simple fait mal aux photographies, d'où le meilleur défaut qu'est le duotone, qui mappe la luminance sur une rampe sombre-claire choisie au lieu de nier les teintes. Les lecteurs demandant des glyphes plus nets ont un levier séparé : l'option de rendu reNoSmoothText désactive l'antialiasing du texte au rendu et se combine bien avec un mode fort contraste à grand zoom.
Deux niveaux de gris qui ne sont pas d'accord
Les options de rendu incluent reGrayscale, qui ressemble à un raccourci évitant le post-traitement. Ce n'est pas la même opération :
// 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'option moteur s'applique à la sortie raster du contenu image mais n'atteint pas les remplissages vectoriels ni les couleurs de texte ; une page avec titres colorés peut donc revenir avec photos grises et titres toujours bleus. GrayscalePdfBitmap sur le bitmap fini convertit tout, sans condition. L'option de rendu reste utile si vous voulez spécifiquement désaturer les images tout en conservant la couleur du texte comme signal, préférence réelle de certains utilisateurs malvoyants ; mais si l'exigence dit « page en niveaux de gris », le post-traitement est la version qui la satisfait. Quel que soit le chemin, souvenez-vous que les deux styles de surcharge RenderPage existent : la forme fonction retourne un bitmap que l'appelant possède et doit libérer, point important dès que les filtres multiplient les bitmaps rendus en vol.
Arrière-plans, marques de sélection et piège PageColor
Tous les ajustements de confort ne sont pas des transformations. Remplacer le fond blanc par un ton chaud suffit souvent aux lecteurs sensibles à l'éblouissement, et une propriété dédiée existe, avec une règle de portée qui piège :
// 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 change ce que TPdfView affiche, mais les bitmaps produits par RenderPage conservent le blanc par défaut sauf si le paramètre Color indique autre chose. Symptôme pratique : l'écran montre une page teintée, l'utilisateur exporte ou imprime, et la sortie revient au blanc, exactement la décision de politique d'export évoquée au début.
Les autres propriétés de couleur, HighlightColor pour les hits de recherche, SelectionColor pour la sélection de texte et ReadingWordColor pour le curseur de mot parlé, définissent des overlays, et chacune doit être re-vérifiée sous chaque filtre proposé. Un curseur ambre qui marche sur blanc disparaît après inversion ; une sélection bleu pâle se perd sur un fond fort contraste. Maintenez des palettes d'overlay par mode plutôt qu'un seul jeu global, et testez délibérément les combinaisons : filtres plus synthèse vocale est une configuration normale pour les utilisateurs concernés, pas un cas limite. La mécanique d'overlay est couverte dans l'article sur le lecteur accessible.
Mesures, vérification et question de l'impression
WCAG 2.1 donne à cette fonction des cibles mesurables : le critère 1.4.3 demande un ratio de contraste de 4,5:1 pour le texte courant, et 1.4.6 le porte à 7:1 pour le contraste renforcé. Vérifiez ponctuellement votre mode fort contraste avec un analyseur sur une sortie réellement rendue ; le texte sur images et le texte dans les champs de formulaire sont les endroits où les ratios échouent discrètement même quand le corps passe.
Pour l'impression, le défaut défendable est l'apparence propre du document, avec « imprimer comme affiché » comme choix explicite. Une page imprimée sert de preuve dans plus de workflows que les auteurs de visualiseurs ne l'imaginent, et une impression inversée d'un contrat est un incident de support à coloration juridique. Comme le rendu filtré double le travail bitmap lors des changements de mode, la stratégie de cache de l'article sur le cache de rendu et les performances de zoom est le complément naturel.
FAQ
Le mode sombre modifie-t-il le fichier PDF ?
Pas dans ce design : les transformations s'exécutent sur les bitmaps rendus et les octets du document ne changent jamais. Faites la même promesse dans l'UI, car réviseurs et auditeurs ont précisément besoin de savoir que le fichier source reste intact.
Pourquoi mon image exportée est-elle blanche quand l'écran montre une page teintée ?
La teinte vient de PageColor, qui n'affecte que l'affichage TPdfView. Les exports passent par RenderPage, qui possède son propre paramètre Color : passez-y la teinte, ou acceptez l'apparence par défaut du document pour les exports et dites-le dans l'UI.
Quel mode faut-il choisir par défaut pour les utilisateurs malvoyants ?
Proposez des choix plutôt qu'un vainqueur unique : fort contraste pour la plupart des lectures textuelles, inversion pour ceux qui veulent explicitement clair sur sombre, niveaux de gris pour réduire le bruit couleur, et teinte de fond pour la sensibilité à l'éblouissement. Persistez le choix par utilisateur, restaurez-le au démarrage, et gardez un raccourci vers le mode normal.
Les filtres affectent-ils les performances de rendu ?
Les transformations sont des passes linéaires sur le bitmap fini ; leur coût suit le nombre de pixels plutôt que la complexité du document, et aux résolutions écran la passe est bien moins chère que le rendu lui-même. L'optimisation pratique est de mettre en cache le bitmap filtré et de relancer la transformation seulement quand page, zoom ou mode changent, pas à chaque message paint.
Les options de rendu, transformations bitmap et propriétés de couleur de vue utilisées ici sont livrées avec PDFium Component pour Delphi, C++Builder et Lazarus/FPC, avec code source complet afin que les transformations puissent être auditées ou étendues.