Articolo tecnico

Validazione PDF/UA-2 in Delphi: non è UA-1 parte 2

PDFlibPas valida i file PDF/UA-2 (ISO 14289-2) in Delphi tramite TPDFlib.CheckFilePDFUACompliance con target part pari a 2, che esegue un profilo PDF/UA-2 separato: risolve i ruoli della struttura attraverso i namespace PDF 2.0 e /RoleMapNS, segnala un input troncato o illeggibile come fallimento e restituisce un report di copertura che non rivendica mai la certificazione completa

Quest'ultima parte conta più di quanto sembri. La validazione dell'accessibilità è un campo in cui il segno di spunta verde finisce copiato in un foglio di calcolo degli acquisti e non viene più messo in discussione. Un validatore che salta in silenzio metà dello standard e stampa comunque «pass» è peggiore di nessun validatore, perché trasforma un'incognita in un falso noto. PDFlibPas prende la posizione opposta: verificare ciò che si può verificare meccanicamente, dichiarare ad alta voce ciò che non è stato verificato e trattare ogni «impossibile completare» come un rilievo, non come un successo

Perché non si può validare PDF/UA-2 con le regole di PDF/UA-1?

PDF/UA-2 non è PDF/UA-1 con il numero di parte incrementato. PDF/UA-1 poggia su PDF 1.7 e su un'unica role map piatta; PDF/UA-2 poggia su PDF 2.0 (ISO 32000-2), dove ogni elemento della struttura può appartenere a un namespace e ogni namespace porta con sé la propria role map. Un checker UA-1 puntato su un file UA-2 sia tralascia problemi reali sia ne inventa di finti

Quelli finti sono la parte imbarazzante. I validatori PDF/UA-1 di solito impongono regole sugli elementi Note che devono portare un ID e su come si può usare l'intestazione non numerata H. Nessuna delle due regole si trasferisce. Nel namespace di struttura PDF 2.0 le note a piè di pagina si esprimono con FENote, e le intestazioni vanno da H1 a Hn senza tetto di sei livelli. PDF/UA-2 fa di più e vieta proprio il ruolo H non numerato. Applicate la logica UA-1 delle intestazioni a quell'albero e vi ritroverete a lamentarvi di cose che lo standard consente o a far passare l'unica cosa che vieta

Il profilo PDF/UA-2 di PDFlibPas è quindi un percorso di codice separato, non un flag su quello vecchio. E si rifiuta anche di riusare i numeri di clausola UA-1. Le diagnostiche condivise con il checker UA-1 vengono riassegnate a ISO 14289-2 nei report UA-2, e dove un numero di clausola non è stato verificato contro il testo dello standard, il report lascia vuoto il campo clausola invece di tirare a indovinare. Una clausola vuota soddisfa meno di un numero di clausola sicuro di sé, ma è l'unico output difendibile quando nessuno ha confermato la corrispondenza

Come PDFlibPas risolve i ruoli attraverso RoleMapNS

PDFlibPas risolve ogni ruolo della struttura in un ruolo standard in un namespace noto, seguendo le catene /RoleMapNS attraverso i namespace, e segnala ogni catena che finisce su un ruolo personalizzato, va in loop o viola le regole di mappatura. È il cuore della validazione di struttura PDF/UA-2, perché le tecnologie assistive capiscono solo i ruoli standard; un /InvoiceLine personalizzato non significa nulla finché non approda su qualcosa come P o TR

Il resolver parte dal nome /S dell'elemento e dal suo namespace /NS. Senza namespace, l'elemento sta implicitamente nel namespace standard PDF 1.7 e si applica la /RoleMap legacy alla radice dell'albero della struttura. Con un namespace, si applica la /RoleMapNS del dizionario del namespace stesso, e una mappatura può puntare a un ruolo in un namespace diverso tramite un array di due elementi. I controlli che ne vengono fuori sono concreti:

  • Un namespace referenziato da un elemento della struttura deve essere un dizionario indiretto con un URI /NS valido ed essere elencato nell'array /Namespaces della radice dell'albero della struttura (diagnostica 10054)
  • Un ruolo personalizzato deve prima o poi risolversi in un ruolo standard nel namespace PDF 1.7, nel namespace PDF 2.0 (http://iso.org/pdf2/ssn) o nel namespace MathML; catene circolari e vicoli ciechi vengono riportati come 10055
  • Un ruolo standard non può essere rimappato a uno personalizzato, e un ruolo non può essere rimappato dentro il proprio namespace esplicito
  • La radice della struttura deve contenere esattamente un elemento Document nel namespace PDF 2.0 (10057), e il /P di ogni elemento deve puntare al suo genitore effettivo (10058)
  • Una H non numerata, o una Note legacy PDF 1.7 non mappata a un ruolo PDF 2.0, viene riportata come 10056

Un dettaglio che vale la pena copiare nei vostri strumenti: PDFlibPas valida ogni mappatura dichiarata, non soltanto quelle raggiungibili dall'albero. Una voce rotta in una role map che nessun elemento usa al momento è comunque un difetto latente, perché il prossimo aggiornamento dei contenuti inizierà a usarla. Anche MathML è trattato onestamente. Il namespace MathML è riconosciuto come obiettivo di risoluzione valido, ma il vocabolario MathML in sé non viene validato, e il report di copertura lo dichiara

Eseguire un controllo PDF/UA-2 da Delphi

TPDFlib.CheckFilePDFUACompliance è il punto d'ingresso a chiamata unica: passate file, password, target part e opzioni, e vi torna un handle di string list contenente i problemi codificati. Un handle pari a zero con LastErrorCode uguale a zero significa che i controlli implementati non hanno trovato nulla; un LastErrorCode diverso da zero significa che la chiamata stessa è fallita

var
  Lib: TPDFlib;
  Issues, I: Integer;
begin
  Lib := TPDFlib.Create;
  try
    // TargetPart: 0 = rileva da XMP pdfuaid:part, 1 = PDF/UA-1, 2 = PDF/UA-2
    // Options: 1 = fermati al primo fallimento, 0 = raccogli ogni problema
    Issues := Lib.CheckFilePDFUACompliance('report.pdf', '', 2, 0);
    if Issues = 0 then
    begin
      if Lib.LastErrorCode <> 0 then
        Writeln('Validation did not run, error ', Lib.LastErrorCode)
      else
        Writeln('No issues from the implemented PDF/UA-2 checks');
    end
    else
    try
      for I := 1 to Lib.GetStringListCount(Issues) do  // gli elementi partono da 1
        Writeln(Lib.GetStringListItem(Issues, I));
    finally
      Lib.ReleaseStringList(Issues);
    end;
  finally
    Lib.Free;
  end;
end;

Scegliete il target part con attenzione. Un target di 0 legge pdfuaid:part dai metadati XMP e da lì ricava il profilo, scelta giusta per un validatore generico di caselle in ingresso. Per la vostra pipeline di output passate esplicitamente 2. Se il vostro generatore ha dimenticato di scrivere l'identificazione, l'auto-rilevamento ripiega sulle regole UA-1 e validate lo standard sbagliato; un 2 esplicito produce invece la diagnostica 10050 per la parte mancante, più 10051 se pdfuaid:rev non è 2024 e 10052 se la versione effettiva del PDF non è 2.0. Sul lato di scrittura, TPDFlib.SetPDFUA2Mode scrive quell'identificazione, porta la versione a 2.0 e seleziona il namespace di struttura PDF 2.0 come predefinito; il modello dei namespace è spiegato nella guida ai namespace PDF 2.0, PDF/A-4 e PDF/UA-2

Cosa succede quando il validatore esaurisce il budget?

PDFlibPas fallisce in modo chiuso: quando il checker PDF/UA-2 incontra un limite di attraversamento, un ciclo, un limite del parser o un documento che non riesce ad aprire, emette una diagnostica, e un risultato senza diagnostiche non viene mai promosso a superato. La diagnostica 10059 è riservata proprio al caso «il lettore non è riuscito a finire»

I budget sono espliciti e pubblicati nel JSON di copertura: profondità della struttura 256, 100.000 oggetti di struttura e 256 salti lungo qualunque catena di role map. Quei numeri esistono perché gli alberi di struttura arrivano da input non fidato. Un file ostile o semplicemente rotto può annidare elementi migliaia di livelli in profondità, far puntare un figlio verso un antenato o costruire una role map che si incatena attraverso centinaia di namespace. Un validatore senza budget o crasha o resta bloccato, e un validatore con budget ma senza diagnostiche fa qualcosa di più sottile e di peggiore: si ferma presto, non trova nulla di sbagliato nella parte che ha letto e riporta successo

Lo stesso principio vale un livello più su. Se il lettore sottostante restituisce un punteggio sotto 100 ma nessuna riga di problema, cosa che può accadere quando il parsing si interrompe prima che una regola qualsiasi sia girata, PDFlibPas inserisce una riga 10059 così l'elenco vuoto non può essere scambiato per un risultato pulito. Anche un'eccezione durante il controllo diventa una riga 10059. È poco codice, e chiude il modo più comune con cui le pipeline di validazione mentono ai loro proprietari

Condividere un solo parse con PDF/A e costruire un report

Per il lavoro in batch, la API della pipeline di validazione parsa un file una volta sola ed esegue più controlli contro il parse condiviso. RunPDFUAValidationPipelineTask è il membro PDF/UA di quella famiglia e accetta gli stessi valori di target part della funzione a chiamata unica, così un preflight PDF/A più PDF/UA-2 non paga due letture complete di un documento grande

var
  Pipe, PDFAIssues, UAIssues: Integer;
  Json: WideString;
begin
  Pipe := Lib.BeginValidationPipeline('archive.pdf', '');
  if Pipe = 0 then
    Exit;
  try
    PDFAIssues := Lib.RunValidationPipelineTask(Pipe, 1, 0);       // 1 = PDF/A
    UAIssues := Lib.RunPDFUAValidationPipelineTask(Pipe, 2, 0);    // PDF/UA-2
    // ... ispezionate e rilasciate entrambi gli handle come nell'esempio precedente ...
    Writeln('Parses: ', Lib.GetValidationPipelineParseCount(Pipe));
    // ComplianceTests: 1 = PDF/A, 2 = PDF/UA, 0 = entrambi; ReportFormat 1 = JSON
    Json := Lib.CreateValidationPipelineReport(Pipe, 0, 0, 1);
  finally
    Lib.ReleaseValidationPipeline(Pipe);
  end;
end;

Il report strutturato etichetta la sezione di accessibilità come PDF/UA-2 e ISO 14289-2 quando il file si identifica come parte 2, e ogni sezione PDF/UA porta "fullCertification": false insieme all'oggetto di copertura incorporato. Il report di preflight testuale aggiunge una riga che dichiara che sono girati solo i controlli implementati e che la certificazione completa dell'accessibilità richiede validazione aggiuntiva e revisione umana. I formati di report più ampi, gli ID delle regole e i pattern di gating CI sono descritti nella guida al preflight PDF/A e PDF/UA

Cosa garantisce davvero il report di copertura?

TPDFlib.GetPDFUAReadValidationCoverageJSON garantisce una cosa: un elenco esatto di ciò che il checker PDF/UA-2 ha esaminato e di ciò che non ha esaminato. Per la parte 2 restituisce fullCertification fissato a false, un array checked, un array unverified e i limiti di attraversamento. Non garantisce che un file sia accessibile, ed è progettato in modo che nessun percorso di codice possa fargli dire il contrario

var
  Coverage: WideString;
begin
  Coverage := Lib.GetPDFUAReadValidationCoverageJSON(2);
  // Conservatelo accanto al risultato della validazione, non al posto suo
  TFile.WriteAllText('report.coverage.json', Coverage, TEncoding.UTF8);
  if Pos('"fullCertification":false', Coverage) > 0 then
    Writeln('Machine checks only; schedule a human accessibility review');
end;

L'elenco checked copre l'identificazione (parte, revisione, prefisso), la versione effettiva del PDF, i metadati del titolo, /MarkInfo, DisplayDocTitle, i riferimenti ai namespace, la risoluzione delle role map, la radice del documento, i genitori della struttura, i cicli dell'albero e le regole su H e Note. L'elenco unverified è altrettanto specifico: taggatura dei contenuti e ordine di lettura, contenimento della struttura, font e mappatura Unicode, annotazioni e moduli, lingua naturale, descrizioni alternative, il vocabolario MathML e la revisione umana dell'accessibilità. Diversi di questi non sono nemmeno decidibili meccanicamente. Nessun parser può dirvi se un alt text descrive davvero il grafico, o se l'ordine di lettura abbia senso per chi lo sta ascoltando

Trattate il JSON di copertura come parte del risultato. Archiviatelo con ogni esecuzione della validazione, e quando un auditor chiede cosa significasse «superato» in una data precisa, potete consegnare l'ambito esatto invece di ricostruirlo a memoria e da un numero di versione

Controllo incrociato con veraPDF

Il nucleo PDF/UA-2 è stato verificato in modo incrociato contro il validatore a riga di comando veraPDF 1.30.3 usando il suo profilo ua2, e i risultati tornano dove la copertura si sovrappone. Un campione valido del nucleo generato indipendentemente ha superato entrambi gli strumenti; un campione che dichiara pdfuaid:rev 2023 e un campione che usa il ruolo H non numerato hanno fallito entrambi gli strumenti

Il quarto campione è quello di cui vale la pena parlare. Conteneva contenuti di disegno per nulla taggati, e veraPDF l'ha segnalato mentre i controlli del nucleo PDFlibPas no, perché la taggatura dei contenuti sta fuori dalla copertura attuale. Quella lacuna è esattamente il motivo per cui content-tagging-and-reading-order sta in cima all'elenco unverified. Far girare un secondo validatore è un'assicurazione economica, e un disaccordo fra due strumenti è spesso il modo più rapido di trovare il confine di ciò che ciascuno controlla. Per il lato di scrittura dello stesso problema, costruire alberi che superino i controlli alla fonte, vedere la guida all'albero della struttura del PDF taggato

FAQ

PDFlibPas certifica un file come conforme a PDF/UA-2?

No. PDFlibPas esegue i controlli macchina implementati per PDF/UA-2 e restituisce un report di copertura con fullCertification sempre false, elencando ogni area che ha ancora bisogno di altri strumenti o di revisione umana

Quale valore di TargetPart passare a CheckFilePDFUACompliance?

Passate 2 quando sapete che il file deve essere PDF/UA-2, così l'identificazione mancante viene riportata. Passate 0 per rilevare automaticamente la parte da XMP pdfuaid:part, e 1 per forzare le regole PDF/UA-1

Cosa significa la diagnostica 10059 in un report PDF/UA-2?

La diagnostica 10059 significa che il validatore non è riuscito a completare: il file non è stato parsato o aperto, un limite di attraversamento è stato superato, è stato trovato un ciclo o si è verificata un'eccezione. Conta sempre come fallimento, mai come superato

La validazione PDF/UA-2, la pipeline di validazione condivisa e le API di scrittura PDF 2.0 sono incluse nella losLab PDF Library for Delphi, insieme al preflight PDF/A e ai report di validazione strutturati