Raport veraPDF mówiący, że szerokość glifu nie zgadza się z osadzonym programem fontu, mówi ci prawie nic o tym, który glif, ani dlaczego. PDFlibPas odpowiada na to pytanie, rozwiązując każdy kod znaku przez osadzony cmap do indeksu glifu, normalizując metrykę programu do 1000 jednostek na em i porównując tam
Dlaczego szerokości glifów się nie zgadzają?
Bo dwie porównywane liczby żyją w różnych układach współrzędnych, a nic w słowniku PDF nie mówi ci, jak je przeliczyć. Słownik fontu zapisuje /Widths w przestrzeni glifów, którą PDF fixuje na jedną tysięczną em (ISO 32000-1 §9.2.4). Tabela hmtx wewnątrz osadzonego programu TrueType zapisuje advance'y w jednostkach projektowych fontu, a tabela head rozstrzyga, ile ich składa się na em: 2048 dla większości krojów TrueType, 1000 dla pochodnych od CFF, czasem coś zupełnie innego. Porównaj surowe wartości, a każdy font 2048-upem w twoim korpusie wygląda na zepsuty. To pułapka, którą ISO 14289-1 §7.21.5 stawia każdemu, kto próbuje audytować szerokości przez czytanie pól słownika
PDFlibPas normalizuje przy wczytaniu. TPDFTrueTypeParser zapisuje Advance * 1000 div unitsPerEm w swojej tablicy szerokości, więc Parser.GetWidth(GID) od razu odpowiada w tych samych tysięcznych em, którymi posługuje się PDF, a GetRawWidth zostaje dostępne, gdy potrzebujesz z powrotem jednostek projektowych. Pozostaje trudniejsza połowa: dostanie się od kodu znaku do indeksu glifu. Dla prostego fontu TrueType trasa zależy od flagi Symbolic w FontDescriptor, bitu 3 /Flags
Parser := TPDFTrueTypeParser.Create;
try
Parser.LoadFromString(FontProgram);
if Symbolic then
begin
// Fonty symboliczne są adresowane bezpośrednio przez cmap programu,
// z konwencją (3,0) wysokiego bajta jako fallbackiem
GID := Parser.GetGlyphIndex(Code);
if GID = 0 then
GID := Parser.GetGlyphIndex($F000 + Code);
end
else
begin
// Niesymboliczne: kod -> nazwa glifu przez encoding, nazwa -> Unicode
// przez Adobe Glyph List, Unicode -> GID przez cmap programu
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;
Dwa detale w tym fragmencie mają wagę. Tolerancja to jedna jednostka, nie zero, bo normalizacja to dzielenie całkowite i legalnie wyprodukowany plik może wylądować o jednostkę obok; to dokładnie sformułowanie „w granicach jednej tysięcznej em", które raportuje diagnostyka 10036. A strażnik GID < Parser.GlyphCount nie jest dekoracją. GetWidth jest napisane wyrozumiale dla wywołujących renderujących: przycina indeks poza zakresem do ostatniego wpisu w hmtx i cofa się do 750, gdy tabeli brak. Wyrozumiałość jest dobra dla renderingu i zła dla audytu, więc audyt odrzuca indeks przed pytaniem o szerokość, zamiast ufać przycinaniu
CIDFontType2 dodaje jeszcze jedno pośrednictwo
PDFlibPas przechodzi fonty złożone tak samo, z /CIDToGIDMap wstawionym między CID a glif. Szerokości przychodzą w tablicy /W, którą ISO 32000-1 §9.7.4.3 obdarza dwoma kształtami swobodnie przeplatanymi w jednej tablicy: CID startowy, po którym idzie tablica kolejnych szerokości, albo pierwszy CID, ostatni CID i jedna szerokość zastosowana do całego biegu. Audyt parsuje oba, potem podaje każdą powstałą parę temu samemu porównaniu i raportuje sumę pod diagnostyką 10037. Krok mapowania to miejsce, w którym fonty złożone się różnią, i dlatego diagnostyka 10021 o brakującej mapie ma znaczenie, zanim w ogóle przeczytasz jakąkolwiek szerokość — nieobecna albo zniekształcona /CIDToGIDMap nie tylko łamie §7.21.3.2, ona czyni pytanie o szerokość bez odpowiedzi
// /CIDToGIDMap to nazwa /Identity albo strumień 16-bitowych indeksów glifów
// big-endian, po jednym na CID (ISO 32000-1 sekcja 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; // łańcuchy Pascal są indeksowane od 1
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;
Co powinien zrobić audytor, gdy program fontu nie chce się dekodować?
Nic nie mówić. Kontrole kompletności /CharSet i /CIDSet, których wymaga ISO 14289-1 §7.21.4.2 — diagnostyki 10038 i 10039 — to miejsce, w którym zbyt gorliwy walidator staje się obciążeniem, bo raport „twój CharSet jest niekompletny" jest dla czytającego nieodróżnialny od „nasz dekoder Type 1 się poddał". PDFlibPas raportuje więc brakujący wpis tylko, gdy trzy rzeczy się powiodą: program fontu się dekoduje, mapowanie kodu na glif się rozwiązuje i sam zbiór się dekoduje. TPDFType1Decoder.LoadPFBFromString musi zwrócić True i dać liczbę charstringów, zanim jakakolwiek nazwa glifu zostanie sprawdzona względem łańcucha /CharSet; ścieżka /CIDSet potrzebuje, by strumień się rozpakował, a licznik glifów wrócił dodatni, zanim pojedynczy bit zostanie przetestowany. Jakikolwiek wyjątek po drodze zapada się w „brak ustalenia", nie w defekt
To celowe wychylenie w stronę fałszywych negatywów i warto powiedzieć to wprost, zamiast zakopywać. Uszkodzona tabela CFF, niewspierany wariant Type 1 albo /CIDSet krótszy niż zakres glifów wszystkie dają ciszę zamiast diagnostyki. Uzasadnienie jest takie, że audyty PDF/UA trafiają dalej do autorów, którzy nie budowali narzędzi, a fałszywe oskarżenie kosztuje więcej niż przeoczone: autor spala dzień na dowodzeniu, że zgodny plik jest zgodny, i przestaje ufać całemu raportowi. Matterhorn Protocol robi to samo rozróżnienie w innej formie, gdy oddziela kontrole, które maszyna może rozstrzygnąć, od kontroli, które musi człowiek, a jego punkt kontrolny Fonts (31) to miejsce, gdzie te mieszkają. Jeśli potrzebujesz ostrzejszego odczytania, użyj PDFlibPas jako szybkiej bramy i dedykowanego walidatora jako drugiej opinii — to ta sama para, którą opisuje przebieg preflightu PDF/A i PDF/UA
/Contents strony to lista, nie strumień
Najdroższy pojedynczy błąd w audycie strumieni treści to traktowanie /Contents jako jednego strumienia. ISO 32000-1 §7.7.3.3 pozwala stronie trzymać tablicę strumieni, których konkatenacja, z białymi znakami między częściami, jest programem strony; producenci tną w dowolnych miejscach, a BT może siedzieć w jednym członie z pasującym ET w następnym. Procesor treści trzyma stan — głębokość zagnieżdżenia marked-content, font wybrany przez ostatnie Tf, flagę obiektu tekstowego — a Process resetuje ten stan przy wejściu. Wywołaj go raz na człon tablicy, a każdy strumień po pierwszym zacznie się bez bieżącego fontu, więc tekst doskonale otagowany czyta się jako szum nieotagowany i bez fontu. PDFlibPas najpierw skleja, potem przetwarza 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 wywołanie Process na całą konkatenację, nigdy jedno na człon
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));
Które Form XObjects faktycznie liczą się jako nieustrukturyzowane?
Tylko te, które strona faktycznie wywołuje, z miejsca wywołania poza marked content, i których własna treść pokazuje tekst. Diagnostyka 10040 egzekwuje ISO 14289-1 §7.20, zapisując trzy niezależne fakty na numer obiektu — ma tekst, był wywołany, był wywołany wewnątrz marked content — i raportując tylko przecięcie dwóch pierwszych minus trzeci. Każdy z dwóch skrótów jest zły w sposób, który byś wysłał: flagowanie każdego Form z tekstem w /Resources karze bibliotekę szablonów, z której nikt nie czerpie, a flagowanie każdego wywołanego Form karze logo wektorowe, które nie niesie tekstu i nie potrzebuje tagowania. Miejsce wywołania jest rozwiązywane po numerze obiektu, nie po nazwie zasobu, bo ten sam Form jest rutynowo osiągany pod różnymi nazwami na różnych stronach. Towarzysząca diagnostyka 10041 przechodzi ten sam sklejony program dla §7.21.8, rozwiązując każdy operand pokazujący tekst przez font w zasięgu i licząc kody, które lądują na .notdef, co jest zabronione niezależnie od trybu renderowania tekstu — włącznie z trybem niewidzialnym używanym za skanowanymi obrazami. Jak owinąć przeżywsze Formy, to pytanie o drzewo struktury, pokryte w artykule o budowaniu struktury oznaczonego PDF
Fonty bez żadnego FontDescriptor
Nieosadzony font to legalne wejście do tego audytu, nie stan błędu, i każdy pomocnik poniżej kontroli osadzenia musi go przetrwać. Gdy PDFlibPas nie znajduje /FontDescriptor albo deskryptora bez FontFile, FontFile2 czy FontFile3, zapisuje diagnostykę 10020 — albo 10022, gdy nazwa to jeden ze Standard 14, których §7.21.4 NOTE 5 wymownie odmawia zwolnienia — i potem idzie dalej przez resztę pliku. W tym cały sens raportu: autor chce wszystkie ustalenia w jednym przebiegu, nie jedno ustalenie na uruchomienie. Referencja deskryptora podawana pomocnikom szerokości, cmap, CharSet i CIDSet może więc być Nil, a każdy z nich testuje to na wejściu, zamiast zakładać, że wcześniejsza kontrola przerwała audyt. Jeśli poprawką jest osadzenie tego, czego brakuje, mechanika jest w nocie o osadzaniu brakujących fontów w istniejącym PDF
Uruchomienie audytu
Jedno wywołanie, na pliku, którego niekoniecznie sam wyprodukowałeś. TPDFlib.CheckFileCompliance przyjmuje selektor testu zgodności — 2 dla PDF/UA-1 pod ISO 14289-1:2014 — i zwraca zero albo uchwyt listy łańcuchów, którego wpisy to kod numeryczny, dwukropek i czytelny komunikat. Ustalenia dotyczące fontów i strumieni treści omawiane tutaj zajmują 10020 do 10041 w tym zakresie, trzymane liczbowo osobno od kodów 00xxx PDF/A, żeby mieszany log pozostawał czytelny. Przekazanie 1 w Options ucina na pierwszym ustaleniu, czego chcesz w bramie builda, a nie w narzędziu autorskim. Dla dokumentu wciąż otwartego w pamięci GetPDFUADiagnostics robi równoważną inspekcję bez objazdu przez dysk
var
Issues, Count, I: Integer;
begin
// ComplianceTest = 2 wybiera PDF/UA-1; Options = 0 raportuje każdy finding
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)); // np. 10037 CIDFontType2 ...
end;
end;
Żadne z tego nie potrzebuje zewnętrznego binarium walidatora na maszynie, co jest różnicą między kontrolą, która działa przy każdym buildzie, a kontrolą, która działa, gdy ktoś sobie przypomni. API zgodności i diagnostyki opisane tutaj są w standardowej PDFlibPas Delphi PDF Library, której strona produktowa niesie pełną tabelę kodów diagnostycznych dla PDF/UA-1 obok suit testowych PDF/A, PDF/X i PDF/E