Articol tehnic

Audit fonturi PDF/UA în Delphi: Widths, CharSet, CIDSet

Un raport veraPDF care spune că o lățime de glifă nu se potrivește cu programul de font înglobat nu vă spune aproape nimic despre care glifă sau de ce. PDFlibPas răspunde acestei întrebări rezolvând fiecare cod de caracter prin cmap-ul înglobat către un index de glifă, normalizând metrica programului la 1000 de unități per em și comparând acolo

De ce nu se potrivesc lățimile glifelor?

Pentru că cele două numere comparate trăiesc în sisteme de coordonate diferite, iar nimic din dicționarul PDF nu vă spune conversia. Un dicționar de font scrie /Widths în spațiul glifelor, pe care PDF îl fixează la o mieime din em (ISO 32000-1 §9.2.4). Tabela hmtx din interiorul programului TrueType înglobat scrie avansurile în unități de design ale fontului, iar tabela head decide câte asemenea unități fac un em: 2048 pentru majoritatea fonturilor TrueType, 1000 pentru cele derivate din CFF, ocazional altceva cu totul. Comparați valorile brute și fiecare font de 2048-upem din corpusul dumneavoastră pare stricat. Aceasta este capcana pe care ISO 14289-1 §7.21.5 o întinde oricui încearcă să auditeze lățimile citind câmpurile din dicționar

Auditul lățimilor de glife PDFlibPas în Delphi: o intrare /Widths în spațiul glifelor și un avans hmtx în unități de design ale fontului sunt aduse în același sistem de coordonate prin scalarea metricii programului la 1000 de unități per em înainte de orice comparație
PDFlibPas scalează fiecare avans hmtx la o mieime din em înainte de a-l compara cu lățimea din dicționar și raportează doar ce se află la mai mult de o unitate distanță

PDFlibPas normalizează la încărcare. TPDFTrueTypeParser stochează Advance * 1000 div unitsPerEm în vectorul său de lățimi, astfel încât Parser.GetWidth(GID) răspunde deja în aceleași miimi de em pe care le folosește PDF-ul, iar GetRawWidth rămâne disponibilă când aveți nevoie de unități de design înapoi. Rămâne totuși jumătatea mai grea: a ajunge de la un cod de caracter la un index de glifă. Pentru un font TrueType simplu, ruta depinde de fanionul Symbolic din FontDescriptor, bitul 3 din /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // Fonturile simbolice sunt adresate direct prin cmap-ul programului,
    // cu convenția octetului înalt (3,0) ca rezervă
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Non-simbolic: cod -> nume de glifă prin encodare, nume -> Unicode
    // prin Adobe Glyph List, Unicode -> GID prin cmap-ul programului
    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;

Două detalii din acel fragment au greutate. Toleranța este de o unitate, nu zero, pentru că normalizarea este împărțire întreagă și un fișier produs legitim poate ieși cu o unitate decalată; exact acesta este sensul „în cadrul unei miimi de em” pe care îl raportează diagnosticul 10036. Iar garda GID < Parser.GlyphCount nu este decorațiune. GetWidth este scrisă să fie îngăduitoare cu apelanții de randare, fixând un index în afara intervalului la ultima intrare din hmtx și revenind la 750 când tabela lipsește. Îngăduința este corectă pentru randare și greșită pentru audit, astfel încât auditul respinge indexul înainte să ceară o lățime, în loc să aibă încredere în fixare

CIDFontType2 adaugă încă o indirectare

PDFlibPas parcurge fonturile compuse la fel, cu /CIDToGIDMap inserat între CID și glifă. Lățimile sosesc în vectorul /W, căruia ISO 32000-1 §9.7.4.3 îi dă două forme care alternează liber într-un singur vector: un CID de start urmat de un vector de lățimi consecutive, sau un prim CID, un ultim CID și o singură lățime aplicată pe toată secvența. Auditul parsează ambele, apoi dă fiecare pereche rezultată aceleiași comparații și raportează totalul sub diagnosticul 10037. Pasul de mapare este locul în care diferă fonturile compuse, și este motivul pentru care diagnosticul 10021 de hartă lipsă contează înainte să citiți vreo lățime — un /CIDToGIDMap absent sau malformat nu doar încalcă §7.21.3.2, ci face întrebarea lățimii fără răspuns

Trei rute pe care PDFlibPas le ia de la un cod de caracter la un index de glifă în Delphi: cmap-ul programului pentru fonturile TrueType simbolice, un ocol prin encodare și Adobe Glyph List pentru cele non-simbolice, și un pas CMap plus /CIDToGIDMap pentru CIDFontType2
Comparația lățimilor nu poate începe până când codul de caracter se rezolvă într-un index de glifă, iar fiecare fel de font ajunge la acel index pe o rută diferită
// /CIDToGIDMap este numele /Identity sau un flux de indici de glifă
// pe 16 biți big-endian, câte unul per CID (ISO 32000-1 secțiunea 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;                       // șirurile Pascal pornesc de la 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;

Ce ar trebui să facă un auditor când programul de font nu se decodează?

Nu spuneți nimic. Verificările de completitudine /CharSet și /CIDSet pe care le cere ISO 14289-1 §7.21.4.2 — diagnosticele 10038 și 10039 — sunt locul în care un validator prea zelos se transformă într-o răspundere, pentru că un raport „CharSet-ul dumneavoastră este incomplet” este de nedistins, pentru omul care îl citește, de „decoderul nostru Type 1 a renunțat”. PDFlibPas raportează prin urmare o intrare lipsă doar când trei lucruri reușesc toate: programul de font se decodează, maparea cod-către-glifă se rezolvă, iar mulțimea în sine se decodează. TPDFType1Decoder.LoadPFBFromString trebuie să întoarcă True și să producă un contor de charstring-uri înainte ca vreun nume de glifă să fie verificat contra șirului /CharSet; calea /CIDSet are nevoie ca fluxul să se inflateze și ca contorul de glife să revină pozitiv înainte ca un singur bit să fie testat. Orice excepție pe drum colapsează în „fără constatare”, nu într-un defect

Regula conservatoare de raportare din auditul PDF/UA PDFlibPas: o intrare /CharSet sau /CIDSet lipsă este raportată doar când programul de font se decodează, maparea cod-către-glifă se rezolvă și mulțimea în sine se decodează, iar orice eșec produce tăcere
Trei reușite independente sunt cerute înainte de a emite o constatare de intrare lipsă, astfel încât un decodor care renunță vă costă cu un fals negativ, nu cu o acuzație falsă

Este o părtinire deliberată spre falsuri negative, și merită spus pe față, nu îngropat. O tabelă CFF coruptă, o variantă Type 1 nesuportată sau un /CIDSet mai scurt decât intervalul de glife produc toate tăcere în locul unui diagnostic. Raționamentul este că auditările PDF/UA ajung trimise către autori care nu au construit unealta, iar o acuzație falsă costă mai mult decât una ratată: autorul arde o zi dovedind că un fișier conform este conform, și încetează să aibă încredere în tot raportul. Protocolul Matterhorn face aceeași distincție într-o altă formă când separă verificările pe care le poate decide o mașină de cele care cer un om, iar checkpoint-ul său Fonts (31) este locul unde trăiesc acestea. Dacă aveți nevoie de lectura mai strictă, rulați PDFlibPas ca poartă rapidă și un validator dedicat ca a doua opinie — acea pereche este aceeași descrisă în plimbarea de preflight PDF/A și PDF/UA

Pagina /Contents este o listă, nu un flux

Cea mai scumpă greșeală singulară în auditarea fluxurilor de conținut este tratarea /Contents ca un singur flux. ISO 32000-1 §7.7.3.3 lasă o pagină să țină un vector de fluxuri a căror concatenare, cu spațiu alb între părți, este programul paginii; producătorii taie în puncte arbitrare, iar un BT poate sta într-un membru cu ET-ul său pereche în următorul. Un procesor de conținut ține stare — adâncimea de imbricare a conținutului marcat, fontul selectat de ultimul Tf, fanionul de obiect-text — iar Process resetează acea stare la intrare. Chemați-l o dată per membru al vectorului și fiecare flux după primul pornește fără font curent, astfel încât text perfect etichetat se citește ca zgomot fără etichete și fără fonturi. PDFlibPas concatenează întâi și procesează o singură dată

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;

// Un singur apel Process peste toată concatenarea, niciodată un apel per membru
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Care Form XObjects chiar contează ca nestructurate?

Doar cele pe care pagina chiar le invocă, dintr-un loc de apel în afara conținutului marcat, al căror conținut propriu arată text. Diagnosticul 10040 impune ISO 14289-1 §7.20 prin înregistrarea a trei fapte independente per număr de obiect — are text, a fost invocat, a fost invocat în interiorul conținutului marcat — și raportând doar intersecția primelor două minus a treia. Fiecare dintre cele două scurtături este greșită într-un fel pe care l-ați livra: semnalarea fiecărei Forme cu text din /Resources pedepsește o bibliotecă de șabloane din care nimeni nu desenează, iar semnalarea fiecărei Forme invocate pedepsește logo-uri vectoriale care nu poartă text și nu au nevoie de etichetare. Locul de invocare este rezolvat după număr de obiect, nu după nume de resursă, fiindcă aceeași Formă este atinsă de regulă prin nume diferite pe pagini diferite. Diagnosticul companion 10041 parcurge același program concatenat pentru §7.21.8, rezolvând fiecare operand care arată text prin fontul în vigoare și numărând codurile care cad pe .notdef, interzis indiferent de modul de randare a textului — inclusiv modul invizibil folosit în spatele imaginilor scanate. Cum ar trebui învelite Formele supraviețuitoare este o întrebare de arbore de structură, acoperită în articolul despre construirea structurii PDF etichetate

Fonturi fără deloc FontDescriptor

Un font neînglobat este o intrare legitimă pentru acest audit, nu o stare de eroare, și fiecare ajutor de sub verificarea de înglobare trebuie să-i supraviețuiască. Când PDFlibPas nu găsește /FontDescriptor, sau un descriptor fără FontFile, FontFile2 sau FontFile3, înregistrează diagnosticul 10020 — sau 10022 când numele este unul dintre Standard 14, pe care NOTE 5 din §7.21.4 refuză expres să le exime — și apoi continuă prin restul fișierului. Acesta este întregul sens al unui raport: un autor vrea fiecare constatare într-o singură trecere, nu o constatare per rulare. Așadar referința de descriptor dată ajutoarelor de lățime, cmap, CharSet și CIDSet poate fi Nil, iar fiecare dintre ele testează asta la intrare, în loc să presupună că o verificare anterioară a abandonat auditul. Dacă a remedia înseamnă a îngloba ce lipsește, mecanica se află în nota despre înglobarea fonturilor lipsă într-un PDF existent

Rularea auditului

Un singur apel, pe un fișier pe care nu l-ați produs neapărat dumneavoastră. TPDFlib.CheckFileCompliance primește un selector de test de conformitate — 2 pentru PDF/UA-1 sub ISO 14289-1:2014 — și întoarce fie zero, fie un handle de listă de șiruri ale cărei intrări sunt un cod numeric, două puncte și un mesaj lizibil. Constatările de font și de flux de conținut discutate aici ocupă 10020 până la 10041 în acel interval, ținute numeric separat de codurile PDF/A 00xxx pentru ca un jurnal mixt să rămână lizibil. Trecând 1 în Options, se scurtcircuitează la prima constatare, ceea ce vreți într-o poartă de build, nu într-o unealtă de redactare. Pentru un document încă deschis în memorie, GetPDFUADiagnostics rulează inspecția echivalentă fără o rundă prin disc

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 selectează PDF/UA-1; Options = 0 raportează fiecare constatare
  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));   // ex. 10037 CIDFontType2 ...
  end;
end;

Nimic din toate acestea nu are nevoie de un binar validator extern pe mașină, ceea ce este diferența dintre o verificare care rulează la fiecare build și o verificare care rulează când își amintește cineva. API-urile de conformitate și diagnostic descrise aici sosesc în PDFlibPas Delphi PDF Library standard, a cărei pagină de produs poartă tabela completă de coduri de diagnostic pentru PDF/UA-1 alături de suitele de test PDF/A, PDF/X și PDF/E