Technisch artikel

PDF/UA-fontaudit in Delphi: widths, CharSet, CIDSet

Een veraPDF-rapport dat zegt dat een glyph-breedte niet overeenkomt met het ingesloten fontprogramma vertelt u bijna niets over welke glyph, of waarom. PDFlibPas beantwoordt die vraag door elke tekencode via de ingesloten cmap naar een glyphindex te herleiden, de metriek van het programma te normaliseren naar 1000 eenheden per em, en daar te vergelijken

Waarom komen glyph-breedten niet overeen?

Omdat de twee vergeleken getallen in verschillende coördinatensystemen leven, en niets in het PDF-woordenboek u de omrekening vertelt. Een fontwoordenboek schrijft /Widths in glyph space, die PDF vastlegt op één duizendste van de em (ISO 32000-1 §9.2.4). De hmtx-tabel in het ingesloten TrueType-programma schrijft advances in font design units, en de head-tabel beslist hoeveel daarvan een em vormen: 2048 voor de meeste TrueType-snitten, 1000 voor CFF-afgeleide, af en toe iets totaal anders. Vergelijk de ruwe waarden en elk 2048-upem-font in uw corpus ziet er kapot uit. Dat is de valkuil die ISO 14289-1 §7.21.5 zet voor iedereen die breedten wil auditen door woordenboekvelden te lezen

Glyph-breedte-audit van PDFlibPas in Delphi: een /Widths-invoer in glyph space en een hmtx-advance in font design units worden naar hetzelfde coördinatensysteem gebracht door de programmametriek te schalen naar 1000 eenheden per em voordat er überhaupt wordt vergeleken
PDFlibPas schaalt elke hmtx-advance naar een duizendste van de em voordat hij haar met de woordenboekbreedte vergelijkt, en meldt alleen wat meer dan één eenheid uiteenligt

PDFlibPas normaliseert bij het laden. TPDFTrueTypeParser bewaart Advance * 1000 div unitsPerEm in zijn breedte-array, dus Parser.GetWidth(GID) antwoordt al in dezelfde duizendsten van een em die de PDF gebruikt, en GetRawWidth blijft beschikbaar wanneer u design units terugnodigt. Dat laat nog de lastiger helft over: van een tekencode naar een glyphindex komen. Voor een simpel TrueType-font hangt de route af van de Symbolic-vlag in de FontDescriptor, bit 3 van /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // Symbolische snitten worden rechtstreeks via de cmap van het programma aangesproken,
    // met de (3,0)-high-byte-conventie als fallback
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Niet-symbolisch: code -> glyphnaam via de encoding, naam -> Unicode
    // via de Adobe Glyph List, Unicode -> GID via de cmap van het programma
    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;

Twee details in dat fragment wegen door. De tolerantie is één eenheid, niet nul, want normaliseren is integerdeling en een legitiem geproduceerd bestand kan er één eenheid naast zitten; dat is precies de formulering "binnen één duizendste van een em" die diagnostiek 10036 meldt. En de guard GID < Parser.GlyphCount is geen versiering. GetWidth is geschreven om coulant te zijn voor rendering-aanroepers: hij klempt een index buiten bereik vast op de laatste invoer van hmtx en valt terug op 750 als de tabel ontbreekt. Coucant is goed voor rendering en fout voor auditen, dus de audit verwerpt de index voordat ze om een breedte vraagt in plaats van op de klem te vertrouwen

CIDFontType2 voegt nog één indirectie toe

PDFlibPas loopt samengestelde fonts op dezelfde manier door, met /CIDToGIDMap tussen de CID en de glyph gevoegd. Breedten komen binnen in de /W-array, die ISO 32000-1 §9.7.4.3 twee vormen geeft die vrij door elkaar in één array voorkomen: een begin-CID gevolgd door een array van opeenvolgende breedten, of een eerste CID, een laatste CID, en één breedte die over de reeks geldt. De audit parst beide, geeft elk resulterend paar aan dezelfde vergelijking, en meldt het totaal onder diagnostiek 10037. De mappingstap is waar samengestelde fonts verschillen, en dat is waarom de ontbrekende-map-diagnostiek 10021 ertoe doet voordat u überhaupt een breedte leest — een afwezige of misvormde /CIDToGIDMap schendt niet alleen §7.21.3.2, ze maakt de breedtevraag onbeantwoordbaar

Drie routes die PDFlibPas in Delphi van een tekencode naar een glyphindex neemt: de cmap van het programma voor symbolische TrueType-fonts, een omweg via encoding en Adobe Glyph List voor niet-symbolische, en een CMap- plus /CIDToGIDMap-stap voor CIDFontType2
De breedtevergelijking kan pas beginnen als de tekencode naar een glyphindex is herleid, en elk soort font bereikt die index via een andere route
// /CIDToGIDMap is de naam /Identity of een stream van big-endian 16-bits
// glyphindices, één per CID (ISO 32000-1 sectie 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-strings zijn 1-gebaseerd
  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;

Wat doet een auditor als het fontprogramma niet decodeert?

Niets zeggen. De volledigheidstoetsen voor /CharSet en /CIDSet die ISO 14289-1 §7.21.4.2 vereist — diagnostiek 10038 en 10039 — zijn de plek waar een te ijverige validator een aansprakelijkheid wordt, want een melding "uw CharSet is incompleet" is voor de lezer niet te onderscheiden van "onze Type 1-decoder gaf het op". PDFlibPas meldt een ontbrekende invoer daarom alleen wanneer drie dingen allemaal slagen: het fontprogramma decodeert, de code-naar-glyph-mapping herleidt, en de set zelf decodeert. TPDFType1Decoder.LoadPFBFromString moet True teruggeven en een charstring-aantal opleveren voordat een glyphnaam tegen de /CharSet-string wordt getoetst; het /CIDSet-pad heeft nodig dat de stroom inflateert en het glyphaantal positief terugkomt voordat er één bit wordt getest. Elke exceptie onderweg klapt ineen tot "geen bevinding", niet tot een defect

De conservatieve meldingsregel in de PDF/UA-audit van PDFlibPas: een ontbrekende /CharSet- of /CIDSet-invoer wordt alleen gemeld wanneer het fontprogramma decodeert, de code-naar-glyph-mapping herleidt en de set zelf decodeert, en elke mislukking produceert stilte
Drie onafhankelijke successen zijn nodig voordat een ontbrekende-invoer-bevinding wordt uitgegeven, dus een decoder die het opgeeft kost u een fout negatief in plaats van een valse beschuldiging

Dat is een weloverwogen neiging naar fout negatieven, en die is het benoemen waard in plaats van hem te begraven. Een corrupte CFF-tabel, een niet-ondersteunde Type 1-variant of een /CIDSet korter dan het glyphbereik produceren allemaal stilte in plaats van een diagnostiek. De redenering is dat PDF/UA-audits worden doorgestuurd naar auteurs die de tooling niet bouwden, en een valse beschuldiging kost meer dan een gemiste: de auteur verbrandt een dag met bewijzen dat een conform bestand conform is, en stopt met het vertrouwen van het hele rapport. Het Matterhorn Protocol maakt hetzelfde onderscheid in een andere vorm wanneer het controles die een machine kan beslissen scheidt van controles die een mens moet, en zijn Fonts-checkpoint (31) is waar deze thuishoren. Wie de strengere lezing nodig heeft, draait PDFlibPas als snelle poort en een gespecialiseerde validator als second opinion — dat koppel is hetzelfde als beschreven in de PDF/A- en PDF/UA-preflight-walkthrough

Pagina-/Contents is een lijst, geen stream

De duurste enkele fout in contentstream-auditing is /Contents als één stream behandelen. ISO 32000-1 §7.7.3.3 laat een pagina een array van streams houden waarvan de aaneenschakeling, met witruimte tussen de delen, het paginaprogramma is; producenten splitsen op willekeurige punten, en een BT kan in het ene lid zitten met zijn passende ET in het volgende. Een contentprocessor houdt toestand vast — de nestdiepte van marked content, het door de laatste Tf gekozen font, de text-object-vlag — en Process reset die toestand bij binnenkomst. Roep hem één keer per arraylid aan en elke stream na de eerste begint zonder huidig font, dus tekst die voortreffelijk getagd was leest als ongetagde, fontloze ruis. PDFlibPas schakelt eerst samen en verwerkt één keer

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;

// Eén Process-aanroep over de hele aaneenschakeling, nooit één aanroep per lid
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Welke Form XObjects tellen werkelijk als ongestructureerd?

Alleen degene die een pagina werkelijk aanroept, vanaf een aanroepplaats buiten marked content, waarvan de eigen content tekst toont. Diagnostiek 10040 handhaaft ISO 14289-1 §7.20 door drie onafhankelijke feiten per objectnummer vast te leggen — heeft tekst, is aangeroepen, is aangeroepen binnen marked content — en meldt alleen de doorsnede van de eerste twee minus de derde. Elk van de twee snelkoppelingen is fout op een manier die u zou verschepen: elke tekstdragende Form in /Resources vinken straft een sjabloonlibrary af waar niemand uit put, en elke aangeroepen Form vinken straft vectorlogo's af die geen tekst dragen en geen tagging nodig hebben. De aanroepplaats wordt herleid op objectnummer in plaats van op resourcenaam, want dezelfde Form wordt routinematig via verschillende namen op verschillende pagina's bereikt. De companion-diagnostiek 10041 wandelt door hetzelfde samengevoegde programma voor §7.21.8, herleidt elke tekst-tonende operand via het font in scope en telt de codes die op .notdef uitkomen, wat verboden is ongeacht de text rendering mode — inclusief de onzichtbare modus die achter gescande afbeeldingen wordt gebruikt. Hoe de overlevende Forms ingepakt horen te worden is een structuurboomvraag, behandeld in het artikel over getagde PDF-structuur bouwen

Fonts zonder FontDescriptor in het geheel

Een niet ingesloten font is een legitieme input voor deze audit, geen fouttoestand, en elke helper onder de embeddingcontrole moet haar overleven. Wanneer PDFlibPas geen /FontDescriptor vindt, of een descriptor zonder FontFile, FontFile2 of FontFile3, legt hij diagnostiek 10020 vast — of 10022 als de naam er een van de Standard 14 is, die §7.21.4 NOTE 5 puntig weigert te vrijwaren — en gaat daarna gewoon verder met de rest van het bestand. Dat is de hele pointe van een rapport: een auteur wil elke bevinding in één doorgang, niet één bevinding per run. Dus de descripterverwijzing die aan de width-, cmap-, CharSet- en CIDSet-helpers wordt doorgegeven kan Nil zijn, en elk van hen toetst dat bij binnenkomst in plaats van te veronderstellen dat een eerdere controle de audit heeft afgebroken. Als de oplossing is om het ontbrekende in te sluiten, staan de mechanismen in de notitie over ontbrekende fonts in een bestaande PDF insluiten

De audit draaien

Eén aanroep, op een bestand dat u niet per se hebt geproduceerd. TPDFlib.CheckFileCompliance neemt een compliancetest-selector — 2 voor PDF/UA-1 onder ISO 14289-1:2014 — en geeft of nul terug of een string-list-handle waarvan de elementen een numerieke code, een dubbele punt en een leesbare melding zijn. De hier besproken font- en contentstream-bevindingen bezetten 10020 tot en met 10041 in dat bereik, numeriek gescheiden gehouden van de 00xxx PDF/A-codes zodat een gemengd log leesbaar blijft. Een 1 meegeven in Options kortsluit op de eerste bevinding, wat u in een buildpoort wilt in plaats van in een authoringtool. Voor een document dat nog in het geheugen openstaat, draait GetPDFUADiagnostics de equivalente inspectie zonder een omweg langs schijf

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 kiest PDF/UA-1; Options = 0 meldt elke bevinding
  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));   // bijv. 10037 CIDFontType2 ...
  end;
end;

Geen van dit alles heeft een externe validator-binary op de machine nodig, en dat is het verschil tussen een controle die bij elke build draait en een controle die draait wanneer iemand eraan denkt. De hier beschreven compliance- en diagnostiek-API's verschepen mee in de standaard PDFlibPas Delphi PDF Library, waarvan de productpagina de volledige diagnostiekcodetabel voor PDF/UA-1 voert naast de PDF/A-, PDF/X- en PDF/E-testsuites