Tehnični članak

Pregled pisav PDF/UA v Delphiju: širine, CharSet, CIDSet

Poročilo veraPDF, ki pravi, da se širina glifa ne sklada z vgrajenim programom pisave, ne pove skoraj nič o tem, kateri glif je, niti zakaj. PDFlibPas to vprašanje odgovori tako, da vsako kodo znaka razreši prek vgrajenega cmap do indeksa glifa, metriko programa normalizira na 1000 enot na em in primerja tam

Zakaj se širine glifov ne ujemajo?

Ker živita primerjani številki v različnih koordinatnih sistemih, in nič v slovarju PDF vam ne pove pretvorbe. Slovar pisave zapiše /Widths v prostoru glifov, ki ga PDF fiksira na tisočinko em (ISO 32000-1 §9.2.4). Tabela hmtx znotraj vgrajenega programa TrueType zapiše zasuke v projektivnih enotah pisave, tabela head pa odloča, koliko teh naredi em: 2048 za večino oblik TrueType, 1000 za tiste iz CFF, občasno pa kaj popolnoma drugega. Primerjajte surovi vrednosti in vsaka pisava z 2048 upem v vašem korpusu izgleda pokvarjena. To je past, ki jo ISO 14289-1 §7.21.5 nastavi vsakomur, ki poskuša pregledati širine z branjem polj slovarja

Pregled širin glifov v PDFlibPas v Delphiju: vnos /Widths v prostoru glifov in zasuk hmtx v projektivnih enotah pisave sta pripeljana v isti koordinatni sistem tako, da se metrika programa skalira na 1000 enot na em, preden se sploh primerja
PDFlibPas vsak zasuk hmtx skalira na tisočinko em, preden ga primerja s širino slovarja, in poroča le o tem, kar je oddaljeno več kot ena enota

PDFlibPas normalizira ob nalaganju. TPDFTrueTypeParser shrani Advance * 1000 div unitsPerEm v svojo tabelo širin, zato Parser.GetWidth(GID) že odgovarja v istih tisočinkah em, ki jih uporablja PDF, GetRawWidth pa ostane na voljo, ko potrebujete projektivne enote nazaj. To še pusti težjo polovico: priti od kode znaka do indeksa glifa. Za preprosto pisavo TrueType pot je odvisna od zastavice Symbolic v FontDescriptor, bita 3 od /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // Simbolične oblike se naslavljajo naravnost skozi cmap programa,
    // s konvencijo (3,0) visokega bajta kot rezervo
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Nesimbolično: koda -> ime glifa prek kodiranja, ime -> Unicode
    // prek Adobe Glyph List, Unicode -> GID prek 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;

Dve podrobnosti v tem odlomku nosita težo. Toleranca je ena enota, ne nič, ker je normalizacija celoštevilčno deljenje in zakonito ustvarjena datoteka lahko zaide enoto zgrešena; to je natanko formulacija »znotraj ene tisočinke em«, ki jo poroča diagnostika 10036. Varnostni pogoj GID < Parser.GlyphCount pa ni okras. GetWidth je napisana odpuščajoče za klicatelje izrisa: indeks izven obsega prime na zadnji vnos v hmtx in se pri odsotni tabeli vrne na 750. Odpuščanje je prav za izris in narobe za pregled, zato pregled zavrne indeks, preden vpraša po širini, namesto da bi zaupal prijem

CIDFontType2 doda še eno posrednost

PDFlibPas prehodi sestavljene pisave na enak način, z /CIDToGIDMap vstavljenim med CID in glif. Širine prispejo v tabeli /W, ki ji ISO 32000-1 §9.7.4.3 daje dve obliki, ki se prosto izmenjujeta v eni tabeli: začetni CID, ki mu sledi tabela zaporednih širin, ali prvi CID, zadnji CID in ena sama širina, ki velja čez razpon. Pregled razčleni oboje, vsak nastali par pa da isti primerjavi in skupno poroča pod diagnostiko 10037. Korak preslikave je mesto, kjer se sestavljene pisave razlikujejo, in zato diagnostika 10021 o manjkajoči preslikavi šteje, preden berete katero koli širino — odsoten ali deformiran /CIDToGIDMap ne krši le §7.21.3.2, ampak vprašanje po širinah naredi neodgovorljivo

Tri poti, ki jih PDFlibPas v Delphiju vzame od kode znaka do indeksa glifa: cmap programa za simbolične pisave TrueType, zavoj prek kodiranja in Adobe Glyph List za nesimbolične ter korak CMap plus /CIDToGIDMap za CIDFontType2
Primerjava širin ne more začeti, dokler se koda znaka ne razreši do indeksa glifa, vsaka vrsta pisave pa do tega indeksa pride po drugi poti
// /CIDToGIDMap je ime /Identity ali tok velikokončnih 16-bitnih
// indeksov glifov, enega na CID (ISO 32000-1, razdelek 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;                       // nizi v Pascalu so 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;

Kaj naj preverjevalnik naredi, ko se program pisave ne dekodira?

Ne recite nič. Preverjanji popolnosti /CharSet in /CIDSet, ki ju zahteva ISO 14289-1 §7.21.4.2 — diagnostiki 10038 in 10039 — sta mesto, kjer preveč vnemevalni validator postane odgovornost, ker je poročilo »vaš CharSet je nepopoln« za bralca nerazločljivo od »naš dekodirnik Type 1 je obupal«. PDFlibPas zato poroča o manjkajočem vnosu le, ko uspejo vse tri stvari: program pisave se dekodira, preslikava od kode do glifa se razreši in sam nabor se dekodira. TPDFType1Decoder.LoadPFBFromString mora vrniti True in dati število charstringov, preden se katero koli ime glifa preveri proti nizu /CharSet; pot /CIDSet potrebuje, da se tok razširi in da število glifov pride pozitivno nazaj, preden se preizkusi en sam bit. Katera koli izjema na poti se zruši v »brez ugotovitve«, ne v napako

Previdno pravilo poročanja v pregledu PDF/UA v PDFlibPas: manjkajoči vnos /CharSet ali /CIDSet se poroča le, ko se program pisave dekodira, preslikava od kode do glifa se razreši in sam nabor se dekodira, vsaka odpoved pa da tišino
Trije neodvisni uspehi so potrebni, preden se izda ugotovitev o manjkajočem vnosu, zato dekodirnik, ki obupa, vas stane lažno negativno in ne lažno obtožbo

To je namerna pristranskost k lažno negativnim in vredno je povedati odkrito, ne pokopati. Pokvarjena tabela CFF, nepodprta različica Type 1 ali /CIDSet, krajši od obsega glifov, vsi dajo tišino namesto diagnostike. Razlog je, da se pregledi PDF/UA posredujejo avtorjem, ki orodja niso zgradili, in lažna obtožba stane več kot spregledana: avtor izgori dan z dokazovanjem, da je skladna datoteka skladna, in neha zaupati celotnemu poročilu. Protokol Matterhorn dela isto razliko v drugi obliki, ko loči preizkuse, ki jih lahko odloči stroj, od preizkusov, ki jih mora človek, njegova kontrolna točka Fonts (31) pa je mesto, kjer ti živijo. Če potrebujete strožje branje, pognajte PDFlibPas kot hitra vrata in namenski validator kot drugo mnenje — ta par je isti, kot ga opisuje vodnik po predletu PDF/A in PDF/UA

Stran /Contents je seznam, ne tok

Najdražja napaka pri pregledovanju tokov vsebine je obravnavati /Contents kot en tok. ISO 32000-1 §7.7.3.3 pusti strani držati tabelo tokov, katere stičenje, s presledkom med deli, je program strani; proizvajalci režejo na poljubnih mestih, BT pa lahko sedi v enem članu z njegovim ujemajočim ET v naslednjem. Procesor vsebine drži stanje — globino gnezdenja označene vsebine, pisavo, izbrano od zadnjega Tf, zastavico besedilnega objekta — Process pa to stanje ponastavi ob vstopu. Klicite enkrat na člana tabele in vsak tok po prvem začne brez trenutne pisave, tako da besedilo, ki je bilo odlično označeno, bere kot neoznačen, nepisaven šum. PDFlibPas najprej stiči in procesira enkrat

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;

// En klic Process čez celo stičenje, nikoli en klic na člana
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Kateri Form XObjects resnično štejejo kot nestrukturirani?

Le tiste, ki jih stran resnično pokliče, z mesta klica izven označene vsebine, in katerih lastna vsebina kaže besedilo. Diagnostika 10040 uveljavlja ISO 14289-1 §7.20 z zabeležbo treh neodvisnih dejstev na številko objekta — ima besedilo, je bil poklican, je bil poklican znotraj označene vsebine — in poroča le o preseku prvih dveh minus tretjega. Vsaka od obeh bližnjic je napačna na način, ki bi ga izdali: označevanje vsake Form z besedilom v /Resources kaznuje knjižnico predlog, iz katere nihče ne izrisuje, označevanje vsake poklicane Form pa kaznuje vektorske logotipe, ki ne nosijo besedila in ne potrebujejo označevanja. Mesto klica se razreši po številki objekta in ne po imenu vira, saj se ista Form rutinsko doseže pod različnimi imeni na različnih straneh. Spremljevalna diagnostika 10041 prehodi isti stičen program za §7.21.8, razreši vsak operand, ki kaže besedilo, skozi pisavo v obsegu, in šteje kode, ki pristanejo na .notdef, kar je prepovedano ne glede na način izrisa besedila — vključno z nevidnim načinom, uporabljenim za skeniranimi slikami. Kako naj se preživele Form ovijejo, je vprašanje drevesa strukture, pokrito v članku o gradnji označene strukture PDF

Pisave brez FontDescriptor sploh

Nevgrajena pisava je zakonit vhod v ta pregled, ne stanje napake, in vsak pomočnik pod preizkusom vgrajevanja ga mora preživeti. Ko PDFlibPas ne najde /FontDescriptor ali deskriptorja brez FontFile, FontFile2 ali FontFile3, zabeleži diagnostiko 10020 — ali 10022, kadar je ime eno od Standard 14, katerih §7.21.4 NOTE 5 izrazito ne oprosti — in nato nadaljuje čez preostanek datoteke. To je celotna poanta poročila: avtor želi vsako ugotovitev v enem prehodu, ne ene ugotovitve na zagon. Zato je lahko sklic na deskriptor, podan pomočnikom za širine, cmap, CharSet in CIDSet, Nil, vsak od njih pa ga preizkusi ob vstopu, namesto da bi privzel, da je zgodnejši preizkus prekinil pregled. Če je popravek vgrajevanje tistega, kar manjka, je mehanika v zapisu o vgrajevanju manjkajočih pisav v obstoječi PDF

Poganjanje pregleda

En klic, na datoteki, ki je niste nujno vi izdelali. TPDFlib.CheckFileCompliance vzame izbirnik preizkusa skladnosti — 2 za PDF/UA-1 pod ISO 14289-1:2014 — in vrne nič ali ročaj seznama nizov, katerega vnosi so številčna koda, dvopičje in berljivo sporočilo. Ugotovitve o pisavah in tokovih vsebine, obravnavane tukaj, zasedajo 10020 do 10041 v tem obsegu, številčno ločeno od kod 00xxx za PDF/A, da mešani dnevnik ostane berljiv. Podajanje 1 v Options skrajša pri prvi ugotovitvi, kar želite v gradbenih vratih in ne v avtorskem orodju. Za dokument, še odprt v pomnilniku, GetPDFUADiagnostics požene enakovreden pregled brez krožnega pota skozi disk

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 izbere PDF/UA-1; Options = 0 poroča vsako ugotovitev
  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č od tega ne potrebuje zunanje binarne validatorje na stroju, kar je razlika med preizkusom, ki teče na vsaki izgradnji, in preizkusom, ki teče, kadar se kdo spomni. API-ji skladnosti in diagnostike, opisani tukaj, pridejo v standardni PDFlibPas Delphi PDF Library, katerega produkcijska stran nosi celotno tabelo diagnostičnih kod za PDF/UA-1 ob testnih zbirkah PDF/A, PDF/X in PDF/E