Rzut kondygnacyjny wyeksportowany przez architektoniczny pakiet CAD ma typowo tablicę /VP na pierwszej stronie z dwoma viewportami, a każdy viewport niesie wpis /Measure, żeby przeglądarka mogła odczytywać metry prosto z rysunku. Arkusz mapy GeoPDF ma tę samą tablicę /VP i ten sam klucz /Measure. Rzut to zwykły PDF 1.6. Arkusz mapy potrzebuje PDF 1.7 plus Adobe Extension Level 3. Sprawdzenie wersji, które zatrzymuje się na nazwie klucza, zażąda rozszerzenia dla obu, a rzut zostanie odmówiony przy zapisie komunikatem o treści geoprzestrzennej, której nigdy nie zawierał
Jeden klucz /Measure, dwa różne słowniki
Nazwa klucza mówi ci tylko, że viewport jest mierzalny; dopiero słownik za nim mówi ci, jak. Słowniki Measure przyszły z PDF 1.6, a ISO 32000-1 §12.9 definiuje rodzaj prostokątny: łańcuch stosunku skali w /R, tablice formatów liczb w /X i /Y oraz dalsze tablice dla odległości (/D) i pola (/A). Pomiar geoprzestrzenny przyszedł później, jako część Adobe Extension Level 3 na PDF 1.7. Oznacza go /Subtype /GEO, a niesie geograficzny układ współrzędnych w /GCS, opcjonalny wyświetlany układ współrzędnych w /DCS, sparowane tablice punktów w /GPTS i /LPTS oraz preferowane jednostki wyświetlania w /PDU — struktury, które obchodzi odczytywanie viewportów GeoPDF i tablic GPTS i LPTS w Delphi
Szczegół, na którym ludzie się potykają, to wartość domyślna. §12.9 daje /Subtype wartość domyślną RL i wielu eksporterów CAD korzysta z tego, zostawiając wpis poza plikiem. Sprawdzacz, który czyta brakujący subtype jako "nieznane, zakładaj najgorsze", zamienia każdy z tych rysunków w dokument geoprzestrzenny. Sprawdzacz, który dopasowuje na kluczu /Measure, w ogóle nie dochodzi do pytania o subtype, bo słownik viewportu trzymający klucz jest poziom nad słownikiem pomiarowym, który na nie odpowiada
Jak preflight zapisu PDF Library for Delphi dochodzi do werdyktu
PDF Library for Delphi (PDFlibPas) uruchamia preflight wersji wewnątrz SaveToFile, zanim jakiekolwiek wyjście zostanie zapisane. Odwiedza każdy obiekt pośredni, schodząc w zagnieżdżone słowniki i tablice, i testuje każdy węzeł względem tabeli reguł cech. Każda reguła wymienia minimalną wersję PDF, a dla cech rozszerzeniowych poziom rozszerzenia Adobe; regułę rozszerzeniową może zaspokoić wyłącznie PDF 1.7 razem z pasującym wpisem /Extensions /ADBE w katalogu. Celem, względem którego sprawdza, jest wersja dokumentu albo wersja przypięta przez LockSaveVersion, plus cokolwiek poziomu rozszerzenia deklaruje katalog. Pierwsza reguła, która zarazem pasuje i przekracza ten cel, kończy zapis: SaveToFile zwraca 0, a LastErrorCode staje się 602, wartością PDFLIB_ERROR_VERSION_COMPLIANCE
Sam kod błędu nie mówi ci, co poprawić. Trzy klucze GetInformation niosą resztę: 311 to wersja, której potrzebuje winna cecha (na przykład 1.7 ExtensionLevel 3), 312 to nazwa cechy, a 313 to wersja, na której przypięty jest cel zapisu, pusta, gdy nic nie jest przypięte
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;
Dla treści pomiarowej tabela trzyma trzy reguły i celowo żadnej reguły na goły klucz /Measure. Strona z /VP albo /UserUnit potrzebuje PDF 1.6. Słownik /PtData potrzebuje poziomu rozszerzenia 3. Słownik pomiarowy potrzebuje poziomu rozszerzenia 3, gdy mówi /Subtype /GEO albo zawiera którykolwiek z /GCS, /DCS, /GPTS, /LPTS albo /PDU. Viewport, którego słownik pomiarowy pomija /Subtype, i taki, który wypisuje wprost /RL, przechodzą oba przy 1.6, podczas gdy ta sama strona przy 1.5 jest odmawiana z powodu /VP. Dodaj tablicę /GPTS, a odpowiedź staje się 1.7 ExtensionLevel 3
Test zawartości jest konserwatywny z zamysłem. Prostokątny słownik, który niesie przy okazji obcy /GCS albo /PDU, jest traktowany jako geoprzestrzenny. Wolelibyśmy odmówić temu plikowi niż przepuścić go machnięciem ręki: te klucze nie znaczą niczego w modelu prostokątnym, więc kto je napisał, to albo narzędzie geoprzestrzenne, albo coś, co uszkodziło plik, a przeglądarka świadoma GEO może je z powodzeniem próbować interpretować. Wczytywacz jest równie ostrożny w drugą stronę. Jego skan cech może podnieść numer wersji dokumentu do 1.7, ale nigdy nie zapisuje deklaracji /Extensions w imieniu pliku wejściowego — deklarowanie rozszerzenia to twierdzenie o treści i powinno pochodzić od twojego kodu albo wywołań autorskich
Sprawdzanie viewportów rysunku przed zapisem
Gdy 312 nazywa cechę geoprzestrzenną, a ty jesteś pewien, że plik to zwykły rysunek, spójrz sam na słowniki pomiarowe. Publiczne wywołania viewportów wystarczają. GetMeasureDictCoordinateSystem czyta /Subtype i zwraca 1 dla RL, 2 dla GEO i 0, gdy wpisu brak — co według §12.9 także znaczy prostokątny. Liczby tablic punktów mówią ci, czy jakiekolwiek dane georeferencyjne są obecne
// Zgłoś każdy słownik pomiarowy w wczytanym dokumencie.
// Wszystkie wywołania tu tylko czytają; żadne nie dodaje kluczy do pliku.
procedure ReportMeasureDicts(Pdf: TPDFlib);
var
Page, VP, VPID, MD, Kind: Integer;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.SelectPage(Page);
for VP := 1 to Pdf.GetPageViewPortCount do
begin
VPID := Pdf.GetPageViewPortID(VP);
MD := Pdf.GetViewPortMeasureDict(VPID);
if MD = 0 then
Continue;
Kind := Pdf.GetMeasureDictCoordinateSystem(MD); // 0 = pominięty (RL), 1 = RL, 2 = GEO
Writeln(Format('page %d viewport "%s": subtype %d, GPTS %d, LPTS %d',
[Page, string(Pdf.GetViewPortName(VPID)), Kind,
Pdf.GetMeasureDictGPTSCount(MD), Pdf.GetMeasureDictLPTSCount(MD)]));
end;
end;
end;
Dwa wywołania celowo nie ma w tej pętli: GetMeasureDictGCSDict i GetMeasureDictDCSDict. Te dwa to akcesory get-or-create zbudowane do autorstwa. Na słowniku pomiarowym bez wpisu /GCS GetMeasureDictGCSDict dodaje pusty i deklaruje dla dokumentu poziom rozszerzenia 3. Wołaj go jako sondę na prostokątnym rzucie, a słownik teraz naprawdę zawiera /GCS; następujący po tym zapis przypięty na 1.6 zostaje odmówiony, słusznie, za treść, którą twoja diagnostyka dopiero co napisała. Jeśli potrzebujesz wiedzieć, czy układ współrzędnych istnieje, subtype i liczby tablic punktów odpowiadają na to bez efektów ubocznych
Przypinanie wyjścia na PDF 1.6
Przypięta wersja trzyma wyjście uczciwym, gdy twoi konsumenci mają sufit. TPDFlib.LockSaveVersion przyjmuje łańcuchy '1.0' przez '1.7' i zwraca 0 dla czegokolwiek innego. Unieruchamia wersję dokumentu i blokuje wywołaniom strony zapisu samodzielne podbijanie, ale preflight zapisu i tak biegnie względem przypiętej wartości. Rysunek CAD przypięty na 1.6 zapisuje się czysto. Prawdziwy GeoPDF przypięty na 1.6 dostaje 602, i to jest odpowiedź, której chcesz, bo alternatywą jest plik, którego nagłówek obiecuje mniej, niż jego treść wymaga
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
try
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
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))]);
finally
Pdf.UnlockSaveVersion;
end;
Przeczytaj 311 i 312, zanim ruszysz wersję. Jeśli cecha to prawdziwa treść geoprzestrzenna, masz dwa uczciwe wybory: zostawić dokument odblokowany na 1.7 z zadeklarowanym rozszerzeniem albo wyrzucić dane geoprzestrzenne z kopii, którą wysyłasz konsumentowi tylko-1.6. Podnoszenie blokady tylko po to, by błąd zniknął, to opcja do unikania. Zakrywa ona prawdziwe pytanie — czy czytnik downstream da radę z tym, co wysyłasz? Gdy budujesz treść geoprzestrzenną przez API, settery SetMeasureDict*, takie jak SetMeasureDictCoordinateSystem i SetMeasureDictGPTSCount, deklarują same poziom rozszerzenia 3, więc pliki autorskie przychodzą do preflightu już oznaczone. Ten sam nawyk ograniczonych, dowodowo popartych kontroli pokazuje się w preflighcie trybu autorskiego PDF/E-1 dla dokumentów inżynieryjnych, gdzie rysunki CAD spotykają standard zgodności zamiast numeru wersji
Dlaczego skan reguł czyta tabelę na miejscu
Koszt preflightu to z grubsza węzły obiektów razy reguły, a liczba węzłów obejmuje każdą liczbę i każdą nazwę wewnątrz każdej tablicy. To sprawia, że narzut na regułę ma większe znaczenie, niż wygląda. Każda reguła w tabeli to rekord z dwoma polami AnsiString: łańcuchem dopasowania i nazwą cechy. Skopiowanie takiego rekordu do zmiennej lokalnej każe Delphi albo FPC podbić dwa liczniki referencji i zwolnić to, co lokalna trzymała wcześniej. Robione raz na regułę na węzeł, znaczy płacenie za księgowość łańcuchów nawet dla reguł, które nie dotyczą wersji celu i są wyrzucane w następnej linii
Tabela reguł jest wypełniana raz przy inicjalizacji jednostki i nigdy potem nie modyfikowana, więc skan podaje wpisy tabeli swojemu kodowi dopasowującemu przez parametry rekordowe const i czyta je tam, gdzie żyją. Parametr rekordowy const jest przekazywany przez referencję, więc testowanie reguły nie dotyka żadnego pola łańcuchowego, a liczniki referencji zostają tam, gdzie były. Efekt koncentruje się na ścieżce dopasowania reguł. Prawdziwy zapis dekoduje też obiekty, uruchamia odroczone wykrywanie cech i serializuje wyjście, i te etapy nie są ruszane przez sposób przekazywania reguł. Kolejność reguł, progi i diagnostyka pierwszej porażki w 311 i 312 nie zależą od tego, a żaden wynik reguły nie jest cache'owany między zapisami, więc dokument edytowany i zapisywany dwa razy jest sprawdzany w pełni za każdym razem
Ten sam wzorzec warto sprawdzić we własnym kodzie Delphi wszędzie tam, gdzie pętla obchodzi tabelę rekordów zawierających łańcuchy, interfejsy albo tablice dynamiczne:
type
TMyRule = record
Name: string; // pole zarządzane: kopie kosztują pracę na licznikach referencji
Threshold: Integer;
end;
// Kod czytelnika, pokazany jako wzorzec
function Applies(const Rule: TMyRule; Value: Integer): Boolean;
begin
Result := Value >= Rule.Threshold; // const: bez kopii, bez ruchu na licznikach referencji
end;
// W gorącej pętli podaj wpis tabeli wprost zamiast
// przypisywania go najpierw do lokalnego rekordu
for I := 0 to High(Rules) do
if Applies(Rules[I], V) then
Inc(Hits);
Viewporty pomiarowe, słowniki geoprzestrzenne, przypinanie wersji i sprawdzenie zgodności przy zapisie pływają wszystkie w PDF Library for Delphi, dostępnej dla projektów Delphi, C++Builder i Lazarus