Aprite un report generato con un screen reader e i soliti guasti emergono entro un minuto: «Pagina 3 di 40» viene letto ad alta voce a ogni pagina, il logo dell'azienda viene annunciato come «immagine» senza nulla dopo, e una tabella esce come una lunga fila di numeri senza nomi di colonna. PDF Library for Delphi evita tutti e tre con il tagging automatico della struttura: chiamate SetAutoTagMode una volta, e le normali chiamate di disegno producono paragrafi, titoli, figure, tabelle e furniture di pagina skippabile mentre il documento viene disegnato
La via manuale esiste ancora. Potete avvolgere ogni chiamata di disegno in BeginTag e EndTag, e per documenti con struttura inconsueta è lo strumento giusto. Per un report, una fattura o un estratto conto ordinario, però, il tagging a mano fa dipendere l'accessibilità dal fatto che nessuno dimentichi mai una coppia, in ogni percorso di codice che disegna qualcosa, per tutta la vita del prodotto. Su un requisito di compliance non ci punterei
Cosa attiva SetAutoTagMode
SetAutoTagMode prende una maschera di bit e restituisce la modalità in vigore prima della chiamata. Ogni bit copre un tipo di contenuto:
AUTOTAG_TEXT(1): il testo diventa un paragrafo, o un titolo quando ne è stato registrato unoAUTOTAG_FURNITURE(2): testate, piè di pagina e numeri di pagina diventano artefatti che un lettore saltaAUTOTAG_FIGURE(4): un'immagine disegnata diventa una figura, o un artefatto quando l'avete dichiarata decorativaAUTOTAG_TABLE(8): le tabelle disegnate portano righe e celle nell'albero di strutturaAUTOTAG_DEFAULT(15): tutti e quattro, ed è il valore che raccomandiamo per qualsiasi nuovo codice di report
Attivare la modalità marca anche il documento come tagged, e quel passo conta più di quanto sembri. Secondo ISO 32000-1 §14.7.1 un lettore considera un file come non taggato a meno che il dizionario /MarkInfo del catalogo non dica /Marked true. Un file può portare un albero di struttura completo e corretto e venire comunque annunciato come privo di struttura, perché manca l'unico flag che dice alle tecnologie assistive di cercarla. L'albero c'è. Niente lo legge
Il tagging del testo funziona per chiamata. Ogni chiamata DrawText, DrawWrappedText o DrawTextBox diventa un elemento, quindi un paragrafo disegnato riga per riga con cinque chiamate DrawText arriva al lettore come cinque paragrafi di una riga. Se il vostro codice di layout spezza già le righe da solo, vale la pena cambiare: consegnate l'intero paragrafo a DrawWrappedText e lasciate che una chiamata produca un paragrafo
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1); // origine in alto a sinistra, y cresce verso il basso
Lib.SetDocumentLanguage('en-US');
Lib.SetAutoTagMode(AUTOTAG_DEFAULT); // prima di qualsiasi chiamata RegisterHeading
Lib.AddStandardFont(4);
Lib.SetTextSize(18);
Lib.RegisterHeading(1, 'Annual service report');
Lib.DrawText(72, 96, 'Annual service report'); // taggato H1
Lib.SetTextSize(11);
Lib.DrawWrappedText(72, 130, 450,
'Every unit installed before 2024 was inspected. ' +
'Units that failed the torque check were serviced on site.'); // un solo P
// ... resto delle pagine ...
// Gira su tutte le pagine esistenti, quindi chiamatelo una volta disegnate le pagine
Lib.AddPageFooter('', 'Page &P of &N', 36, 1.0, 1); // Artefatto di paginazione
Lib.SaveToFile('report.pdf');
finally
Lib.Free;
end;
end;
Il commento sulla terza riga c'è per un motivo. RegisterHeading accoda un livello di titolo solo quando il tagging del testo è già attivo, e SetAutoTagMode cancella qualsiasi livello in coda quando gira. Registrare prima il titolo e attivare dopo la modalità fa uscire il titolo come paragrafo semplice, senza alcun errore da nessuna parte a dirvelo
Perché un titolo attende attraverso l'immagine disegnata prima di lui?
RegisterHeading nomina il livello per il prossimo pezzo di testo, e solo il testo può consumarlo. Se in mezzo viene disegnata un'immagine, l'immagine diventa una figura e il titolo resta in coda per il testo che segue. L'implementazione ovvia lascia che qualsiasi tag consumi il livello in coda, e allora la sequenza comune di registrare un titolo di sezione, disegnare un'immagine banner e disegnare il titolo produce un banner taggato come H2 e un vero titolo declassato a paragrafo. Un lettore che naviga per titoli atterra su una foto
Due altre cose seguono da come funziona la coda. Il livello di titolo si attacca a qualunque testo venga dopo, qualunque stringa vi abbiate passato a RegisterHeading: se il vostro layout disegna un piccolo «Sezione 3» sopra il titolo, quel kicker diventa il titolo. E i livelli oltre il sesto vengono scritti come H6, perché un lettore non conosce nulla oltre. L'argomento Title continua a fare il suo lavoro, alimentando l'outline che DrawTOC trasforma poi in un indice
Le immagini ricevono lo stesso trattamento «consumato da un solo elemento». RegisterFigure fornisce la descrizione che la prossima immagine porta con sé, e RegisterDecoration dichiara la prossima immagine una linea, un bordo o uno sfondo privo di significato, che viene poi scritto come artefatto Layout (ISO 32000-1 §14.8.2.2). Ogni dichiarazione viene consumata da esattamente una immagine, quindi un'immagine successiva non eredita mai una descrizione scritta per una precedente. Un testo alternativo attaccato alla foto sbagliata è peggio di niente, perché il lettore afferma qualcosa di falso con piena sicurezza
Non abbiamo volutamente provato a dedurre quali immagini siano decorative. Un'immagine è o contenuto che il lettore deve descrivere o contenuto che deve saltare, e solo il codice chiamante lo sa. Il caso da evitare è il terzo: un'immagine disegnata senza alcuna dichiarazione diventa una figura senza descrizione, fallisce PDF/UA e fa fermare lo screen reader per dire «immagine» e poi il silenzio. Se cercate DrawImage con grep nel vostro codice di report, ogni occorrenza dovrebbe avere una RegisterFigure o una RegisterDecoration subito sopra
Lib.RegisterHeading(2, 'Gearbox inspection'); // in coda per il prossimo testo
Lib.RegisterFigure('Exploded view of the gearbox assembly');
Lib.AddImageFromFile('gearbox.png', 0); // carica e seleziona l'immagine
Lib.DrawImage(72, 160, 360, 270); // diventa una Figure taggata
Lib.RegisterDecoration; // linea priva di significato
Lib.AddImageFromFile('divider.png', 0);
Lib.DrawImage(72, 450, 360, 4); // disegnata dentro un artefatto di layout
Lib.DrawText(72, 480, 'Gearbox inspection'); // solo ora appare l'H2
La descrizione merita più cura di qualsiasi altra stringa nel documento. Un lettore non vedente la ottiene al posto della foto, e questo è tutto ciò che ottiene. «Grafico» non gli dice nulla; «Ricavi trimestrali per regione, con la regione orientale più alta nel Q3» gli dice ciò che un lettore vedente ricava dall'immagine in due secondi
Tabelle: le righe di intestazione si dichiarano, non si indovinano
Con il bit di tabella attivo, DrawTableRows scrive la tabella, le sue righe e le sue celle nell'albero di struttura, così un lettore può dire in quale colonna si trova un valore invece di leggere la griglia come testo non correlato. SetTableHeaderRowCount dice quante righe iniziali sono intestazioni. Quelle righe vengono scritte come celle TH con uno scope di colonna, che è ciò che permette a un lettore di annunciare «Torque» prima del valore su cui l'utente è arrivato
Il numero di righe di intestazione è predefinito a zero, e fu una scelta. La prima riga di una tabella di solito è l'intestazione, quindi indovinare sarebbe giusto la maggior parte delle volte. Il costo dell'errore è però sbilanciato: una riga di dati scambiata per intestazione viene letta ad alta voce davanti a ogni valore della sua colonna, cosa molto più confusionaria di una tabella senza intestazioni. Una riga di codice da parte vostra costa meno di quel guasto
Dichiarare le righe di intestazione e ripeterle su ogni pagina sono decisioni separate. SetTableHeaderRowCount è semantico e non cambia nulla sulla pagina. La ripetizione è layout, e appartiene all'argomento RepeatHeaderRows di DrawTaggedTableRows. Se le due fossero una sola impostazione, chiedere tag di intestazione corretti farebbe anche comparire fasce di intestazione ripetute mai richieste, e un layout verificato pagina per pagina si sposterebbe. Le righe passate come RepeatHeaderRows vengono taggate come intestazioni anche loro, così non dovete dire la stessa cosa due volte
var
TableID: Integer;
begin
TableID := Lib.CreateTable(40, 3);
Lib.SetTableHeaderRowCount(TableID, 1); // la riga 1 è la fascia di intestazione
Lib.SetTableCellContent(TableID, 1, 1, 'Part');
Lib.SetTableCellContent(TableID, 1, 2, 'Torque');
Lib.SetTableCellContent(TableID, 1, 3, 'Unit');
// ... riempi le righe di dati ...
// Disegna le righe 1..40 in una fascia da 600pt, ripetendo una riga di intestazione per pagina
Lib.DrawTaggedTableRows(TableID, 72, 150, 600, 1, 40, 1);
end;
Sezioni taggate a mano dentro un documento automatico
Il tagging automatico si fa da parte mentre un tag aperto a mano è ancora aperto. Questo lascia al vostro codice di descrivere da sé una parte del documento e lasciare il resto alla libreria, senza che i due si annidino l'uno nell'altro, che è l'assetto che la maggior parte dei documenti reali vuole. La copertina e il blocco della firma hanno una struttura che solo voi capite. Le duecento pagine di corpo testo in mezzo no
Tre regole mantengono l'output valido qualunque miscela usiate. Niente viene taggato dentro un artefatto, perché la sequenza di contenuto marcato di un artefatto non può anche portare un elemento di struttura, e un file che ci prova fallisce la validazione PDF/UA. Il testo vuoto non apre alcun elemento, quindi una DrawText dispersa con stringa vuota non può lasciare dietro un elemento che un lettore annuncerebbe come vuoto. E una chiamata di libreria che disegna internamente attraverso altre chiamate di disegno apre un solo tag al livello esterno, così i suoi pezzi non finiscono mai annidati dentro un secondo elemento proprio
Dopo il salvataggio, rileggete l'albero prima di fidarvi. L'API di lettura è 1-based, e ci ha colto in contropiede: il nostro primo controllo ciclava da 0 a StructTreeItemCount - 1, riceveva un record vuoto all'inizio, non arrivava mai all'ultimo elemento, e per un po' eravamo convinti che un paragrafo fosse sparito. Non era sparito nulla. Il ciclo era semplicemente sbagliato di uno su entrambi i lati
var
I: Integer;
begin
// Una citazione evidenziata taggata a mano; il tagging automatico resta fuori
Lib.BeginTag('BlockQuote', '', '');
Lib.DrawWrappedText(72, 520, 450,
'All readings were taken with the unit at operating temperature.');
Lib.EndTag;
Lib.SaveToFile('report.pdf');
// Rileggi l'albero: gli indici vanno da 1 a StructTreeItemCount
if Lib.LoadFromFile('report.pdf', '') = 1 then
for I := 1 to Lib.StructTreeItemCount do
Writeln(Lib.GetStructTreeItemText(I, 2), ' ', // tipo risolto
Lib.GetStructTreeItemText(I, 4)); // /Alt, se presente
end;
Un elenco come questo è un rapido controllo di sanità, non un verdetto. Ciò che volete vedere è un H1 prima del primo paragrafo, ogni figura con la propria descrizione, e nessuna figura con /Alt vuoto
Quando ordine di disegno e ordine di lettura si separano
Il tagging automatico registra gli elementi nell'ordine in cui il contenuto viene disegnato. Se il vostro codice di layout disegna la barra laterale prima del corpo, quello è l'ordine che l'albero conserva, e quello è l'ordine che uno screen reader legge. Per i documenti in cui l'ordine visivo e quello di lettura differiscono davvero, o che hanno bisogno di altri tipi di struttura o di lingua per elemento, l'API di tagging manuale è più adatta, e la guida di tagged PDF e struttura di accessibilità copre ruoli, scope e binding delle intestazioni in dettaglio
Per le pagine che non avete disegnato voi, come le pagine importate senza contenuto marcato, AutoTagPages funziona dopo il fatto: prende la dimensione mediana del testo attraverso le pagine date come corpo del testo e tratta le sequenze marcatamente più grandi come titoli, e marca le altre pitture come artefatti. È lo strumento giusto per contenuto che non potete rigenerare. Per tutto ciò che il vostro codice disegna, preferiamo il tagging al momento del disegno, perché lì alla libreria viene detto che cos'è un titolo invece di indovinarlo dalla dimensione del font
Quando il documento è finito, fatelo girare in un checker invece di presumere: le note su preflight PDF/A e PDF/UA mostrano come ottenere un verdetto sulla struttura che avete prodotto, e l'articolo sull'esportazione di rapporti guidata da dataset mostra dove queste chiamate si collocano in un motore di report che genera il proprio layout dai dati
Poiché PDF Library for Delphi è scritta in Pascal nativo per Delphi, C++Builder e Lazarus, senza alcun runtime PDF esterno da distribuire, l'output taggato viene dallo stesso codice che disegna la pagina. Il riferimento API completo e la lista delle piattaforme sono sulla pagina prodotto PDF Library for Delphi