Tehnički članak

PDF/UA revizija fontova u Delphiju: širine, CharSet, CIDSet

veraPDF izvještaj koji kaže da se širina glifa ne slaže s ugrađenim font programom ne govori vam gotovo ništa o tome koji je glif ni zašto. PDFlibPas na to pitanje odgovara tako što svaki znakovni kod razriješi kroz ugrađeni cmap do indeksa glifa, normalizira metriku programa na 1000 jedinica po emu, i uspoređuje tamo

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

Jer dva uspoređivana broja žive u različitim koordinatnim sustavima, i ništa u PDF rječniku ne govori konverziju. Font rječnik piše /Widths u prostoru glifova, koji PDF fiksira na tisućiti dio ema (ISO 32000-1 §9.2.4). Tablica hmtx unutar ugrađenog TrueType programa piše napredovanja u jedinicama dizajna fonta, i tablica head odlučuje koliko ih čini em: 2048 za većinu TrueType pisama, 1000 za CFF-izvedena, povremeno nešto sasvim drugo. Usporedite sirove vrijednosti i svaki 2048-upem font u vašem korpusu izgleda pokvaren. To je zamka koju ISO 14289-1 §7.21.5 postavlja svakome tko pokuša revidirati širine čitajući polja rječnika

PDFlibPas revizija širina glifova u Delphiju: stavka /Widths u prostoru glifova i hmtx napredovanje u jedinicama dizajna fonta dovode se u isti koordinatni sustav skaliranjem metrike programa na 1000 jedinica po emu prije bilo kakve usporedbe
PDFlibPas skalira svako hmtx napredovanje na tisućiti dio ema prije usporedbe sa širinom rječnika i izvještava samo ono što je udaljenije više od jedne jedinice

PDFlibPas normalizira pri učitavanju. TPDFTrueTypeParser sprema Advance * 1000 div unitsPerEm u svoje polje širina, pa Parser.GetWidth(GID) već odgovara u istim tisućitima ema koje koristi PDF, a GetRawWidth ostaje dostupan kad vam trebaju jedinice dizajna nazad. To još ostavlja težu polovicu: doći od znakovnog koda do indeksa glifa. Za jednostavni TrueType font put ovisi o Symbolic zastavici u FontDescriptoru, bit 3 od /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // Simbolična pisma adresiraju se izravno kroz cmap programa,
    // s (3,0) konvencijom visokog bajta kao rezervnim putem
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Nesimbolično: kod -> ime glifa preko kodiranja, ime -> Unicode
    // preko Adobe Glyph List, Unicode -> GID preko cmap programa
    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;

Dvije pojedinosti u tom ulomku nose težinu. Tolerancija je jedna jedinica, a ne nula, jer je normalizacija cjelobrojno dijeljenje i zakonito proizvedena datoteka može odstupiti za jedinicu; to je točno formulacija "unutar jednog tisućitog ema" koju dijagnostika 10036 izvještava. I čuvar GID < Parser.GlyphCount nije dekoracija. GetWidth napisan je da bude popustljiv prema pozivateljima prikazivanja, steže indeks izvan raspona na zadnju stavku u hmtx i vraća 750 kad tablice nema. Popustljivo je ispravno za prikazivanje i pogrešno za reviziju, pa revizija odbacuje indeks prije nego što zatraži širinu umjesto da vjeruje stezanju

CIDFontType2 dodaje još jedno posredovanje

PDFlibPas prolazi složene fontove na isti način, s /CIDToGIDMap umetnutim između CID-a i glifa. Širine stižu u /W polju, koje ISO 32000-1 §9.7.4.3 daje u dva oblika koja se slobodno izmjenjuju u jednom polju: početni CID iza kojega slijedi polje uzastopnih širina, ili prvi CID, zadnji CID i jedna širina primijenjena na cijeli raspon. Revizija parsira oba, zatim svaki nastali par predaje istoj usporedbi i izvještava ukupno pod dijagnostikom 10037. Korak mapiranja je mjesto gdje se složeni fontovi razlikuju, i zato dijagnostika 10021 za nedostajuću mapu važi prije nego što pročitate bilo koju širinu — odsutan ili deformiran /CIDToGIDMap ne krši samo §7.21.3.2, on pitanje širine čini neodgovorivim

Tri puta kojima PDFlibPas u Delphiju od znakovnog koda dolazi do indeksa glifa: cmap programa za simbolične TrueType fontove, zaobilazak preko kodiranja i Adobe Glyph List za nesimbolične, te korak CMap plus /CIDToGIDMap za CIDFontType2
Usporedba širina ne može početi dok se znakovni kod ne razriješi u indeks glifa, i svaka vrsta fonta do tog indeksa dolazi drugim putem
// /CIDToGIDMap je ime /Identity ili tok big-endian 16-bitnih
// indeksa glifova, jedan po CID-u (ISO 32000-1 odjeljak 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 nizovi indeksiraju se 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;

Što bi revizor trebao učiniti kad se font program ne može dekodirati?

Ništa ne reći. Provjere potpunosti /CharSet i /CIDSet koje ISO 14289-1 §7.21.4.2 traži — dijagnostika 10038 i 10039 — mjesto su gdje se previše revan verificiratelj pretvara u teret, jer izvještaj "vaš CharSet je nepotpun" osobi koja ga čita nije razlučiv od "naš Type 1 dekoder se predao". PDFlibPas zato izvještava nedostajuću stavku samo kad tri stvari uspiju: font program se dekodira, mapiranje koda u glif se razriješi, i sam se skup dekodira. TPDFType1Decoder.LoadPFBFromString mora vratiti True i dati broj charstringova prije nego što se bilo koje ime glifa provjeri protiv /CharSet niza; /CIDSet put treba da se tok dekomprimira i da se broj glifova vrati pozitivan prije ispitivanja jednog jedinog bita. Svaka iznimka usput sažme se u "bez nalaza", a ne u nedostatak

Konzervativno pravilo izvještavanja u PDFlibPas PDF/UA reviziji: nedostajuća stavka /CharSet ili /CIDSet izvještava se samo kad se font program dekodira, mapiranje koda u glif razriješi i sam se skup dekodira, a svaki neuspjeh proizvodi tišinu
Tri neovisna uspjeha potrebna su prije nego što se ispušta nalaz nedostajuće stavke, pa dekoder koji se preda košta vas lažno negativnog, a ne lažne optužbe

To je namjerna pristranost prema lažno negativnim nalazima, i vrijedi je izreći otvoreno, a ne zakopati. Pokvarena CFF tablica, nepodržana Type 1 varijanta ili /CIDSet kraći od raspona glifova proizvode tišinu umjesto dijagnostike. Obrazloženje jest da se PDF/UA revizije prosljeđuju autorima koji nisu gradili alate, i lažna optužba košta više od promašene: autor sagori dan dokazujući da usklađena datoteka jest usklađena i prestaje vjerovati cijelom izvještaju. Matterhorn Protocol istu razliku čini u drugom obliku kad razdvaja provjere koje stroj može odlučiti od provjera koje mora čovjek, i njegova Fonts kontrolna točka (31) mjesto je gdje ovo živi. Ako trebate strože čitanje, pustite PDFlibPas kao brzu kapiju i namjenski verificiratelj kao drugo mišljenje — taj par opisan je u prošetnji PDF/A i PDF/UA preflightom

Stranični /Contents popis je, a ne tok

Najskuplja pojedinačna pogreška u reviziji tokova sadržaja jest tretirati /Contents kao jedan tok. ISO 32000-1 §7.7.3.3 dopušta stranici držati polje tokova čija je konkatenacija, s razmakom između dijelova, stranični program; proizvođači režu na proizvoljnim mjestima, i BT može sjediti u jednom članu sa svojim odgovarajućim ET u sljedećem. Procesor sadržaja drži stanje — dubinu gniježđenja označenog sadržaja, font odabran posljednjim Tf, zastavicu tekst objekta — i Process to stanje resetira pri ulasku. Pozovite ga jednom po članu polja i svaki tok nakon prvog kreće bez trenutačnog fonta, pa tekst koji je bio savršeno označen čita se kao neoznačeni šum bez fonta. PDFlibPas najprije konkatenira pa jednom obrađuje

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 Process poziv nad cijelom konkatenacijom, nikad jedan poziv po članu
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Koji Form XObjects stvarno računaju se kao nestrukturirani?

Samo oni koje stranica stvarno pozove, s pozivnog mjesta izvan označenog sadržaja, i čiji vlastiti sadržaj pokazuje tekst. Dijagnostika 10040 provodi ISO 14289-1 §7.20 bilježeći tri neovisne činjenice po broju objekta — ima tekst, bio je pozvan, bio je pozvan unutar označenog sadržaja — i izvještava samo presjek prvih dviju umanjen za treću. Svaka od te dvije prečice pogrešna je na način koji biste isporučili: označiti svaki Form s tekstom u /Resources kazni knjižnicu predložaka iz koje nitko ne crta, a označiti svaki pozvani Form kazni vektorske logotipe koji ne nose tekst i ne trebaju označavanje. Mjesto poziva razrješuje se brojem objekta, a ne imenom resursa, jer se isti Form redovito dohvaća različitim imenima na različitim stranicama. Prateća dijagnostika 10041 prolazi isti konkatenirani program za §7.21.8, razrješujući svaki operand koji pokazuje tekst kroz font u opsegu i brojeći kodove koji padnu na .notdef, što je zabranjeno bez obzira na tekstni način prikazivanja — uključujući nevidljivi način korišten iza skeniranih slika. Kako preživjele Form objekte treba umotati pitanje je stabla strukture, obrađeno u članku o izgradnji tagged PDF strukture

Fontovi bez ikakvog FontDescriptora

Neugrađen font valjan je ulaz ove revizije, a ne stanje pogreške, i svaki pomoćnik ispod provjere ugrađivanja mora ga preživjeti. Kad PDFlibPas ne nađe /FontDescriptor, ili deskriptor bez FontFile, FontFile2 ili FontFile3, bilježi dijagnostiku 10020 — ili 10022 kad je ime jedno od Standard 14, koje §7.21.4 NOTE 5 oštroumno odbija izuzeti — i zatim nastavlja kroz ostatak datoteke. U tom je poanta izvještaja: autor želi svaki nalaz u jednom prolazu, a ne jedan nalaz po pokretanju. Zato referenca deskriptora predana pomoćnicima širine, cmap, CharSet i CIDSet može biti Nil, i svaki od njih to ispituje pri ulasku umjesto da pretpostavi da je ranija provjera prekinula reviziju. Ako je popravak ugraditi ono što nedostaje, mehanika je u napomeni o ugrađivanju nedostajućih fontova u postojeći PDF

Pokretanje revizije

Jedan poziv, nad datotekom koju niste nužno proizveli. TPDFlib.CheckFileCompliance uzima birač usklađenosti — 2 za PDF/UA-1 pod ISO 14289-1:2014 — i vraća nulu ili handle liste znakovnih nizova čije su stavke brojčani kod, dvotočka i čitljiva poruka. Nalazi o fontovima i tokovima sadržaja o kojima ovdje raspravljamo zauzimaju 10020 do 10041 u tom rasponu, brojčano odvojeni od 00xxx PDF/A kodova da miješani dnevnik ostane čitljiv. Prosljeđivanje 1 u Options kratko spoji na prvom nalazu, što želite u build kapiji, a ne u alatu za autorstvo. Za dokument još otvoren u memoriji, GetPDFUADiagnostics izvodi istovjetni pregled bez zaokruživanja preko diska

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 bira PDF/UA-1; Options = 0 izvještava 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 treba vanjsku verificirateljsku binarnu datoteku na mašini, a to je razlika između provjere koja izvodi na svakom buildu i provjere koja izvodi kad se netko sjeti. API-ji usklađenosti i dijagnostike opisani ovdje stižu u standardnoj PDFlibPas Delphi PDF Library, čija stranica proizvoda nosi potpunu tablicu dijagnostičkih kodova za PDF/UA-1 uz PDF/A, PDF/X i PDF/E testne skupove