Teknisk artikel

PDF/UA-fontrevision i Delphi: bredder, CharSet, CIDSet

En veraPDF-rapport, der siger, at en glyfbredde ikke stemmer med det indlejrede fontprogram, fortæller dig næsten intet om, hvilken glyf, eller hvorfor. PDFlibPas besvarer det spørgsmål ved at opløse hver tegnkode gennem den indlejrede cmap til et glyfindeks, normalisere programmetrikken til 1000 enheder pr. em og sammenligne dér

Hvorfor stemmer glyfbredder ikke?

Fordi de to tal, der sammenlignes, bor i forskellige koordinatsystemer, og intet i PDF-ordbogen fortæller dig konverteringen. En fontordbog skriver /Widths i glyfrum, som PDF fastlægger til en tusindedel af em (ISO 32000-1 §9.2.4). hmtx-tabellen inde i det indlejrede TrueType-program skriver advances i font design units, og head-tabellen afgør, hvor mange af dem der udgør et em: 2048 for de fleste TrueType-snit, 1000 for CFF-afledte, lejlighedsvis noget helt andet. Sammenlign råværdierne, og hver 2048-upem-font i dit korpus ser defekt ud. Det er den fælde, ISO 14289-1 §7.21.5 sætter for enhver, der forsøger at revidere bredder ved at læse ordbogsfelter

PDFlibPas-glyfbredderevision i Delphi: en /Widths-post i glyfrum og et hmtx-advance i font design units bringes ind i samme koordinatsystem ved at skalere programmetrikken til 1000 enheder pr. em, før nogen sammenligning foretages
PDFlibPas skalerer hvert hmtx-advance til en tusindedel af em, før det sammenlignes med ordbogsbredden, og rapporterer kun det, der ligger mere end én enhed fra

PDFlibPas normaliserer ved indlæsning. TPDFTrueTypeParser gemmer Advance * 1000 div unitsPerEm i sit breddearray, så Parser.GetWidth(GID) allerede svarer i de samme tusindedele af em, som PDF bruger, og GetRawWidth forbliver tilgængelig, når du behøver design units igen. Det efterlader stadig den sværere halvdel: at komme fra en tegnkode til et glyfindeks. For en simpel TrueType-font afhænger ruten af Symbolic-flagget i FontDescriptor, bit 3 af /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // Symboliske snit adresseres gennem program-cmap direkte,
    // med (3,0) high-byte-konventionen som fallback
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Ikke-symbolisk: kode -> glyfnavn via encodingen, navn -> Unicode
    // via Adobe Glyph List, Unicode -> GID via program-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;

To detaljer i det fragment vejer tungt. Tolerancen er én enhed, ikke nul, fordi normaliseringen er heltalsdivision, og en legitimt produceret fil kan lande én enhed ved siden af; det er præcis formuleringen "inden for en tusindedel af et em", diagnostic 10036 rapporterer. Og GID < Parser.GlyphCount-vagten er ikke pynt. GetWidth er skrevet til at være eftergivende over for renderingskaldere, klemmer et indeks uden for intervallet til den sidste post i hmtx og falder tilbage til 750, når tabellen mangler. Eftergivende er rigtigt for rendering og forkert for revision, så revisionen afviser indekset, før den beder om en bredde, i stedet for at stole på klemmen

CIDFontType2 tilføjer endnu en indirektion

PDFlibPas går gennem sammensatte fonte på samme måde, med /CIDToGIDMap indskudt mellem CID'en og glyfen. Bredder ankommer i /W-arrayet, som ISO 32000-1 §9.7.4.3 giver to former, der frit skiftes i ét array: en start-CID efterfulgt af et array af fortløbende bredder, eller en første CID, en sidste CID og én enkelt bredde anvendt på tværs af rækken. Revisionen parser begge og giver hvert resulterende par til samme sammenligning og rapporterer totalen under diagnostic 10037. Mappingtrinnet er, hvor sammensatte fonte adskiller sig, og det er, hvorfor diagnostic 10021 for manglende map betyder noget, før du overhovedet læser nogen bredde — en fraværende eller misdannet /CIDToGIDMap overtræder ikke blot §7.21.3.2, den gør bredde-spørgsmålet ubesvarbart

Tre ruter, PDFlibPas tager fra en tegnkode til et glyfindeks i Delphi: program-cmap for symboliske TrueType-fonte, en omvej via encoding og Adobe Glyph List for ikke-symboliske, og et CMap plus /CIDToGIDMap-trin for CIDFontType2
Bredde-sammenligningen kan ikke begynde, før tegnkoden er opløst til et glyfindeks, og hver slags font når det indeks ad en anden rute
// /CIDToGIDMap er navnet /Identity eller en stream af big-endian 16-bit
// glyfindekser, én pr. CID (ISO 32000-1 afsnit 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-strenge er 1-baserede
  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;

Hvad skal en revisor gøre, når fontprogrammet ikke kan dekodes?

Sig intet. /CharSet- og /CIDSet-kompletthedskontrollerne, som ISO 14289-1 §7.21.4.2 kræver — diagnostic 10038 og 10039 — er stedet, hvor en overivrig validator bliver til en plage, fordi en rapport om "dit CharSet er ufuldstændigt" for den læsende er uadskillelig fra "vores Type 1-dekoder gav op". PDFlibPas rapporterer derfor kun en manglende post, når tre ting alle lykkes: fontprogrammet dekodes, kode-til-glyf-mappingen opløses, og sættet selv dekodes. TPDFType1Decoder.LoadPFBFromString skal returnere True og give et charstring-antal, før noget glyfnavn tjekkes mod /CharSet-strengen; /CIDSet-stien behøver, at streamen inflater og glyfantallet kommer tilbage positivt, før én eneste bit testes. Enhver undtagelse undervejs kollapser til "intet fund", ikke til en defekt

Den konservative rapporteringsregel i PDFlibPas PDF/UA-revisionen: en manglende /CharSet- eller /CIDSet-post rapporteres kun, når fontprogrammet dekodes, kode-til-glyf-mappingen opløses og sættet selv dekodes, og enhver fejl producerer tavshed
Tre uafhængige succeser kræves, før et fund om manglende post udsendes, så en dekoder, der giver op, koster dig en falsk negativ snarere end en falsk anklage

Det er en bevidst skævhed mod falske negativer, og den er værd at sige rent ud i stedet for at begrave den. En korrupt CFF-tabel, en ikke-understøttet Type 1-variant eller en /CIDSet kortere end glyfintervallet producerer alle tavshed i stedet for en diagnostic. Ræsonnementet er, at PDF/UA-revisioner videresendes til forfattere, der ikke byggede værktøjet, og en falsk anklage koster mere end en overset: forfatteren brænder en dag af på at bevise, at en compliant fil er compliant, og holder op med at stole på hele rapporten. Matterhorn Protocol laver samme skelnen i en anden form, når den adskiller tjek, en maskine kan beslutte, fra tjek, et menneske må, og dets Fonts-checkpoint (31) er, hvor disse bor. Hvis du behøver den strengere læsning, så kør PDFlibPas som den hurtige port og en dedikeret validator som anden mening — det par er det samme, som beskrevet i PDF/A- og PDF/UA-preflight-gennemgangen

Sidens /Contents er en liste, ikke en stream

Den dyreste enkeltfejl i content-stream-revision er at behandle /Contents som én stream. ISO 32000-1 §7.7.3.3 lader en side holde et array af streams, hvis konkatenering, med whitespace mellem delene, er sideprogrammet; producenter deler ved vilkårlige punkter, og en BT kan sidde i ét medlem med sin matchende ET i det næste. En content-processor holder tilstand — marked-content-nestingsdybden, fonten valgt af den sidste Tf, text-object-flagget — og Process nulstiller den tilstand ved indgang. Kald den én gang pr. array-medlem, og hver stream efter den første starter uden aktuel font, så tekst, der var perfekt godt tagget, læses som utagget, fontløs støj. PDFlibPas konkatenerer først og processerer én gang

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;

// Ét Process-kald over hele konkateneringen, aldrig ét kald pr. medlem
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Hvilke Form XObjects tæller reelt som ustrukturerede?

Kun de, en side reelt kalder, fra et kaldsted uden for marked content, hvis eget indhold viser tekst. Diagnostic 10040 håndhæver ISO 14289-1 §7.20 ved at registrere tre uafhængige fakta pr. objektnummer — har tekst, blev kaldt, blev kaldt inde i marked content — og rapporterer kun skæringspunktet af de to første minus det tredje. Hver af de to genveje er forkert på en måde, du ville skibe: at flagge hver tekstbærende Form i /Resources straffer et skabelonbibliotek, ingen tegner fra, og at flagge hver kaldt Form straffer vektorlogoer, der bærer ingen tekst og behøver ingen tagning. Kaldstedet opløses efter objektnummer snarere end efter ressourcenavn, eftersom samme Form rutinemæssigt nås gennem forskellige navne på forskellige sider. Companion-diagnostic 10041 går gennem samme konkatenerede program for §7.21.8, opløser hver tekstvisende operand gennem fonten i scope og tæller koderne, der lander på .notdef, hvilket er forbudt uanset text rendering mode — inklusive den usynlige tilstand, der bruges bag scannede billeder. Hvordan de overlevende Forms skal pakkes, er et strukturetræ-spørgsmål, dækket i artiklen om at bygge tagget PDF-struktur

Fonte uden nogen FontDescriptor overhovedet

En ikke-indlejret font er legitimt input til denne revision, ikke en fejltilstand, og hver hjælper under indlejringstjekket skal overleve det. Når PDFlibPas ikke finder nogen /FontDescriptor, eller en deskriptor uden FontFile, FontFile2 eller FontFile3, registrerer den diagnostic 10020 — eller 10022, når navnet er et af Standard 14, som §7.21.4 NOTE 5 nægtspidst nægter at fritage — og fortsætter derefter gennem resten af filen. Det er hele pointen med en rapport: en forfatter vil have hvert fund i én gennemløbning, ikke ét fund pr. kørsel. Så deskriptorreferencen, der gives til width-, cmap-, CharSet- og CIDSet-hjælperne, kan være Nil, og hver af dem tester for det ved indgang i stedet for at antage, at et tidligere tjek afbrød revisionen. Hvis rettelsen er at indlejre det manglende, er mekanikken i noten om at indlejre manglende fonte i en eksisterende PDF

Kørsel af revisionen

Ét kald, på en fil, du ikke nødvendigvis har produceret. TPDFlib.CheckFileCompliance tager en compliance-testvælger — 2 for PDF/UA-1 under ISO 14289-1:2014 — og returnerer enten nul eller et string-list-handle, hvis poster er en numerisk kode, et kolon og en læsbar besked. Font- og content-stream-fundene omtalt her beboer 10020 til 10041 i det interval, holdt numerisk adskilt fra 00xxx PDF/A-koderne, så en blandet log forbliver læsbar. At give 1 i Options short-circuiter ved det første fund, hvilket er, hvad du vil have i en build-port snarere end i et authoring-værktøj. For et dokument stadig åbent i hukommelsen kører GetPDFUADiagnostics den tilsvarende inspektion uden en omvej gennem disk

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 vælger PDF/UA-1; Options = 0 rapporterer hvert fund
  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));   // fx 10037 CIDFontType2 ...
  end;
end;

Intet af dette behøver en ekstern validator-binær på maskinen, hvilket er forskellen mellem et tjek, der kører ved hver build, og et tjek, der kører, når nogen kommer i hu. Compliance- og diagnostic-API'erne omtalt her leveres i standard-PDFlibPas Delphi PDF Library, hvis produktside bærer den fulde diagnostiske kodetabel for PDF/UA-1 ved siden af PDF/A-, PDF/X- og PDF/E-testsuiterne