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 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
// /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
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