Pointez NVDA sur un visualiseur PDF Delphi fraîchement construit et vous obtenez généralement l'un de deux résultats : le silence, ou un texte lu dans l'ordre où le flux de contenu l'a stocké, pied de page d'abord, puis colonne de droite, puis le titre qui ouvre visuellement la page. Le rendu est impeccable ; l'expérience d'écoute est inutilisable. L'écart existe parce que la rastérisation et la lecture sont deux pipelines distincts : l'ordre de peinture dans un flux de contenu PDF n'a aucune obligation de correspondre à l'ordre qu'un humain doit entendre. PDFium Component, wrapper VCL/LCL du moteur PDFium pour Delphi, C++Builder et Lazarus, fournit une famille dédiée d'API de lecture précisément parce que les API de rendu ne peuvent pas faire ce travail.
Trois problèmes décident du succès d'un projet de lecteur accessible : extraire un ordre de lecture prononçable, garder un curseur de mot visible synchronisé avec la synthèse vocale, et dégrader honnêtement quand le document n'a jamais été balisé. Chacun possède un chemin API concret et un mode d'échec tout aussi concret qu'il vaut mieux connaître avant d'écrire le premier gestionnaire d'événement.
L'ordre de lecture vit dans l'arbre de structure, pas dans l'ordre de peinture
ISO 32000-1 §14.8 définit la structure logique comme un arbre d'éléments de structure posé sur le contenu des pages, et PDF/UA (ISO 14289-1) rend cet arbre obligatoire : chaque contenu réel doit être accessible par lui dans l'ordre de lecture, les artefacts étant exclus. Un rapport correctement balisé sait que « Quarterly Results » est un titre de niveau deux et que la grille de totaux est un tableau avec cellules d'en-tête. Un rapport non balisé n'est qu'une série de glyphes positionnés.
ReadablePageContent parcourt cette structure lorsqu'elle existe et retourne des fragments de contenu étiquetés par un Kind sémantique, cfHeading, cfParagraph et valeurs liées, afin que l'interface annonce « titre » avant le texte au lieu de lire une ligne en gras comme du corps. Lorsque l'arbre de structure est absent ou inutilisable, le même appel bascule vers une analyse heuristique de mise en page : détection de colonnes, regroupement de lignes de base, ordre gauche-droite. La sortie est souvent exploitable pour des documents à une colonne et peu fiable pour newsletters, formulaires multi-colonnes et contenus avec encadrés. La discipline cruciale consiste à dire à l'utilisateur dans quel cas il se trouve ; l'API vous donne ce fait directement : l'enregistrement TPdfReadableContent retourné porte un champ Source valant rosStructure quand l'ordre vient de l'arbre balisé et rosHeuristic quand il a été deviné depuis la mise en page. Présenter un ordre deviné comme un ordre vérifié est l'équivalent accessibilité d'une coche verte sur un build non testé.
Une manière pratique de classer un fichier à l'ouverture consiste à vérifier IsTagged et à lancer ValidatePdfUa une fois, en mettant le verdict en cache. Un échec PDF/UA ne signifie pas rejeter le document : il signifie que la barre d'état affiche « ordre de lecture estimé », et que votre support sait exactement ce qu'il regarde lorsqu'un client signale une narration absurde sur un fichier précis.
De la page à la file vocale avec ReadingUnits
Pour la synthèse vocale, l'outil principal est ReadingUnits : il retourne un tableau d'enregistrements TPdfReadingUnit pour la page active, chacun portant le texte à prononcer, son rôle sémantique et les rectangles de surbrillance qui le localisent sur la page. Une variante document, DocumentReadingUnits, existe pour la lecture continue. Une unité se mappe naturellement sur une entrée de file vocale :
procedure TReaderForm.QueuePageSpeech(PageNumber: Integer);
var
Units: TPdfReadingUnits;
i: Integer;
begin
Pdf.PageNumber := PageNumber; // ReadingUnits works on the active page
Units := Pdf.ReadingUnits;
FSpeechQueue.Clear;
for i := Low(Units) to High(Units) do
FSpeechQueue.Add(Units[i]); // text + semantics + highlight rects
FCurrentPage := PageNumber;
SpeakNextUnit;
end;
Deux détails de cette boucle méritent de l'attention. D'abord, gardez la file strictement par page et reconstruisez-la à la navigation : les unités de lecture portent des rectangles en espace page ; une file obsolète peint les surbrillances sur la mauvaise page après un saut. Ensuite, un tableau Units vide sur une page visiblement remplie est votre détecteur de page image seule. Une page scannée contient des pixels mais pas de couche texte, et la bonne réponse est un avertissement parlé, « cette page ne contient aucun texte extractible », plutôt qu'un silence impossible à distinguer d'un plantage.
Un curseur de mot qui suit la voix
La surbrillance par bloc paraît lente aux utilisateurs malvoyants qui suivent visuellement pendant l'écoute. La surbrillance mot à mot, façon karaoké, a besoin de deux ingrédients : la géométrie des mots et un mapping des callbacks de progression du moteur TTS vers cette géométrie. PageWordBoxes fournit la géométrie sous forme d'enregistrements TPdfWordBox, texte du mot, offset caractère, nombre de caractères et rectangle en espace page. TrackReadingWordAt fournit le mapping : il convertit une position caractère, exactement ce que la notification de frontière de mot de SAPI donne, en index dans le tableau de boîtes de mots, et met en surbrillance le mot correspondant dans le même appel.
procedure TReaderForm.PrepareKaraoke(PageNumber: Integer);
begin
// The view's word boxes come from the page the view displays —
// setting Pdf.PageNumber alone would not move the view
PdfView.PageNumber := PageNumber;
FWordBoxes := PdfView.PageWordBoxes;
end;
procedure TReaderForm.OnTtsWordBoundary(Sender: TObject; CharIndex: Integer);
var
WordIdx: Integer;
begin
// TrackReadingWordAt maps the offset AND paints the word cursor
WordIdx := PdfView.TrackReadingWordAt(FCurrentPage, CharIndex);
if WordIdx < 0 then
PdfView.ClearReadingWord; // boundary ran past the page text
end;
Le contrat est tolérant d'un côté et strict de l'autre. Tolérant : TrackReadingWordAt maintient son propre cache de boîtes de mots pour la page suivie ; vous n'avez donc pas à le préalimenter, et aucun rendu n'est impliqué, car les boîtes de mots viennent de la couche texte de la page. Strict : l'index caractère doit se référer au texte extrait par le composant. La fonction retourne aussi -1 au lieu de lever lorsque CharIndex pointe au-delà de la fin du texte de page, ce qui arrive souvent quand un moteur TTS émet une dernière frontière pour la ponctuation finale. Traitez -1 comme « effacer le curseur », pas comme une erreur.
Côté affichage, ReadingWordColor contrôle la surbrillance du curseur ; l'ambre par défaut survit à la plupart des arrière-plans, mais vérifiez-le sous tous les filtres d'affichage de votre visualiseur, car un curseur ambre peut disparaître entièrement sous inversion de couleurs, combinaison justement courante chez les utilisateurs malvoyants avec synthèse vocale. Mettre ReadingWordFollow à True fait défiler automatiquement la vue pour rendre le mot parlé visible, essentiel sur des pages zoomées multi-écrans. Règle de portée : SetReadingWord peint seulement sur la page active du TPdfView actif ; décidez donc si le défilement manuel met la parole en pause ou si le suivi gagne, car ne choisir ni l'un ni l'autre laisse la parole continuer avec un curseur invisible.
Documents qui résistent
Trois classes d'entrées cassent assez souvent les implémentations naïves pour mériter des échantillons permanents dans la suite de régression.
- Fichiers non balisés mais riches en texte. L'ordre heuristique est souvent bon pour des rapports linéaires et faux pour les mises en page avec encadrés ou citations. Étiquetez l'ordre comme estimé dans l'UI et dans le journal de diagnostics.
- Scans image seule. Pas de couche texte. Détectez-les par unités de lecture vides et orientez l'utilisateur vers une étape OCR amont au lieu de laisser le lecteur ne rien dire.
- Caractères combinants et écritures mixtes. Les marques combinantes Unicode ne se mappent pas toujours un pour un sur les mots visuels ; le nombre de boîtes de mots peut donc différer de ce que votre propre tokenizer prédirait. N'indexez jamais le tableau de boîtes de mots avec de l'arithmétique issue de votre découpage ; utilisez seulement les indices retournés par
TrackReadingWordAt.
Acceptation : tester comme un auditeur, pas comme une démo
« Il a lu mon échantillon à voix haute » n'est pas une acceptation. Un passage défendable lance le build fini avec NVDA sur trois documents : un fichier correctement balisé, où les titres sont annoncés comme titres et le tableau lu par lignes ; un fichier connu non balisé, où l'indicateur d'ordre estimé est visible ; et un scan, avec avertissement explicite d'absence de texte prononcé.
Vérifiez ensuite que le curseur de mot reste attaché à double et demi-vitesse de parole, et que le défilement ReadingWordFollow ne lutte pas contre le défilement manuel. Enfin, activez chaque filtre de couleur pendant la parole et confirmez que le curseur reste visible ; l'article sur les filtres de couleur basse vision couvre ce chemin de rendu, et l'analyse détaillée du curseur vocal mot à mot va plus loin dans le timing TTS.
FAQ
Le lecteur exige-t-il un PDF balisé pour fonctionner ?
Non. ReadablePageContent et ReadingUnits retombent sur l'analyse heuristique de mise en page pour les fichiers non balisés, et le champ Source du contenu lisible indique quel chemin a produit l'ordre. La charge revient à votre UI : distinguer l'ordre vérifié par arbre de structure de l'ordre estimé, car les deux échouent différemment et le support doit savoir lequel concerne une plainte.
Pourquoi TrackReadingWordAt retourne-t-il -1 au milieu d'une page ?
En général, l'index caractère de votre moteur TTS se réfère à du texte prétraité avant mise en file, ou tombe sur une espace entre mots. Les offsets doivent pointer dans le texte extrait par le composant, le même que PageWordBoxes a tokenisé, pas dans une copie nettoyée.
Puis-je vérifier la conformité accessibilité programmatiquement ?
Oui. ValidatePdfUa retourne le niveau de conformité détecté et un ensemble de violations PDF/UA par document, et BuildPdfPreflightReport intègre le même contrôle dans un rapport multi-standards. C'est un détecteur, pas un outil de réparation : utilisez le verdict pour fixer les attentes à l'ouverture et trier les fichiers entrants.
Les API d'unités de lecture et de boîtes de mots montrées ici font partie de PDFium Component pour Delphi et C++Builder (VCL) et Lazarus/FPC (LCL). La page produit renvoie à la référence complète, y compris les dispositions d'enregistrements utilisées dans les exemples.