Un rapporto veraPDF che dice che una larghezza di glifo è in disaccordo con il font program incorporato non ti dice quasi nulla su quale glifo, né perché. PDFlibPas risponde a quella domanda risolvendo ogni codice carattere attraverso il cmap incorporato fino a un indice di glifo, normalizzando la metrica del programma a 1000 unità per em, e confrontando lì
Perché le larghezze dei glifi sono in disaccordo?
Perché i due numeri messi a confronto vivono in sistemi di coordinate diversi, e nulla nel dizionario PDF ti dice la conversione. Un dizionario font scrive /Widths nello spazio glifo, che il PDF fissa a un millesimo dell'em (ISO 32000-1 §9.2.4). La tabella hmtx dentro il programma TrueType incorporato scrive gli avanzamenti in unità di design del font, e la tabella head decide quanti di quelli formano un em: 2048 per la maggior parte dei font TrueType, 1000 per quelli derivati da CFF, occasionalmente qualcosa di completamente diverso. Confronta i valori grezzi e ogni font a 2048 upem del tuo corpus sembra rotto. È la trappola che ISO 14289-1 §7.21.5 tende a chiunque provi a verificare le larghezze leggendo i campi del dizionario
PDFlibPas normalizza al caricamento. TPDFTrueTypeParser memorizza Advance * 1000 div unitsPerEm nel suo array delle larghezze, così Parser.GetWidth(GID) risponde già nei millesimi di em che il PDF usa, e GetRawWidth resta disponibile quando ti servono le unità di design. Resta comunque la metà più difficile: passare da un codice carattere a un indice di glifo. Per un font TrueType semplice la strada dipende dal flag Symbolic nel FontDescriptor, il bit 3 di /Flags
Parser := TPDFTrueTypeParser.Create;
try
Parser.LoadFromString(FontProgram);
if Symbolic then
begin
// I font simbolici si indirizzano direttamente attraverso il cmap
// del programma, con la convenzione high-byte (3,0) come ripiego
GID := Parser.GetGlyphIndex(Code);
if GID = 0 then
GID := Parser.GetGlyphIndex($F000 + Code);
end
else
begin
// Non simbolico: codice -> nome glifo via la codifica, nome -> Unicode
// via l'Adobe Glyph List, Unicode -> GID via il cmap del programma
UnicodeValue := GetGlyphUnicode(EncodingNames[Code and $FF]);
if UnicodeValue = 0 then
Continue;
GID := Parser.GetGlyphIndex(UnicodeValue);
end;
if (GID > 0) and (GID < Parser.GlyphCount) then
if Abs(PDFWidth - Parser.GetWidth(GID)) > 1 then
Inc(MismatchCount);
finally
Parser.Free;
end;
Due dettagli in quel frammento hanno peso. La tolleranza è di una unità, non zero, perché la normalizzazione è una divisione intera e un file prodotto legittimamente può sbagliare di una unità; è esattamente la formulazione "entro un millesimo di un em" che il diagnostico 10036 riporta. E la guardia GID < Parser.GlyphCount non è decorazione. GetWidth è scritto per essere tollerante con i chiamanti di rendering, clampando un indice fuori intervallo all'ultima voce di hmtx e ripiegando a 750 quando la tabella manca. Tollerante è giusto per il rendering e sbagliato per l'audit, così l'audit respinge l'indice prima di chiedere una larghezza invece di fidarsi del clamp
CIDFontType2 aggiunge un ulteriore livello di indirizzamento
PDFlibPas percorre i font compositi allo stesso modo, con /CIDToGIDMap inserito tra il CID e il glifo. Le larghezze arrivano nell'array /W, al quale ISO 32000-1 §9.7.4.3 dà due forme che si alternano liberamente in un unico array: un CID iniziale seguito da un array di larghezze consecutive, oppure un primo CID, un ultimo CID e una singola larghezza applicata all'intera sequenza. L'audit analizza entrambe, passa ogni coppia risultante alla stessa comparazione e riporta il totale sotto il diagnostico 10037. Il passaggio di mappatura è ciò che distingue i font compositi, ed è per questo che il diagnostico 10021 sulla mappa mancante conta prima ancora di leggere una larghezza qualunque — un /CIDToGIDMap assente o malformato non si limita a violare §7.21.3.2, rende la domanda sulle larghezze senza risposta
// /CIDToGIDMap è il nome /Identity oppure uno stream di indici di glifo
// a 16 bit big-endian, uno per CID (ISO 32000-1 sezione 9.7.4.2)
Obj := DerefIndRef(FDoc, CIDFont.FindValueByKeyName('CIDToGIDMap'));
if (Obj is TPDFName) and (TPDFName(Obj).Name = 'Identity') then
begin
GID := CID;
Result := True;
end
else if Obj is TPDFStream then
begin
Data := TPDFStream(Obj).GetDecodedStream;
P := CID * 2 + 1; // le stringhe Pascal partono da 1
if (P >= 1) and (P + 1 <= Length(Data)) then
begin
GID := (Integer(Byte(Data[P])) shl 8) or Integer(Byte(Data[P + 1]));
Result := True;
end;
end;
Cosa deve fare un audit quando il programma font non si decodifica?
Niente. I controlli di completezza su /CharSet e /CIDSet richiesti da ISO 14289-1 §7.21.4.2 — diagnostici 10038 e 10039 — sono il punto in cui un validatore troppo zelante diventa un problema, perché un rapporto "il tuo CharSet è incompleto" è indistinguibile, per chi lo legge, da "il nostro decodificatore Type 1 si è arreso". PDFlibPas quindi riporta una voce mancante solo quando tre cose riescono tutte: il programma font si decodifica, la mappatura codice-glifo si risolve e l'insieme stesso si decodifica. TPDFType1Decoder.LoadPFBFromString deve restituire True e produrre un conteggio di charstring prima che un nome di glifo qualunque venga confrontato con la stringa /CharSet; il percorso /CIDSet richiede che lo stream si gonfi e che il conteggio dei glifi torni positivo prima di testare un solo bit. Qualsiasi eccezione lungo la strada collassa in "nessun riscontro", non in un difetto
È un orientamento deliberato verso i falsi negativi, e vale la pena dichiararlo apertamente invece di nasconderlo. Una tabella CFF corrotta, una variante Type 1 non supportata o un /CIDSet più corto dell'intervallo dei glifi producono tutti silenzio anziché un diagnostico. Il ragionamento è che gli audit PDF/UA vengono inoltrati a autori che non hanno costruito gli strumenti, e una falsa accusa costa più di un riscontro mancato: l'autore brucia una giornata a dimostrare che un file conforme è conforme, e smette di fidarsi dell'intero rapporto. Il Matterhorn Protocol fa la stessa distinzione in forma diversa quando separa i controlli che una macchina può decidere da quelli che richiedono un essere umano, e il suo checkpoint Fonts (31) è dove questi vivono. Se ti serve la lettura più severa, usa PDFlibPas come gate veloce e un validatore dedicato come seconda opinione — quella coppia è la stessa descritta nella guida al preflight PDF/A e PDF/UA
La /Contents di pagina è una lista, non uno stream
L'errore più costoso nell'audit dei content stream è trattare /Contents come uno stream unico. ISO 32000-1 §7.7.3.3 permette a una pagina di contenere un array di stream la cui concatenazione, con spazi bianchi tra le parti, è il programma della pagina; i produttori tagliano in punti arbitrari, e un BT può stare in un membro con il suo ET corrispondente nel successivo. Un processore di contenuti mantiene uno stato — la profondità di annidamento del marked content, il font selezionato dall'ultimo Tf, il flag dell'oggetto testo — e Process azzera quello stato all'ingresso. Chiamalo una volta per membro dell'array e ogni stream dopo il primo parte senza font corrente, così testo taggato alla perfezione si legge come rumore senza tag e senza font. PDFlibPas concatena prima e processa una volta sola
function ContentObjectData(FDoc: TSmartPDFDocument; Obj: TPDFObject): AnsiString;
var
I: Integer;
begin
Result := '';
Obj := DerefIndRef(FDoc, Obj);
if Obj is TPDFStream then
Result := TPDFStream(Obj).GetDecodedStream
else if Obj is TPDFArray then
for I := 0 to TPDFArray(Obj).Count - 1 do
Result := Result + ContentObjectData(FDoc, TPDFArray(Obj).Item[I]) + #10;
end;
// Una chiamata Process su tutta la concatenazione, mai una chiamata per membro
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));
Quali Form XObjects contano davvero come non strutturati?
Solo quelli che una pagina invoca davvero, da un sito di chiamata fuori dal marked content, il cui contenuto mostra testo. Il diagnostico 10040 applica ISO 14289-1 §7.20 registrando tre fatti indipendenti per numero di oggetto — ha testo, è stato invocato, è stato invocato dentro marked content — e riportando solo l'intersezione dei primi due meno il terzo. Ognuna delle due scorciatoie è sbagliata in un modo che spediresti in produzione: segnalare ogni Form con testo presente in /Resources punisce una libreria di template da cui nessuno disegna, e segnalare ogni Form invocato punisce i logo vettoriali che non portano testo e non hanno bisogno di tag. Il sito di invocazione si risolve per numero di oggetto anziché per nome di risorsa, dato che lo stesso Form si raggiunge di routine attraverso nomi diversi su pagine diverse. Il diagnostico gemello 10041 percorre lo stesso programma concatenato per §7.21.8, risolvendo ogni operando che mostra testo attraverso il font in ambito e contando i codici che cadono su .notdef, vietato a prescindere dalla modalità di rendering del testo — inclusa la modalità invisibile usata dietro le immagini scansionate. Come avvolgere i Form sopravvissuti è una domanda sull'albero di struttura, trattata nell'articolo sulla costruzione della struttura PDF taggata
Font senza alcun FontDescriptor
Un font non incorporato è un input legittimo per questo audit, non uno stato di errore, e ogni helper sotto il controllo di incorporamento deve saperlo gestire. Quando PDFlibPas non trova /FontDescriptor, oppure trova un descrittore senza FontFile, FontFile2 o FontFile3, registra il diagnostico 10020 — o 10022 quando il nome è uno dei Standard 14, che §7.21.4 NOTE 5 si rifiuta con decisione di esentare — e poi continua per il resto del file. Questo è il senso di un rapporto: un autore vuole ogni riscontro in un solo passaggio, non un riscontro per esecuzione. Così il riferimento al descrittore passato agli helper di larghezza, cmap, CharSet e CIDSet può essere Nil, e ognuno di essi lo testa all'ingresso invece di dare per scontato che un controllo precedente abbia interrotto l'audit. Se la correzione è incorporare ciò che manca, la meccanica è nella nota sull'incorporamento dei font mancanti in un PDF esistente
Eseguire l'audit
Una chiamata, su un file che non hai necessariamente prodotto tu. TPDFlib.CheckFileCompliance accetta un selettore del test di compliance — 2 per PDF/UA-1 sotto ISO 14289-1:2014 — e restituisce zero oppure un handle di string-list le cui voci sono un codice numerico, due punti e un messaggio leggibile. I riscontri su font e content stream discussi qui occupano 10020-10041 in quell'intervallo, tenuti numericamente separati dai codici 00xxx del PDF/A perché un log misto resti leggibile. Passare 1 in Options interrompe al primo riscontro, che è ciò che vuoi in un gate di build anziché in uno strumento di scrittura. Per un documento ancora aperto in memoria, GetPDFUADiagnostics esegue l'ispezione equivalente senza un giro per il disco
var
Issues, Count, I: Integer;
begin
// ComplianceTest = 2 seleziona PDF/UA-1; Options = 0 riporta ogni riscontro
Issues := PDF.CheckFileCompliance('delivery.pdf', '', 2, 0);
if Issues = 0 then
WriteLn('delivery.pdf: PDF/UA-1 conformant')
else
begin
Count := PDF.GetStringListCount(Issues);
for I := 1 to Count do
WriteLn(' ', PDF.GetStringListItem(Issues, I)); // es. 10037 CIDFontType2 ...
end;
end;
Tutto questo non richiede un binario di validatore esterno sulla macchina, ed è la differenza tra un controllo che gira a ogni build e un controllo che gira quando qualcuno se lo ricorda. Le API di compliance e diagnostica descritte qui sono incluse nella standard PDFlibPas Delphi PDF Library, la cui pagina prodotto porta la tabella completa dei codici diagnostici per PDF/UA-1 insieme alle suite di test PDF/A, PDF/X e PDF/E