Articolo tecnico

Leggere e scrivere marked content PDF in Delphi

Il marked content è il meccanismo che ISO 32000-1 §14.6 definisce per taggare il contenuto di pagina, e sia il tagged PDF sia PDF/UA sono costruiti su di esso. PDFium Component lo espone direttamente: PageObjectMarks legge ogni tag BDC e la sua property list da un oggetto di pagina, AddPageObjectMark ne scrive uno, RemovePageObjectMark ne cancella uno, e PageObjectMarkedContentID riporta l'MCID che lega il contenuto all'albero di struttura

Fino a quando l'albero di struttura non può essere ricondotto al contenuto che descrive, il lavoro degli strumenti di accessibilità è un'ipotesi. L'albero di struttura dice «questo è un titolo»; l'MCID dice quali mark su quale pagina è davvero quel titolo. Entrambe le metà devono essere leggibili prima che un'applicazione possa controllare, riparare o riportare il tagging

Cos'è un mark, in byte?

Un operatore BDC con un tag name e una property list opzionale, chiuso da EMC. Nel content stream appare come /P <</MCID 3>> BDC ... EMC: il tag /P nomina il ruolo, il dizionario porta le proprietà, e tutto ciò che sta tra gli operatori è il marked content. Un oggetto di pagina dentro quello span porta il mark, che è ciò che PDFium restituisce e che PDFium Component trasforma in un record

TPdfContentMark tiene un handle, il Name del tag, e un array di TPdfContentMarkParam. Ciascun parametro ha una Key, un Kind e un campo valore significativo unico selezionato da quel kind: pmpInt, pmpFloat, pmpString o pmpBlob. Il kind deriva dal report di tipo di PDFium stesso anziché da quale getter è riuscito a avere successo, che è la differenza tra leggere una property list e tirare a indovinarne una

var
  Marks: TPdfContentMarks;
  M: TPdfContentMark;
  P: TPdfContentMarkParam;
  I: Integer;
begin
  Pdf.PageNumber := 1;                    // PageNumber is 1-based
  for I := 0 to Pdf.ObjectCount - 1 do    // page object indexes are 0-based
  begin
    Marks := Pdf.PageObjectMarks(I);
    for M in Marks do
    begin
      Memo1.Lines.Add('mark ' + M.Name +
        ' (MCID ' + IntToStr(Pdf.PageObjectMarkedContentID(I)) + ')');
      for P in M.Params do
        case P.Kind of
          pmpInt:    Memo1.Lines.Add('  ' + P.Key + ' = ' + IntToStr(P.IntValue));
          pmpString: Memo1.Lines.Add('  ' + P.Key + ' = ' + P.StringValue);
          pmpFloat:  Memo1.Lines.Add('  ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
          pmpBlob:   Memo1.Lines.Add('  ' + P.Key + ' = ' +
                       IntToStr(Length(P.BlobValue)) + ' bytes');
        end;
    end;
  end;
end;

Perché pmpUnknown significa due cose diverse

pmpUnknown viene restituito quando PDFium riporta FPDF_OBJECT_UNKNOWN, e PDFium lo restituisce anche per una chiave che non esiste. I due casi non si possono distinguere a questo livello, e far finta del contrario sarebbe peggio che ammetterlo

La conseguenza pratica per il tuo codice: tratta pmpUnknown come «nessun valore utilizzabile qui» anziché come un tipo che potresti comunque decodificare. Se una proprietà conta per il tuo flusso, verifica che sia presente con un kind che riconosci, e non dedurne l'assenza da un unknown — un mark la cui property list non riesci a leggere è un mark su cui dovresti riportare, non uno che dovresti accettare silenziosamente

Un record di mark è uno snapshot, non un handle che possiedi

Il campo Handle appartiene alla libreria. Diventa stale nel momento in cui il mark viene rimosso, l'oggetto di pagina distrutto o la pagina scaricata, così il record è uno snapshot read-only con vita breve. Lo cachia attraverso un cambio di pagina e tieni in mano un puntatore in memoria che il motore ha reclamizzato

È la stessa disciplina che si applica agli handle di oggetti di pagina in generale in PDFium, e intercetta le persone nello stesso punto: un controllo lista popolato con record di mark, un utente che naviga a un'altra pagina, e un crash che sembra non correlato alla navigazione. Copia fuori i valori che ti servono — il nome, le chiavi, i numeri — e lascia andare l'handle. Le note sugli handle di oggetti di pagina che diventano stale dopo una trasformazione coprono la regola generale e come morde altrove

Aggiungere un mark, e lo step di salvataggio facile da perdere

AddPageObjectMark prende l'indice dell'oggetto di pagina, un tag name e un insieme completo di parametri. I parametri vengono scritti come insieme anziché rattoppati una chiave alla volta, il che è il motivo per cui TPdfContentMarkParam non ha sentinel Has* — il caso «aggiorna un campo di un record esistente» che quelli proteggerebbero non si presenta

La parte che vale la pena dire esplicitamente: aggiungere un mark ricostruisce il content stream della pagina così che il tag sopravviva a un salvataggio. Questo doveva essere esplicito perché SaveAs non rigenera il contenuto da solo — una modifica vissuta solo nel modello a oggetti verrebbe scartata, e il file salvato apparirebbe esattamente come quello da cui sei partito. Se hai mai aggiunto qualcosa a una pagina PDFium e l'hai trovato mancante nell'output, di solito è questo il motivo

var
  Params: TPdfContentMarkParams;
begin
  SetLength(Params, 1);
  Params[0].Key := 'MCID';
  Params[0].Kind := pmpInt;
  Params[0].IntValue := NextMcid;
  Pdf.AddPageObjectMark(ObjectIndex, 'P', Params);   // rebuilds the content stream
  Pdf.UpdatePage;
  Pdf.SaveAs('tagged-out.pdf');
end;

Cosa questo fa e non fa di un documento

I mark da soli non fanno un PDF tagged. Un documento tagged conforme ha bisogno di un albero di struttura i cui elementi referenzino questi MCID, di una voce /MarkInfo che dichiari il documento marcato, e di nomi di ruolo che significhino ciò che lo standard dice che significano. Scrivere un mark /P con un MCID a cui nessun elemento di struttura punta ti dà contenuto che rivendica di essere taggato e un albero di struttura che non lo menziona mai

Dove il marked content guadagna davvero il suo valore a questo livello è ispezione e riparazione: fare l'audit di quali oggetti di pagina sono taggati, trovare artefatti che avrebbero dovuto essere marcati come tali, o confrontare gli MCID con un albero di struttura per trovare gli orfani. Per la metà albero di struttura di quel lavoro, vedi la guida sulla validazione dell'albero di struttura PDF/UA, e per l'esperienza di lettura a cui i tag sono infine destinati, le note sulla costruzione di un lettore PDF accessibile in Delphi

PDFium Component dà alle applicazioni Delphi, C++Builder e Lazarus un'API VCL di alto livello sul motore PDFium, con marked content, alberi di struttura e validazione di accessibilità raggiungibili da codice Pascal ordinario — vedi la pagina prodotto PDFium Component per la superficie API completa