Teknisk artikel

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

En rapport från veraPDF som säger att en glyfbredd inte stämmer med det inbäddade teckensnittsprogrammet berättar nästan ingenting om vilken glyf det gäller, eller varför. PDFlibPas besvarar frågan genom att slå upp varje teckenkod via den inbäddade cmap-en till ett glyfindex, normalisera programmets mått till 1000 enheter per em och jämföra där

Varför skiljer sig glyfbredder åt?

För att de två tal som jämförs lever i olika koordinatsystem, och inget i PDF-ordlistan talar om hur man omvandlar mellan dem. En teckensnittsordlista skriver /Widths i glyfrymd, som PDF fastställer till en tusendel av em (ISO 32000-1 §9.2.4). Tabellen hmtx inuti det inbäddade TrueType-programmet skriver advance-värden i teckensnittets designenheter, och tabellen head bestämmer hur många av dem som utgör ett em: 2048 för de flesta TrueType-snitt, 1000 för CFF-baserade, ibland något helt annat. Jämför man råvärdena ser varje 2048-upem-teckensnitt i ditt material trasigt ut. Det är den fälla ISO 14289-1 §7.21.5 ställer upp för alla som försöker granska bredder genom att läsa ordlistefält

PDFlibPas glyfbreddsgranskning i Delphi: en /Widths-post i glyfrymd och ett hmtx-advance i teckensnittets designenheter förs in i samma koordinatsystem genom att programmets mått skalas till 1000 enheter per em innan någon jämförelse görs
PDFlibPas skalar varje hmtx-advance till en tusendel av em innan det jämförs med ordlistebredden och rapporterar bara det som avviker med mer än en enhet

PDFlibPas normaliserar vid inläsning. TPDFTrueTypeParser lagrar Advance * 1000 div unitsPerEm i sin breddarray, så Parser.GetWidth(GID) svarar redan i samma tusendelar av em som PDF använder, och GetRawWidth finns kvar när du behöver designenheterna tillbaka. Den svårare hälften återstår dock: att ta sig från en teckenkod till ett glyfindex. För ett enkelt TrueType-teckensnitt beror vägen på Symbolic-flaggan i FontDescriptor, bit 3 av /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // Symboliska snitt adresseras direkt via programmets cmap,
    // med konventionen (3,0) högbyte som reservväg
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Icke-symboliskt: kod -> glyfnamn via teckentabellen, namn -> Unicode
    // via Adobe Glyph List, Unicode -> GID via programmets 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;

Två detaljer i det fragmentet har betydelse. Toleransen är en enhet, inte noll, eftersom normaliseringen är heltalsdivision och en korrekt producerad fil kan hamna en enhet fel; det är exakt den formulering om ”inom en tusendel av ett em” som diagnos 10036 rapporterar. Och skyddet GID < Parser.GlyphCount är ingen dekoration. GetWidth är skrivet för att vara fördragande mot anropare som ritar: ett index utanför intervallet kläms till sista posten i hmtx och 750 används när tabellen saknas. Eftergivenhet passar rendering men inte granskning, därför avvisar granskningen indexet innan någon bredd begärs i stället för att lita på klämningen

CIDFontType2 lägger till ännu ett indirekt steg

PDFlibPas går igenom sammansatta teckensnitt på samma sätt, med /CIDToGIDMap infogad mellan CID och glyfen. Bredderna kommer i arrayen /W, som ISO 32000-1 §9.7.4.3 ger två former som varvas fritt i en och samma array: en start-CID följd av en array med sammanhängande bredder, eller en första CID, en sista CID och en enda bredd som gäller för hela sekvensen. Granskningen tolkar båda formerna, skickar varje framkommet par till samma jämförelse och rapporterar summan under diagnos 10037. Mappningssteget är det som skiljer sammansatta teckensnitt åt, och det är därför diagnos 10021 om saknad mappning spelar roll innan du läser någon bredd alls — en saknad eller felaktig /CIDToGIDMap bryter inte bara mot §7.21.3.2, den gör breddfrågan obesvarbar

Tre vägar PDFlibPas tar från en teckenkod till ett glyfindex i Delphi: programmets cmap för symboliska TrueType-teckensnitt, en omväg via teckentabellen och Adobe Glyph List för icke-symboliska, samt ett steg med CMap plus /CIDToGIDMap för CIDFontType2
Breddjämförelsen kan inte börja förrän teckenkoden upplösts till ett glyfindex, och varje typ av teckensnitt når det indexet via en egen väg
// /CIDToGIDMap är namnet /Identity eller en ström av big-endian 16-bitars
// glyfindex, ett per CID (ISO 32000-1 avsnitt 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-strängar är 1-baserade
  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;

Vad ska en granskare göra när teckensnittsprogrammet inte går att avkoda?

Säg ingenting. De fullständighetskontroller av /CharSet och /CIDSet som ISO 14289-1 §7.21.4.2 kräver — diagnos 10038 och 10039 — är platsen där en alldeles ivrig validerare blir en belastning, för den som läser rapporten är ”din CharSet är ofullständig” omöjlig att skilja från ”vår Type 1-avkodare gav upp”. PDFlibPas rapporterar därför en saknad post bara när tre saker alla lyckas: teckensnittsprogrammet avkodas, mappningen från kod till glyf löses och mängden i sig avkodas. TPDFType1Decoder.LoadPFBFromString måste returnera True och ge ett antal charstrings innan något glyfnamn kontrolleras mot strängen /CharSet; /CIDSet-vägen kräver att strömmen expanderas och att glyfantalet blir positivt innan en enda bit testas. Undantag på vägen degenererar till ”ingen notering”, inte till ett fel

Den konservativa rapporteringsregeln i PDFlibPas PDF/UA-granskning: en saknad /CharSet- eller /CIDSet-post rapporteras bara när teckensnittsprogrammet avkodas, mappningen från kod till glyf löses och mängden i sig avkodas, och varje fel ger tystnad
Tre oberoende lyckade steg krävs innan en notering om saknad post släpps fram, så en avkodare som ger upp kostar dig en falsk negativ i stället för en falsk anklagelse

Det är en medveten förskjutning mot falska negativ, och den är värd att sägas rent ut i stället för att grävas ner. En skadad CFF-tabell, en Type 1-variant som inte stöds eller en /CIDSet kortare än glyfintervallet ger alla tystnad i stället för en diagnos. Resonemanget är att PDF/UA-granskningar vidarebefordras till författare som inte byggde verktygen, och en falsk anklagelse kostar mer än en missad: författaren bränner en dag på att bevisa att en konform fil är konform och slutar lita på hela rapporten. Matterhorn Protocol gör samma åtskillnad i en annan form när det skiljer kontroller en maskin kan avgöra från kontroller en människa måste, och dess kontrollpunkt Fonts (31) är där dessa hör hemma. Behöver du den strängare läsningen, kör PDFlibPas som snabb grind och en särskild validerare som andra åsikt — den kombinationen beskrivs också i genomgången om PDF/A- och PDF/UA-preflight

Sidans /Contents är en lista, inte en ström

Det enskilt dyraste misstaget vid granskning av innehållsströmmar är att behandla /Contents som en enda ström. ISO 32000-1 §7.7.3.3 låter en sida hålla en array av strömmar vars konkatenering, med blanktecken mellan delarna, är sidans program; producenter delar vid godtyckliga punkter, och en BT kan ligga i en medlem med sin motsvarande ET i nästa. En innehållsprocessor håller tillstånd — häckningsdjupet för markerat innehåll, teckensnittet som valdes av senaste Tf, flaggan för textobjekt — och Process nollställer det tillståndet vid inträde. Anropa den en gång per arraymedlem så börjar varje ström efter den första utan aktuellt teckensnitt, och text som var helt korrekt taggad läses som otaggat, teckensnittslöst brus. PDFlibPas konkaterar först och processar en gång

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;

// Ett Process-anrop över hela konkateneringen, aldrig ett anrop per medlem
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Vilka Form XObjects räknas egentligen som ostrukturerade?

Bara de som en sida verkligen anropar, från en anropspunkt utanför markerat innehåll och vars eget innehåll visar text. Diagnos 10040 driver ISO 14289-1 §7.20 genom att registrera tre oberoende fakta per objektnummer — har text, blev anropad, anropades inuti markerat innehåll — och rapporterar bara snittet av de två första minus den tredje. Varje genväg för sig är fel på ett sätt du skulle skeppa: att flagga varje textbärande Form i /Resources bestraffar ett mallbibliotek som ingen ritar från, och att flagga varje anropad Form bestraffar vektorlogotyper utan text som inte behöver taggas. Anropspunkten löses via objektnummer i stället för resursnamn, eftersom samma Form rutinmässigt nås under olika namn på olika sidor. Systerdiagnosen 10041 går igenom samma konkaterade program för §7.21.8, löser varje textvisande operand via teckensnittet i omfattning och räknar koderna som landar på .notdef, vilket är förbjudet oavsett textrenderingsläge — även det osynliga läge som används bakom skannade bilder. Hur de kvarvarande Formerna bör omslutas är en strukturträdsfråga, som tas upp i artikeln om att bygga taggad PDF-struktur

Teckensnitt helt utan FontDescriptor

Ett icke inbäddat teckensnitt är en legitim indata till den här granskningen, inte ett feltillstånd, och varje hjälpfunktion under inbäddningskontrollen måste klara det. När PDFlibPas inte hittar någon /FontDescriptor, eller en deskriptor utan FontFile, FontFile2 eller FontFile3, registrerar det diagnos 10020 — eller 10022 när namnet är ett av Standard 14, vilka §7.21.4 NOTE 5 punktligt vägrar undanta — och fortsätter sedan igenom resten av filen. Det är hela poängen med en rapport: en författare vill ha varje notering i ett svep, inte en notering per körning. Så referensen till deskriptorn som lämnas till hjälpfunktionerna för bredd, cmap, CharSet och CIDSet kan vara Nil, och var och en av dem testar det vid inträde i stället för att anta att en tidigare kontroll avbröt granskningen. Om åtgärden är att bädda in det som saknas finns mekaniken i noteringen om att bädda in saknade teckensnitt i en befintlig PDF

Köra granskningen

Ett anrop, på en fil du inte nödvändigtvis har producerat. TPDFlib.CheckFileCompliance tar en väljare för efterlevnadstest — 2 för PDF/UA-1 enligt ISO 14289-1:2014 — och returnerar antingen noll eller ett stränglistehandle vars poster är en numerisk kod, ett kolon och ett läsbart meddelande. Teckensnitts- och innehållsströmsnoteringarna som diskuteras här upptar 10020 till 10041 i det intervallet, hållna numeriskt åtskilda från 00xxx-koderna för PDF/A så att en blandad logg förblir läsbar. Att skicka 1 i Options ger kortslutning vid första noteringen, vilket är vad du vill ha i en bygggrind snarare än i ett författarverktyg. För ett dokument som fortfarande är öppet i minnet kör GetPDFUADiagnostics motsvarande granskning utan en omväg över disken

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 väljer PDF/UA-1; Options = 0 rapporterar varje notering
  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));   // t.ex. 10037 CIDFontType2 ...
  end;
end;

Inget av detta kräver någon extern validerarbinär på maskinen, och det är skillnaden mellan en kontroll som körs vid varje bygge och en kontroll som körs när någon kommer ihåg det. De efterlevnads- och diagnos-API:er som beskrivs här medföljer den vanliga PDFlibPas Delphi PDF Library, vars produktsida har hela tabellen med diagnostiska koder för PDF/UA-1 vid sidan av testsviterna för PDF/A, PDF/X och PDF/E