PDFlibPas (PDF Library for Delphi) sprawdza każdy obiekt względem tabeli reguł wersji PDF, zanim zapisze plik, i do niedawna ten preflight wersji PDF brał zwykłe słowniki pomiarowe CAD za geoprzestrzenne. Jednostronicowy rysunek CAD wczytywał się bez zarzutu, po czym SaveToFile zwracał 0 z LastErrorCode 602 i żądał 1.7 ExtensionLevel 3. Poprawione reguły traktują prostokątne słowniki /Measure (/Subtype /RL) jako zwykłe PDF 1.6 i rezerwują bramkę rozszerzenia dla prawdziwych znaczników geoprzestrzennych
Plik przyszedł przez przyjęcie do korpusu: jedna strona, jedna grupa optional content, dwa prostokątne viewporty pomiarowe, czyli ten rodzaj wyjścia, jaki pisze pakiet CAD architektonicznego, żeby przeglądarka mogła odczytać odległości z rzutu. Nic w nim nie było egzotyczne i właśnie dlatego odmowa bolała. Preflight, który blokuje poprawny plik, jest gorszy od wolnego, bo wołający dostaje diagnozę wyglądającą autorytatywnie i wskazującą na cechę, której dokument nie zawiera. Poprawka miała dwie części: odczyt specyfikacji stojący za jedną regułą i uświadomienie sobie, że reguła nie potrafi odróżnić dwóch typów słowników na poziomie, na jakim patrzyła
Jak działa preflight wersji PDF przy zapisie w PDFlibPas?
Bramka zapisu, PrepareAndCheckSaveVersion, porównuje każdy obiekt pośredni z PDFFeatureRules i pada na pierwszej regule, która zarazem pasuje i wymaga więcej, niż cel pozwala. Celem jest wersja dokumentu (albo wersja przypięta przez LockSaveVersion) plus poziom rozszerzenia Adobe zadeklarowany pod /Extensions /ADBE. Każdy rekord TPDFFeatureRule niesie MinVersion, MinExtensionLevel, MatchKind taki jak fmkDictKey albo fmkDictSubtype, łańcuch Match, czytelny dla człowieka Feature i opcjonalny callback. AddRule rejestruje zwykłą regułę wersji; AddExtensionRule zawsze przypina MinVersion na 17 i dokłada poziom rozszerzenia, więc reguła rozszerzenia może być zaspokojona wyłącznie przez PDF 1.7 plus właściwy wpis /Extensions. Gdy bramka zadziała, wymagana wersja i nazwa cechy zostają dla wołającego, a klucze GetInformation 311, 312 i 313 je wystawiają
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
raise Exception.Create('load failed');
if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 311: wymagana wersja, 312: cecha, która ją wywołała,
// 313: wersja, na której przypięty jest cel zapisu ('' gdy odblokowany)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
Dlaczego zwykły rysunek CAD padał z błędem 602?
Tabela reguł zawierała AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), która odpalała na każdym słowniku mającym po prostu klucz /Measure, a każdy viewport pomiarowy taki ma. Tablica /VP strony trzyma słowniki viewportów, każdy viewport wskazuje swój słownik pomiarowy przez /Measure, i dopasowanie po samej obecności klucza się tam kończyło, bez patrzenia, czym ten słownik pomiarowy faktycznie jest. Skan cech przy wczytywaniu mógł potem podnieść numer wersji dokumentu do 1.7, ale nigdy nie pisze deklaracji /Extensions w imieniu pliku wejściowego, więc bramka zapisu widziała PDF 1.7 na poziomie rozszerzenia 0 i raportowała 1.7 ExtensionLevel 3. Ta odmowa wymyślania deklaracji rozszerzenia jest celowa: biblioteka nie promuje po cichu pliku wejściowego, żeby zaklepać regułę, która jest zła
Specyfikacja w prostokątnym przypadku nie zostawia wątpliwości. Słowniki Measure przyszły z PDF 1.6, a ISO 32000-1 §12.9 daje /Subtype domyślnie RL, prostokątny układ współrzędnych opisany własnym zestawem wpisów: stosunek skali, formaty liczb X i Y, odległość i pole. Pomiar geoprzestrzenny to późniejszy dodatek z Adobe Extension Level 3 na PDF 1.7, rozpoznawany po /Subtype /GEO i niosący tablice punktów geograficznych, słowniki układów współrzędnych i jednostki wyświetlania, czyli struktury, które obchodzi artykuł o odczytywaniu viewportów GeoPDF i tablic GPTS i LPTS w Delphi. Oba słowniki wiszą na tym samym kluczu /Measure, więc każda reguła, która zatrzymuje się na kluczu, nie może być dobra dla obu. Informacja rozstrzygająca siedzi poziom niżej, w samym słowniku pomiarowym
Czego poprawiony zestaw reguł nadal pilnuje?
Poprawka usuwa bezwarunkową regułę klucza i zostawia bramki opisujące prawdziwe wymagania wersji. Strona niosąca /VP albo /UserUnit nadal potrzebuje PDF 1.6 przez CB_PagePDF16Entries, klucz /PtData nadal potrzebuje poziomu rozszerzenia 3, a CB_GeospatialDictionary rozstrzyga, czy słownik pomiarowy jest geoprzestrzenny, po jego zawartości, a nie po kluczu, przez który przyszedł
// Usunięte: każdy słownik z kluczem /Measure liczony był jako geoprzestrzenny
// AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil);
AddRule(16, fmkCustom, '', 'Page PDF 1.6 entry /UserUnit /VP', CB_PagePDF16Entries);
AddExtensionRule(3, fmkDictKey, 'PtData', '/PtData geospatial dictionary', Nil);
AddExtensionRule(3, fmkCustom, '', 'geospatial measure dictionary', CB_GeospatialDictionary);
function CB_GeospatialDictionary(Obj: TPDFObject; const Ctx: TPDFRuleContext): Boolean;
var
Dict: TPDFDictionary;
begin
Result := False;
if not (Obj is TPDFDictionary) then
Exit;
Dict := TPDFDictionary(Obj);
Result := (Dict.StringValue('Subtype') = 'GEO') or
(Dict.FindIndexByKeyName('GCS') >= 0) or (Dict.FindIndexByKeyName('DCS') >= 0) or
(Dict.FindIndexByKeyName('GPTS') >= 0) or (Dict.FindIndexByKeyName('LPTS') >= 0) or
(Dict.FindIndexByKeyName('PDU') >= 0);
end;
Wspólne regresje Delphi i FPC przypinają tę granicę z obu stron. Viewport, którego słownik pomiarowy pomija /Subtype, i taki, który wypisuje wprost /RL, oba przechodzą przy PDF 1.6, ta sama strona jest nadal odrzucana przy PDF 1.5, a wykrywanie cech nie raportuje dla niej już rozszerzenia. Dodanie tablicy /GPTS odwraca werdykt z powrotem na 1.7 ExtensionLevel 3, co przechodzi, gdy poziom rozszerzenia jest zadeklarowany, a goły słownik /Subtype /GEO jest odmawiany bez niego. Callback jest z założenia konserwatywny: prostokątny słownik, który niesie przy okazji obcy klucz /GCS albo /PDU, jest traktowany jako geoprzestrzenny, bo te klucze nie mają znaczenia w modelu RL
LockSaveVersion to miejsce, gdzie ta zmiana staje się widoczna dla wołających. TPDFlib.LockSaveVersion przyjmuje '1.0' przez '1.7', zwraca 0 dla czegokolwiek innego, przypina wersję dokumentu i blokuje wywołaniom strony zapisu po cichu podbijanie, a bramka zapisu i tak biegnie względem przypiętej wartości. Z poprawionymi regułami plik CAD przypięty na 1.6 zapisuje się czysto. Prawdziwy GeoPDF przypięty na 1.6 nadal dostaje 602, co jest poprawną odpowiedzią, a geoprzestrzenne wywołania autorskie, takie jak SetMeasureDictCoordinateSystem, deklarują same poziom rozszerzenia 3, gdy budujesz tę treść przez API
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
begin
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// Prawdziwa treść powyżej 1.6, na przykład słownik pomiarowy GEO
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;
Dlaczego skan reguł wersji był wolniejszy, niż musiał być?
Skan kopiował każdy TPDFFeatureRule do lokalnego rekordu przed testem, a że rekord trzyma dwa pola AnsiString, każda kopia ruszała dwa liczniki referencji i zwalniała poprzednie wartości. Preflight odwiedza każdy węzeł każdego drzewa obiektów, skalary włącznie, więc ten koszt mnożył liczbę obiektów przez liczbę reguł, a reguły, które w ogóle nie dotyczyły wersji celu, były najpierw kopiowane, a potem pomijane. Ponieważ PDFFeatureRules jest wypełniany raz przy inicjalizacji jednostki i traktowany jako tylko do odczytu, v3.539.17 podaje wpisy tabeli wprost do MatchSingleRule i RuleExceedsTarget, których parametry const Rule biorą referencję, nie dotykając łańcuchów
// Przed: zarządzana kopia rekordu na regułę, na odwiedzony obiekt
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
Continue;
// Po: parametry const czytają niezmienny wpis tabeli na miejscu
if not RuleExceedsTarget(PDFFeatureRules[X], TargetVersion, TargetExtensionLevel) then
Continue;
if MatchSingleRule(Obj, Ctx, PDFFeatureRules[X]) then
begin
RequiredVersion := RequiredVersionString(PDFFeatureRules[X]);
FeatureName := PDFFeatureRules[X].Feature;
Result := False;
Exit;
end;
Zmierzony efekt jest wąski i tak go należy cytować. Benchmark sprawdza tablicę 20 000 obiektów liczbowych względem celu PDF 1.4 dziesięć razy na rundę; zbudowany z FPC Win64 przy -O2, mediana pięciu rund spadła z 0.711 s do 0.203 s, a puszczenie obu budow w odwróconej kolejności dało 0.459 s wobec 0.150 s. To w przybliżeniu zysk 3x na samej ścieżce dopasowania reguł. Prawdziwy zapis płaci też za odroczone wykrywanie cech, dekodowanie obiektów i serializację, więc iloraz nie przenosi się na całkowity czas zapisu. Kolejność reguł, callbacki, progi wersji i diagnostyka pierwszej porażki są bez zmian, a żadna reguła nie była cache'owana między zapisami ani pomijana, żeby to osiągnąć
Co sprawdzić, gdy wczytany PDF pada na preflighcie wersji?
Przeczytaj klucze 311 i 312, zanim ruszysz wersję. Jeśli cecha nazywa słownik geoprzestrzenny, a plik rysuje tylko prostokątne pomiary, to był ten fałszywy alarm i aktualna budowa zapisze plik bez zmian. Jeśli cecha jest prawdziwa, zadeklaruj rozszerzenie albo przypnij wersję, która szczerze zawiera tę treść; podbicie wersji tylko po to, by uciszyć bramkę, chowa pytanie, czy konsumenci downstream przeczytają to, co wysyłasz. Ta sama zasada ograniczonych, dowodowo popartych kontroli napędza preflight trybu autorskiego PDF/E-1 dla dokumentów inżynieryjnych, gdzie rysunki CAD spotykają standard zgodności, a nie numer wersji
Kontrole zgodności wersji, słowniki pomiarowe i geoprzestrzenne oraz przypinanie wersji zapisu to wszystko części PDF Library for Delphi, zestawu narzędzi PDFlibPas dla programistów Delphi, C++Buildera i Lazarusa