Odborný článok

PDF/UA audit fontov v Delphi: šírky, CharSet, CIDSet

VeraPDF správa hovoriaca, že šírka glyfu nesúhlasí s vloženým fontovým programom, vám nepovie takmer nič o tom, ktorý glyf to je, alebo prečo. PDFlibPas odpovedá na túto otázku tak, že vyrieši každý kód znaku cez vložený cmap na index glyfu, normalizuje programovú metriku na 1000 jednotiek na em a porovnáva tam

Prečo sa šírky glyfov nezhodujú?

Pretože dve porovnávané čísla žijú v rôznych súradnicových systémoch a nič v PDF slovníku vám nepovie konverziu. Fontový slovník zapisuje /Widths v glyfovom priestore, ktorý PDF fixuje na jednu tisícinu em (ISO 32000-1 §9.2.4). Tabuľka hmtx vo vnútri vloženého TrueType programu zapisuje advance v dizajnových jednotkách fontu a tabuľka head rozhoduje, koľko z nich tvorí em: 2048 pre väčšinu TrueType rezov, 1000 pre CFF odvodené, občas niečo úplne iné. Porovnajte surové hodnoty a každý 2048-upem font vo vašom korpuse vyzerá rozbitý. To je pasca, ktorú ISO 14289-1 §7.21.5 kladie každému, kto sa pokúsi auditovať šírky čítaním polí slovníka

Audit šírky glyfov PDFlibPas v Delphi: položka /Widths v glyfovom priestore a advance z hmtx v dizajnových jednotkách fontu sa privedú do rovnakého súradnicového systému skalovaním programovej metriky na 1000 jednotiek na em skôr, než sa urobí akékoľvek porovnanie
PDFlibPas škáluje každý advance z hmtx na tisícinu em pred porovnaním so slovníkovou šírkou a hlási len to, čo je viac než o jednu jednotku vedľa

PDFlibPas normalizuje pri načítaní. TPDFTrueTypeParser ukladá Advance * 1000 div unitsPerEm do svojho poľa šírok, takže Parser.GetWidth(GID) už odpovedá v tých istých tisícinách em, ktoré PDF používa, a GetRawWidth zostáva dostupný, keď potrebujete dizajnové jednotky naspäť. Tým ostáva tažšia polovica: dostať sa od kódu znaku k indexu glyfu. Pre jednoduchý TrueType font závisí cesta od vlajky Symbolic vo FontDeskriptore, bit 3 z /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // Symbolické rezy sa adresujú priamo cez programový cmap,
    // s konvenciou (3,0) high-byte ako zálohou
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Nesymbolické: kód -> názov glyfu cez kódovanie, názov -> Unicode
    // cez Adobe Glyph List, Unicode -> GID cez programový 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 detaily v tomto fragmente nesú váhu. Tolerancia je jedna jednotka, nie nula, pretože normalizácia je celočíselné delenie a korektne vyrobený súbor môže pristáť o jednotku vedľa; to je presne formulácia „v rámci jednej tisíciny em", ktorú hlási diagnostika 10036. A strážca GID < Parser.GlyphCount nie je dekorácia. GetWidth je napísaná zhovievavo pre renderovacích volajúcich, svorkuje index mimo rozsahu na poslednú položku v hmtx a pada späť na 750, keď tabuľka chýba. Zhovievavosť je správna pre renderovanie a zlá pre audit, takže audit odmieta index predtým, než si vyžiada šírku, namiesto dôvery vo svorku

CIDFontType2 pridáva ešte jednu smerovanie

PDFlibPas prechádza kompozitné fonty rovnakým spôsobom, s /CIDToGIDMap vloženou medzi CID a glyf. Šírky prichádzajú v poli /W, ktoré ISO 32000-1 §9.7.4.3 dáva v dvoch tvaroch voľne sa striedajúcich v jednom poli: počiatočný CID nasledovaný poľom po sebe idúcich šírok, alebo prvý CID, posledný CID a jedna šírka aplikovaná na celý beh. Audit parsuje oba a potom odovzdáva každý výsledný pár tomu istému porovnaniu a hlási súčet pod diagnostikou 10037. Krok mapovania je miesto, kde sa kompozitné fonty líšia, a je dôvod, prečo diagnostika chýbajúcej mapy 10021 záleží skôr, než si prečítate akúkoľvek šírku — absentujúci alebo poškodený /CIDToGIDMap nelen porušuje §7.21.3.2, robí otázku šírky neodpovedateľnou

Tri cesty, ktorými PDFlibPas vedie od kódu znaku k indexu glyfu v Delphi: programový cmap pre symbolické TrueType fonty, objazd cez kódovanie a Adobe Glyph List pre nesymbolické a krok CMap plus /CIDToGIDMap pre CIDFontType2
Porovnanie šírok nemôže začať, kým sa kód znaku nerozlíši na index glyfu, a každý druh fontu sa k tomu indexu dostáva inou cestou
// /CIDToGIDMap je názov /Identity alebo stream big-endian 16-bitových
// indexov glyfov, jeden na CID (ISO 32000-1 kapitola 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 reťazce sú 1-based
  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;

Čo má audítor robiť, keď sa fontový program nedekóduje?

Nič nepovedať. Kompletnosťné kontroly /CharSet a /CIDSet, ktoré ISO 14289-1 §7.21.4.2 vyžaduje — diagnostiky 10038 a 10039 — sú miesto, kde sa nadšený validátor mení na záťaž, pretože hlásenie „váš CharSet je nekompletný" je pre človeka, ktorý ho číta, neodlíšiteľné od „náš dekodér Type 1 vzdal to". PDFlibPas preto hlási chýbajúcu položku len keď sa podarí tri veci naraz: fontový program sa dekóduje, mapovanie kódu na glyf sa rozlíši a samotná sada sa dekóduje. TPDFType1Decoder.LoadPFBFromString musí vrátiť True a dať počet charstringov skôr, než sa akýkoľvek názov glyfu skontroluje proti reťazcu /CharSet; cesta /CIDSet potrebuje, aby sa stream rozbalil a počet glyfov vrátil pozitívny skôr, než sa testuje jediný bit. Akákoľvek výnimka na ceste sa zrúti na „žiadny nález", nie na defekt

Konzervatívne pravidlo hlásenia v PDF/UA audite PDFlibPas: chýbajúca položka /CharSet alebo /CIDSet sa hlási len keď sa fontový program dekóduje, mapovanie kódu na glyf sa rozlíši a samotná sada sa dekóduje, a akékoľvek zlyhanie produkuje ticho
Tri nezávislé úspechy sa vyžadujú pred emitovaním nálezu chýbajúcej položky, takže dekodér, ktorý vzdal to, vás stojí falošný negatív namiesto falošného obvinenia

To je zámerná zaujatosť k falošným negatívom a stojí za to to povedať priamo, nie pochovať. Poškodená tabuľka CFF, nepodporovaná varianta Type 1 alebo /CIDSet kratšia než rozsah glyfov všetky produkujú ticho namiesto diagnostiky. Uvažovanie je, že audity PDF/UA sa preposielajú autorom, ktorí nástroj nestavili, a falošné obvinenie stojí viac než premeškané: autor spáli deň dokazovaním, že kompatibilný súbor je kompatibilný, a prestane dôverovať celej správe. Matterhorn Protocol robí to isté rozlíšenie v inej forme, keď oddeľuje kontroly, ktoré môže stroj rozhodnúť, od kontrol, ktoré musí človek, a jeho checkpoint Fonts (31) je miesto, kde tieto žijú. Ak potrebujete prísnejšie čítanie, spustite PDFlibPas ako rýchlu bránu a dedikovaný validátor ako druhý názor — toto párovanie je to isté, ktoré popisuje prechádzka preflight PDF/A a PDF/UA

Page /Contents je zoznam, nie stream

Najdrahšia chyba pri audite obsahových streamov je považovať /Contents za jeden stream. ISO 32000-1 §7.7.3.3 dovoľuje strane držať pole streamov, ktorých konatenácia s bielym miestom medzi časťami je program strany; producenti režú na ľubovoľných miestach a BT môže sedieť v jednom člene so svojím zodpovedajúcim ET v nasledujúcom. Obsahový procesor drží stav — hĺbku zanorenia marked-content, font vybraný posledným Tf, vlajku textového objektu — a Process resetuje tento stav na vstupe. Zavolajte ho raz na člena poľa a každý stream po prvom začína bez aktuálneho fontu, takže text, ktorý bol dokonale označkovaný, sa číta ako neoznačkovaný, nefontovaný šum. PDFlibPas najprv skonatenuje a procesuje raz

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;

// Jedno volanie Process nad celou konatenáciou, nikdy jedno na člena
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Ktoré Form XObjects naozaj počítajú ako nestruktúrované?

Len tie, ktoré strana skutočne vyvolá, z miesta volania mimo marked content, ktorých vlastný obsah ukazuje text. Diagnostika 10040 vynucuje ISO 14289-1 §7.20 záznamom troch nezávislých skutočností na číslo objektu — má text, bol vyvolaný, bol vyvolaný vo vnútri marked content — a hlási len prienik prvých dvoch mínus tretí. Každá z oboch skratiek je zlá spôsobom, ktorý by ste odoslali: označkovanie každého textového Formu v /Resources trestá knižnicu šablón, z ktorej nikto nekreslí, a označkovanie každého vyvolaného Formu trestá vektorové logá, ktoré nemajú text a nepotrebujú označkovanie. Miesto vyvolania sa rozlíši číslom objektu namiesto názvu zdroja, keďže ten istý Form sa bežne dosahuje pod rôznymi názvami na rôznych stranách. Sprievodná diagnostika 10041 prechádza ten istý skonatený program pre §7.21.8, rozlíši každý operand ukazujúci text cez font v rozsahu a počíta kódy, ktoré pristanú na .notdef, čo je zakázané bez ohľadu na textový renderovací režim — vrátane neviditeľného režimu použitého za naskenovanými obrázkami. Ako by mali preživšie Formy byť zabalené, je otázka stromu štruktúry, pokrytá v článku o stavbe tagged PDF štruktúry

Fonty bez FontDescriptor vôbec

Nevložený font je legitimný vstup do tohto auditu, nie chybový stav, a každý pomocník pod kontrolou vkladania musí prežiť. Keď PDFlibPas nájde žiadny /FontDescriptor alebo deskriptor bez FontFile, FontFile2 alebo FontFile3, zaznamená diagnostiku 10020 — alebo 10022, keď je názov jeden zo Štandardných 14, ktoré §7.21.4 NOTE 5 cieľavedome odmieta vyňať — a potom pokračuje cez zvyšok súboru. To je celý zmysel správy: autor chce každý nález v jednom prechode, nie jeden nález na beh. Takže referencia deskriptora odovzdaná pomocníkom šírok, cmap, CharSet a CIDSet môže byť Nil a každý z nich to testuje na vstupe namiesto predpokladu, že skoršia kontrola prerušila audit. Ak je opravou vložiť to, čo chýba, mechanika je v poznámke o vkladaní chýbajúcich fontov do existujúceho PDF

Spustenie auditu

Jedno volanie, na súbore, ktorý ste nemuseli vy produkovať. TPDFlib.CheckFileCompliance berie selektor compliance testu — 2 pre PDF/UA-1 pod ISO 14289-1:2014 — a vracia buď nulu, alebo handle zoznamu reťazcov, ktorého položky sú číselný kód, dvojbodka a čitateľná správa. Nálezy fontov a obsahových streamov diskutované tu zaberajú 10020 až 10041 v tom rozsahu, numericky oddelené od kódov PDF/A 00xxx, takže zmiešaný log ostáva čitateľný. Posunutie 1 v Options skracuje na prvom náleze, čo je to, čo chcete v build bráne, nie v autorovskom nástroji. Pre dokument stále otvorený v pamäti spustí GetPDFUADiagnostics ekvivalentnú inšpekciu bez okruhu cez disk

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 vyberá PDF/UA-1; Options = 0 hlási každý nález
  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));   // napr. 10037 CIDFontType2 ...
  end;
end;

Nič z tohto nepotrebuje externý validátorový binárka na stroji, čo je rozdiel medzi kontrolou, ktorá beží pri každom buildu, a kontrolou, ktorá beží, keď si niekto spomenie. Compliance a diagnostické API popísané tu sa dodávajú v štandardnej PDFlibPas Delphi PDF Library, ktorej produktová stránka nesie kompletnú tabuľku diagnostických kódov pre PDF/UA-1 vedľa testových sád PDF/A, PDF/X a PDF/E