Artykuł techniczny

PDF Library for Delphi: scalanie i dzielenie dużych plików PDF z bezpośrednim dostępem

Łączenie albo dzielenie dwugigabajtowego PDF w oczywisty sposób kosztuje cię dwie rzeczy naraz: czas rzeczywisty i przestrzeń adresową. Oczywistym sposobem jest wczytanie każdego wejścia, wykonanie pracy, zapisanie wyjścia. Wczytywanie to miejsce, gdzie to się łamie. Archiwum skanów, które przechodzi z 300 na 600 DPI, podwaja swoją rozdzielczość liniową i mniej więcej czterokrotnie zwiększa rozmiar na dysku, więc to samo zadanie składania, które obsługiwało pliki 400 MB przez cały rok, zaczyna się szarpać w chwili, gdy wejście przekracza gigabajt, często nie robiąc nic więcej niż liczenie stron. Zadanie nigdy nie stało się trudniejsze. Otwórz, policz, wybierz zakresy, połącz, to cała jego treść. Wczytywanie pełnego drzewa po prostu przestało być rozsądnym domyślnym wyborem przy takim rozmiarze. PDF Library for Delphi, biblioteka PDF losLab dla Delphi i C++Builder, odpowiada na to warstwą Direct Access: rodziną funkcji z prefiksem DA, wspieraną przez strumieniowy czytnik, który przechodzi przez tablicę odniesień krzyżowych w miejscu, zamiast budować cały dokument w pamięci

Gdzie znika pamięć przy pełnym wczytaniu

Wczytanie PDF "normalnie" oznacza parsowanie xref, rozwiązanie każdego obiektu pośredniego w drzewo w pamięci, dekodowanie strumieni obiektów i połączenie drzewa stron, czcionek i adnotacji w obiekty, którymi można manipulować. Dla przepływów edycyjnych to właściwy kompromis. Dla pracy nad łączeniem, dzieleniem i inspekcją to głównie marnotrawstwo. Archiwum skanów na 30 000 stron może zawierać miliony obiektów pośrednich, a zadanie dzielenia musi odczytać kilkaset z nich: węzły stron w żądanym zakresie plus wszystko, do czego te węzły się odwołują

Warstwa Direct Access odwraca ten model. DAOpenFile i DAOpenFileReadOnly parsują trailer i xref, kilka kilobajtów na końcu pliku, i zwracają uchwyt pliku. Obiekty są pobierane leniwie, gdy wywołanie ich potrzebuje. Praktyczną konsekwencją jest to, że otwarcie kilkugigabajtowego pliku trwa mniej więcej tyle samo, co otwarcie małego, a pamięć śledzi to, czego dotykasz, a nie to, co zawiera plik

Porównanie PDF Library for Delphi wczytywania gigabajtowego PDF do pełnego drzewa obiektów w pamięci z otwarciem w trybie bezpośredniego dostępu, gdzie parsowanie zatrzymuje się na trailerze i xref, a uchwyt obsługuje leniwe odczyty per obiekt
Pełne wczytanie dekoduje każdy obiekt pośredni, zanim scalenie może się zacząć, więc RAM i czas otwarcia skalują się z archiwum. Ścieżka bezpośredniego dostępu zwraca działający uchwyt po odczycie kilobajtów i pozwala każdemu wywołaniu pobrać tylko potrzebne obiekty

Sondowanie ogromnego pliku bez jego wczytywania

Poniższy wzorzec pochodzi z własnego benchmarku dużych plików biblioteki: otwórz tylko do odczytu, zadaj pytania, zamknij. Drzewo dokumentu nigdy nie powstaje

var
  Lib: TPDFlib;
  Handle, Pages: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Handle := Lib.DAOpenFileReadOnly('archive-2025.pdf', '');
    if Handle = 0 then
      raise Exception.Create('Direct access open failed');
    Pages := Lib.DAGetPageCount(Handle);
    Writeln('pages : ', Pages);
    Writeln('title : ', Lib.DAGetInformation(Handle, 'Title'));
    Lib.DACloseFile(Handle);
  finally
    Lib.Free;
  end;
end;

Tryb tylko do odczytu warto preferować, kiedy tylko się da: pozwala etapowi pobierania działać, podczas gdy inne procesy trzymają plik, i dokumentuje intencję. Etap sondowania, który przez przypadek wywoła funkcję mutującą, zawodzi szybko zamiast uszkadzać archiwum

PageRef to uchwyt obiektu, nie numer strony

Najczęstszym pojedynczym błędem z API DA jest przekazanie numeru strony tam, gdzie funkcja oczekuje PageRef. Niemal każde wywołanie DA per strona przyjmuje uchwyt referencyjny do obiektu strony, a nie numer strony: DAExtractPageText, DARenderPageToFile, DARotatePage i DACapturePage wszystkie oczekują referencji. Dostajesz ją, tłumacząc czytelny dla człowieka numer przez DAFindPage:

PDF Library for Delphi: Przepływ tłumaczenia numeru strony na PageRef pokazujący DAFindPage zasilające bezpośrednie wywołania dostępu per strona, w przeciwieństwie do surowego odniesienia całkowitoliczbowego lądującego na dowolnym obiekcie i po cichu produkującego tekst złej strony
Każde wywołanie bezpośredniego dostępu dla strony konsumuje PageRef wyprodukowany przez DAFindPage, nigdy numer widziany przez człowieka. Pominięcie tego tłumaczenia sprawia, że liczba całkowita podszywa się pod id obiektu, a tekst ze złej strony może niepostrzeżenie trafić do odbiorcy
PageRef := Lib.DAFindPage(Handle, 250);          // numer strony -> uchwyt obiektu
if PageRef <> 0 then
begin
  Text := Lib.DAExtractPageText(Handle, PageRef, 0);
  Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;

Przekazanie zamiast tego surowego numeru 250 nie zgłasza błędu. Adresuje to, co akurat siedzi za tą wartością uchwytu, co w dobry dzień zawodzi widocznie, a w zły dzień wyodrębnia tekst z niewłaściwej strony do dokumentu widocznego dla klienta. Jeśli owijasz warstwę DA własnym kodem usługi, spraw, by tłumaczenia nie dało się pominąć: przyjmuj numery stron na granicy, wywołuj DAFindPage natychmiast i przekazuj wewnętrznie tylko referencje

Łączenie setek plików za pomocą nazwanej listy

Dla dwóch plików wystarczy MergeFiles(First, Second, Output). Wsadowe składanie skaluje się lepiej przez listy plików: zarejestruj wejścia pod nazwą listy, a potem połącz listę w jednym przebiegu

PDF Library for Delphi: Przepływ nazwanej listy plików, gdzie zestawienia stycznia, lutego i marca rejestrują się pod jedną nazwą listy i scalają w jednym przebiegu, przy czym warianty Fast, domyślny i strict wymieniają zachowanie drzewa struktury na szybkość
Setki zarejestrowanych wejść zapadają się w jeden przebieg MergeFileList, którego wynik weryfikuje się w milisekundach przez kolejną sondę tylko do odczytu. Wariant to decyzja per potok, ponieważ Fast porzuca drzewo struktury Tagged PDF
Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');

// Sprawdź wynik tanim sposobem: znowu bezpośredni dostęp
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);

Rodzina funkcji łączących ma trzy warianty, a różnica to nie tylko szybkość. MergeFileListFast pomija zachowanie drzewa struktury; MergeFileListStrict wymusza tryb ścisły; wersja bez sufiksu to zrównoważony domyślny wybór. Zasada operacyjna, która z tego wynika: jeśli którekolwiek wejście to Tagged PDF, którego struktura dostępności musi przetrwać, z oczywistym przykładem wszystkiego, co produkowane jest dla PDF/UA, sięgnij po wariant domyślny albo Strict, bo Fast po cichu porzuca drzewo struktury. Dla zwykłych archiwów skanów bez tagowania Fast to darmowa wydajność. Decyduj per potok, nie per nastrój programisty, i zapisuj użyty wariant w dzienniku zadania

Dzielenie bez wczytywania: wyodrębnianie zakresów

Dzielenie podąża za tą samą filozofią braku wczytywania. ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) ciągnie zakres stron wprost z pliku do pliku, z listą zakresów taką jak '1-500', '501-1000', albo wyborami rozdzielonymi przecinkami, a źródło nigdy nie staje się drzewem dokumentu. Gdy dokument jest już wczytany z innych powodów, ExtractPageRanges produkuje nowy dokument w pamięci z bieżącego, a CopyPageRanges ciągnie zakresy z innego wczytanego dokumentu po ID. Dla dzielenia strumieni druku skonsolidowanych na poszczególne wyciągi, forma z pliku do pliku to ta, która nie pozwala wejściu 4 GB nigdy napęcznieć do RAM

Pliki, które kłamią o swojej geometrii

Potoki dużych plików spotykają uszkodzone pliki w tempie, którego potoki małych plików nigdy nie widzą, po prostu dlatego, że wejścia przechodzą przez więcej systemów. Dwa kształty awarii zasługują na jawną obsługę

Po pierwsze, przesunięte nagłówki. Bramy pocztowe i buforowanie druku czasem dodają bajty przed PDF, więc znacznik %PDF nie siedzi już na przesunięciu 0, a każde przesunięcie xref w pliku jest błędne o tę samą wartość. Strumieniowy czytnik wykrywa to i udostępnia (DAShiftedHeader na poziomie płaskim, ShiftedHeader na TSmartPDFReader), a potem kompensuje to podczas odczytów. Domowa arytmetyka przesunięć zwykle tego nie robi, dlatego "działa na każdym pliku, który generujemy, zawodzi na plikach od klienta X" to klasyczny objaw

Po drugie, uszkodzone tablice odniesień krzyżowych. DACopyFile(InputFileName, OutputFileName, PageCount) strumieniuje cały plik do nowej kopii, jednocześnie odbudowując xref, zwracając liczbę stron jako produkt uboczny. Uruchomienie tego jako etapu normalizacji przed wybrednym konsumentem niżej w potoku zamienia klasę przejściowych awarii parsowania w jeden przewidywalny krok naprawy. A gdy twoje własne edycje wymagają zapisu, DAAppendFile zapisuje je jako przyrostową aktualizację, dopisując nową rewizję zamiast przepisywać gigabajty, co utrzymuje koszt zapisu proporcjonalny do zmiany, a nie do pliku

Szczegóły dostawy: linearyzacja i kompozycja

Dwie sąsiadujące funkcje dopełniają potok dużych plików. Gdy złożone wyjście jest serwowane przez HTTP do przeglądania w przeglądarce, LinearizeFile reorganizuje je do strumieniowania po zakresach bajtów, tak by pierwsza strona wyświetlała się, zanim reszta pakietu 500 MB skończy się pobierać. Uruchom to jako etap końcowy, po całym łączeniu, bo każda późniejsza modyfikacja ponownie odlinearyzuje plik. A gdy pakiety potrzebują kompozycji zamiast zwykłej konkatenacji, powiedzmy strony tytułowej odbitej za każdym wyciągiem albo dwóch stron źródłowych złożonych na jeden arkusz wyjściowy, DACapturePage zamienia dowolną stronę w wielokrotnego użytku szablon, który DADrawCapturedPage umieszcza na stronie docelowej w dowolnym prostokącie, wciąż bez pełnego wczytania dokumentu na kilkugigabajtowym źródle

Granice i co pozostaje tylko do odczytu

Sam format kończy się miejsce na długo przed tym, jak zabraknie go Direct Access. Przesunięcia są typu Int64 przez całą warstwę DA, więc prawdziwymi sufitami są dostępny dysk i 10-cyfrowe pole przesunięcia xref klasycznych (nie-strumieniowych) tablic odniesień krzyżowych. Kilkugigabajtowe archiwa skanów są w praktyce niewyróżniające się, a pamięć pozostaje ograniczona bez względu na rozmiar pliku, bo obiekty są odczytywane tylko wtedy, gdy wywołanie o nie poprosi

Dwa pytania pojawiają się na tyle często, że warto na nie odpowiedzieć wprost. Łączenie ścieżką domyślną przenosi strukturę dokumentu, więc zakładki i linki przetrwają; wariant Fast to ten, który wymienia drzewo struktury na szybkość, co jest całym powodem, by rezerwować go dla wejść nieotagowanych. Bezpiecznym nawykiem jest otwarcie połączonego wyjścia, przejście jego konspektu i wyrywkowe sprawdzenie kilku linków wewnętrznych przed wysłaniem go. Jeśli chodzi o edycję: jest użyteczny środek między sondowaniem tylko do odczytu a pełnym wczytaniem. Operacje na poziomie strony działają bezpośrednio na uchwycie, wśród nich DARotatePage, DAMovePage i DAHidePage, wraz z odczytami pól formularza, a DAAppendFile utrwala te edycje jako przyrostową rewizję. Edycja na poziomie treści, cokolwiek przepisuje operatory znakowania wewnątrz strony, wciąż należy do warstwy pełnego dokumentu

Powiązane artykuły

Jeśli twoje połączone wyjście musi pozostać dostępne, tło dotyczące drzewa struktury jest omówione w artykule o dostępności Tagged PDF, który wyjaśnia dokładnie, co wariant Fast łączenia by odrzucił. Do wyciągania treści z zakresów, które podzieliłeś, zobacz przewodnik po wyodrębnianiu tekstu, obrazów i czcionek

Pełna lista funkcji Direct Access jest dostarczana z biblioteką; edycje i pobrania wersji próbnej znajdują się na stronie produktu PDF Library for Delphi