Łą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
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:
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
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