Tehnički članak

PDF/UA font revizija u Delphi-ju: širine, CharSet, CIDSet

veraPDF izveštaj koji kaže da se širina glifa ne slaže sa ugrađenim font programom ne govori vam skoro ništa o tome koji je glif, ni zašto. PDFlibPas odgovara na to pitanje razrešavanjem svakog koda znaka kroz ugrađeni cmap do indeksa glifa, normalizacijom metrike programa na 1000 jedinica po em, i poređenjem tamo

Zašto se širine glifova ne slažu?

Jer dva broja koja se porede žive u različitim koordinatnim sistemima, i ništa u PDF rečniku vam ne govori konverziju. Font rečnik upisuje /Widths u glyph prostoru, koji PDF fiksira na jednu hiljaditinu em (ISO 32000-1 §9.2.4). hmtx tabela unutar ugrađenog TrueType programa upisuje širine napredovanja u jedinicama dizajna fonta, a head tabela odlučuje koliko njih čini em: 2048 za većinu TrueType faca, 1000 za CFF-izvedene, povremeno nešto sasvim drugo. Uporedite sirove vrednosti i svaki 2048-upem font u vašem korpusu deluje pokvaren. To je zamka koju ISO 14289-1 §7.21.5 postavlja svakome ko pokuša reviziju širina čitanjem rečničkih polja

PDFlibPas revizija širine glifa u Delphi-ju: /Widths unos u glyph prostoru i hmtx širina napredovanja u jedinicama dizajna fonta dovode se u isti koordinatni sistem skaliranjem metrike programa na 1000 jedinica po em pre bilo kakvog poređenja
PDFlibPas skalira svaku hmtx širinu napredovanja na hiljaditinu em pre nego što je uporedi sa rečničkom širinom, i prijavljuje samo ono što odstupa za više od jedne jedinice

PDFlibPas normalizuje pri učitavanju. TPDFTrueTypeParser čuva Advance * 1000 div unitsPerEm u svom nizu širina, pa Parser.GetWidth(GID) već odgovara u istim hiljaditinama em koje PDF koristi, a GetRawWidth ostaje dostupan kada vam trebaju nazad jedinice dizajna. To još ostavlja težu polovinu: doći od koda znaka do indeksa glifa. Za jednostavni TrueType font ruta zavisi od Symbolic zastavice u FontDescriptor-u, bit 3 od /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // Simboličke face se adresiraju direktno kroz programski cmap,
    // sa (3,0) konvencijom visokog bajta kao rezervnim putem
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Nije simbolički: kod -> ime glifa preko kodiranja, ime -> Unicode
    // preko Adobe Glyph List, Unicode -> GID preko programskog cmap
    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;

Dva detalja u tom odlomku nose težinu. Tolerancija je jedna jedinica, ne nula, jer je normalizacija deljenje celih brojeva i legitimno proizveden fajl može odstupiti za jedinicu; to je upravo formulacija „unutar jedne hiljaditine em" koju dijagnostika 10036 prijavljuje. A GID < Parser.GlyphCount čuvar nije dekoracija. GetWidth je napisan da bude popustljiv prema pozivaocima renderovanja, steže indeks van opsega na poslednji unos u hmtx i vraća 750 kada tabela nedostaje. Popustljivost je prava za renderovanje i pogrešna za reviziju, pa revizija odbija indeks pre nego što traži širinu, umesto da veruje stezanju

CIDFontType2 dodaje još jedno posredovanje

PDFlibPas prelazi kompozitne fontove na isti način, sa /CIDToGIDMap umetnutim između CID-a i glifa. Širine stižu u /W nizu, koji ISO 32000-1 §9.7.4.3 daje u dva oblika koja se slobodno smenjuju u jednom nizu: početni CID praćen nizom uzastopnih širina, ili prvi CID, poslednji CID i jedna širina primenjena preko celog niza. Revizija parsira oba, pa svaki dobijeni par predaje istom poređenju, i ukupno prijavljuje pod dijagnostikom 10037. Korak mapiranja je gde se kompozitni fontovi razlikuju, i zato dijagnostika 10021 za nedostajuću mapu važi pre nego što pročitate bilo koju širinu — odsutan ili deformisan /CIDToGIDMap ne samo da krši §7.21.3.2, on čini pitanje širine neodgovorivim

Tri rute koje PDFlibPas prelazi od koda znaka do indeksa glifa u Delphi-ju: programski cmap za simboličke TrueType fontove, zaobilazak preko kodiranja i Adobe Glyph List za nesimboličke, i korak CMap plus /CIDToGIDMap za CIDFontType2
Poređenje širina ne može početi dok se kod znaka ne razreši u indeks glifa, i svaka vrsta fonta stiže do tog indeksa drugom rutom
// /CIDToGIDMap je ime /Identity ili tok big-endian 16-bitnih
// indeksa glifova, jedan po CID-u (ISO 32000-1 sekcija 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;                       // Pascal stringovi su indeksirani od 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;

Šta revizor da radi kada se font program ne može dekodirati?

Ne kaže ništa. Provere kompletnosti /CharSet i /CIDSet koje ISO 14289-1 §7.21.4.2 zahteva — dijagnostika 10038 i 10039 — su mesto gde se previše revan validator pretvara u odgovornost, jer je izveštaj „vaš CharSet je nepotpun" za osobu koja ga čita nerazlikoviv od „naš Type 1 dekoder je odustao". PDFlibPas zato prijavljuje nedostajući unos samo kada tri stvari uspeju: font program se dekodira, mapiranje koda na glif se razreši, i sam skup se dekodira. TPDFType1Decoder.LoadPFBFromString mora vratiti True i dati broj charstring-ova pre nego što bilo koje ime glifa bude provereno protiv /CharSet stringa; /CIDSet putanja traži da tok otpakuje i da broj glifova izađe pozitivan pre nego što se jedan bit testira. Bilo koji izuzetak na putu kolapsira u „nema nalaza", ne u defekt

Konzervativno pravilo prijavljivanja u PDFlibPas PDF/UA reviziji: nedostajući /CharSet ili /CIDSet unos se prijavljuje samo kada se font program dekodira, mapiranje koda na glif razreši i sam skup dekodira, i svaki neuspeh proizvodi tišinu
Tri nezavisna uspeha su potrebna pre nego što se izda nalaz o nedostajućem unosu, pa dekoder koji odustane košta vas lažno negativan nalaz umesto lažne optužbe

To je namerna naklonost lažno negativnim nalazima, i vredi je izreći jasno umesto da je sakrijete. Oštećena CFF tabela, nepodržana varijanta Type 1 ili /CIDSet kraći od opsega glifova proizvode tišinu umesto dijagnostike. Obrazloženje je da se PDF/UA revizije prosleđuju autorima koji nisu gradili alat, a lažna optužba košta više od promašenog nalaza: autor sagori dan dok dokazuje da usklađen fajl jeste usklađen, i prestaje da veruje celom izveštaju. Matterhorn Protocol pravi istu razliku u drugom obliku kada razdvoji provere koje mašina može da odluči od onih koje mora čovek, a njegova Fonts kontrolna tačka (31) je mesto gde one žive. Ako vam treba strože čitanje, koristite PDFlibPas kao brzu kapiju i namenski validator kao drugo mišljenje — taj par opisan je u vodiču kroz PDF/A i PDF/UA preflight

Page /Contents je lista, a ne tok

Najskuplja greška u reviziji content tokova je tretiranje /Contents kao jednog toka. ISO 32000-1 §7.7.3.3 dozvoljava da stranica drži niz tokova čija konkatencacija, sa belinom između delova, čini program stranice; proizvođači seku na proizvoljnim mestima, pa BT može biti u jednom članu dok je njegov ET u sledećem. Procesor sadržaja održava stanje — dubinu ugnežđavanja marked-content, font izabran poslednjim Tf, zastavicu tekst objekta — a Process to stanje resetuje pri ulasku. Pozovite ga jednom po članu niza i svaki tok posle prvog kreće bez tekućeg fonta, pa tekst koji je bio besprekorno označen deluje kao neoznačena buka bez fonta. PDFlibPas prvo spaja, a zatim obrađuje jednom

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;

// Jedan poziv Process nad celom konkatencacijom, nikada jedan poziv po članu
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Koji Form XObjects zaista spadaju u nestrukturisane?

Samo oni koje stranica zaista poziva, sa mesta poziva van marked content, i čiji sopstveni sadržaj prikazuje tekst. Dijagnostika 10040 sprovodi ISO 14289-1 §7.20 tako što po broju objekta beleži tri nezavisne činjenice — ima tekst, bio je pozvan, bio je pozvan unutar marked content — i prijavljuje samo presek prva dva bez treće. Svaka od te dve prečice je pogrešna na način koji biste i otpremili: označavanje svakog Form-a sa tekstom u /Resources kažnjava biblioteku šablona koju niko ne crta, a označavanje svakog pozvanog Form-a kažnjava vektorske logotipe koji ne nose tekst i ne treba im označavanje. Mesto poziva razrešava se po broju objekta, a ne po imenu resursa, jer se isti Form rutinski doseže kroz različita imena na različitim stranicama. Prateća dijagnostika 10041 prelazi isti spojeni program za §7.21.8, razrešavajući svaki operand koji prikazuje tekst kroz font koji je u opsegu i brojeći kodove koji padnu na .notdef, što je zabranjeno bez obzira na režim renderovanja teksta — uključujući i nevidljivi režim iza skeniranih slika. Omotavanje preživelih Form-ova pitanje je strukturnog stabla, i pokriveno je u članku o izgradnji tagged PDF strukture

Fontovi bez ikakvog FontDescriptor-a

Neugrađen font je legitimni ulaz za ovu reviziju, a ne stanje greške, i svaki pomoćnik ispod provere ugrađivanja mora to da preživi. Kada PDFlibPas ne nađe /FontDescriptor, ili nađe deskriptor bez FontFile, FontFile2 ili FontFile3, beleži dijagnostiku 10020 — ili 10022 kada je ime jedno od Standard 14, koje §7.21.4 NOTE 5 uporno odbija da izuzme — i zatim nastavlja kroz ostatak fajla. U tome je cela poenta izveštaja: autor želi svaki nalaz u jednom prolazu, a ne jedan nalaz po pokretanju. Zato referenca deskriptora koja se predaje pomoćnicima za širine, cmap, CharSet i CIDSet može biti Nil, i svaki od njih to proverava pri ulasku umesto da pretpostavi da je ranija provera prekinula reviziju. Ako je rešenje da se ugradi ono što nedostaje, mehanika je opisana u belešci o ugrađivanju nedostajućih fontova u postojeći PDF

Pokretanje revizije

Jedan poziv, na fajlu koji niste nužno ni vi proizveli. TPDFlib.CheckFileCompliance prima selektor compliance testa — 2 za PDF/UA-1 pod ISO 14289-1:2014 — i vraća nulu ili handle string liste čiji su unosi numerički kod, dvotačka i čitljiva poruka. Nalazi o fontovima i content tokovima razmatrani ovde zauzimaju 10020 do 10041 u tom opsegu, numerički odvojeni od 00xxx PDF/A kodova da mešani log ostane čitljiv. Vrednost 1 u Options prekida izvršavanje pri prvom nalazu, što je ono što želite u build kapiji, a ne u alatu za autorstvo. Za dokument koji je još otvoren u memoriji, GetPDFUADiagnostics izvodi ekvivalentan pregled bez odlaska na disk

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 bira PDF/UA-1; Options = 0 prijavljuje svaki nalaz
  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));   // npr. 10037 CIDFontType2 ...
  end;
end;

Ništa od ovoga ne zahteva eksternu binarnu datoteku validatora na mašini, a to je razlika između provere koja se pokreće na svakom buildu i provere koja se pokrene kada se neko seti. Compliance i dijagnostički API opisani ovde stižu uz standardnu PDFlibPas Delphi PDF Library, čija stranica proizvoda nosi kompletnu tabelu dijagnostičkih kodova za PDF/UA-1 uz PDF/A, PDF/X i PDF/E test pakete