Policzenie stron w skanowanym archiwum o rozmiarze 1,4 GB powinno być tanie. Wywołaj LoadFromFile na tym pliku, a przestaje być tanie: HotPDF parsuje dane odsyłaczy i buduje obiekt w pamięci dla każdego z kilkuset tysięcy obiektów pośrednich dokumentu, a 32-bitowy proces roboczy uderza w sufit 2 GB przestrzeni adresowej gdzieś w środku tego parsowania. Operacja, której chciałeś, czyli liczba stron, nigdy nie potrzebowała żadnego z tych obiektów. Potrzebowała drzewa stron i niczego więcej. Ta luka, między tym, o co prosi zadanie, a tym, co dostarcza pełne wczytanie, jest całym powodem istnienia Direct File API
Direct File API daje Delphi i C++Builder dostęp do PDF na poziomie pliku: liczbę stron, kopiowanie, odszyfrowywanie, przyrostowe dopisywanie, wszystko czytając z dysku to, czego faktycznie potrzebuje, zamiast odtwarzać cały model dokumentu w pamięci RAM. Sztuką jest dopasowanie każdego zadania do najlżejszej warstwy, która potrafi na nie odpowiedzieć. Traf w to dopasowanie, a usługa utrzyma płaskie zużycie pamięci przy dowolnym rozmiarze wejścia. Pomyl się, a pierwszy przerośnięty plik położy proces roboczy
Ile kosztuje cię pełne wczytanie
LoadFromFile nie jest wrogiem. Zarabia na swoją pamięć: gdy drzewo jest już w RAM, masz swobodny dostęp do każdej strony i każdego obiektu, a dokładnie tego wymagają InsertPagesFromDocument, MovePage i ponowna serializacja przez SaveLoadedDocument. Nie ma skrótu do prawdziwej przebudowy struktury; żeby dokument przestawić, trzeba go trzymać
Kłopot zaczyna się, gdy rozmiary wejść nie są w twoich rękach. Pliki wysyłane przez klientów, wyjście skanera i archiwa sprzed dekady ignorują wszystko, co zakładał twój korpus testowy. Wczytuj każde wejście bezwarunkowo, a twój sufit pamięci wyznaczy pojedynczy największy plik, jaki ktokolwiek kiedykolwiek prześle. Czas parsowania podąża za liczbą obiektów, a pamięć rezydentna ustala się na kilkukrotności rozmiaru pliku po doliczeniu struktur obiektowych i zdekodowanych strumieni, więc gigabajt na dysku może oznaczać kilka gigabajtów rezydentnych
Przekompilowanie na 64 bity podnosi sufit przestrzeni adresowej, ale zostawia rachunek nietknięty. Proces roboczy nadal pali sekundy procesora i wielokrotność pliku w RAM, by odpowiedzieć na pytanie, na które własna struktura pliku odpowiedziałaby w milisekundach. Przy współbieżności matematyka robi się wroga: cztery duże wczytania działające naraz dzielą jeden budżet pamięci, a przepustowość załamuje się dokładnie wtedy, gdy kolejka jest najgłębsza i najmniej cię na to stać
Odczyt pliku przez uchwyt
Warstwa tylko do odczytu otwiera plik jako uchwyt, odpowiada na pytania o jego strukturę i zamyka go. Żadnego drzewa obiektów, żadnego renderowania stron, żadnej pamięci rosnącej wraz z wejściem
var
Pdf: THotPDF;
Handle, PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Handle := Pdf.DAOpenFileReadOnly('archive-2026-06.pdf', '');
if Handle > 0 then
try
PageCount := Pdf.DAGetPageCount(Handle);
RouteByPageCount('archive-2026-06.pdf', PageCount);
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
Trzy nawyki utrzymują tę warstwę w uczciwości. Po pierwsze, sprawdzaj wartość zwracaną. Niedodatni uchwyt oznacza, że otwarcie zawiodło, a wystrzelenie DAGetPageCount w martwy uchwyt to rodzaj błędu, który siedzi w ukryciu do dnia, w którym klient przyśle zniekształcony plik. Po drugie, paruj każde udane otwarcie z DACloseFile wewnątrz bloku finally; usługa, która przecieka uchwytami, nie pada, ona po prostu gnije, co jest gorsze. Po trzecie, uszanuj to, co naprawdę robi parametr hasła. DAOpenFileReadOnly je przyjmuje, ale dla wejść zaszyfrowanych po cichu schodzi do pełnego parsowania, by odczytać liczbę stron, więc gwarancja płaskiej pamięci wyparowuje. Przepuść pliki chronione najpierw przez DecryptFile, a reszta potoku pozostanie tania
Ta sama sonda służy zarazem jako bramka segregacyjna. Pliki przychodzą źle opisane, wysłane w połowie albo przemianowane z całkiem innego formatu, a sprawdzenie przez DAOpenFileReadOnly odrzuca je wszystkie już przy drzwiach w milisekundach, z błędem przypiętym do konkretnego pliku. Alternatywą jest pozwolenie plikowi ze śmieciami wjechać głęboko w proces roboczy kolejki i wybuchnąć tam, gdzie rozplątanie, które wejście to spowodowało, może kosztować popołudnie
Kopiowanie, odszyfrowywanie i szyfrowanie całych plików
Druga warstwa przenosi i przekształca kompletne pliki, nigdy nie odsłaniając ich wnętrza. To wywołania, na których potoki przyjmowania opierają się najbardziej
// Kopia strukturalna: sprawdź i przenieś bez parsowania drzewa obiektów
Status := Pdf.DACopyFile('incoming\statement.pdf', 'verified\statement.pdf');
LogDirectFileStatus('copy', Status);
// Odszyfruj przy kopiowaniu: droga Direct File do wejść chronionych
Status := Pdf.DecryptFile('incoming\protected.pdf',
'verified\plain.pdf', 'batch-password');
LogDirectFileStatus('decrypt-copy', Status);
// Zaszyfruj przy kopiowaniu: chroń wyjście bez pełnego wczytania
Status := Pdf.EncryptFile('verified\statement.pdf',
'outbound\statement.pdf', 'owner-secret', '', aes256, [prPrint]);
LogDirectFileStatus('encrypt-copy', Status);
Każde wywołanie zasługuje na swoje miejsce. DACopyFile to sprawdzona kopia z katalogu kwarantanny do zarządzanego magazynu: otwiera i indeksuje strukturę PDF po drodze, więc obcięte albo niebędące PDF wejście zawodzi już tutaj, a nie trzy etapy dalej. DecryptFile zapisuje odszyfrowaną kopię ścieżką bezpośredniego przepisania AES-256, która pomija drzewo obiektów zawsze, gdy wejście na to pozwala, będąc odpowiednikiem dla dużych plików przepływu odszyfrowania przez wczytaj-i-zapisz opisanego w artykule o szyfrowaniu AES-256. EncryptFile wykonuje ten sam ruch w drugą stronę, nakładając ochronę hasłem podczas kopiowania na poziomie pliku, z tymi samymi parametrami typu klucza i uprawnień, których używa już ścieżka pamięciowa
Dopisywanie zmian zamiast przepisywania
Aktualizacja przyrostowa, zdefiniowana w ISO 32000-1 §7.5.6, to trzecia warstwa. Oryginalne bajty zostają tam, gdzie są na dysku, a wszystkie nowe albo zmodyfikowane obiekty są dopisywane za nimi, a po nich świeża sekcja odsyłaczy wiążąca się z powrotem z oryginałem. Dla archiwum o rozmiarze 900 MB, które wymaga dodania jednej strony, koszt zapisu to różnica, a nie cały plik
// Dopisz stronę audytową do dużego archiwum bez przepisywania go
Pdf.BeginIncrementalUpdate('archive-2026-06.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Processed by intake service 2026-06-11');
Pdf.SaveIncrementalUpdate('archive-2026-06-stamped.pdf'); // oryginalne bajty + różnica
Dwie kwestie dyscypliny mają tu znaczenie. BeginIncrementalUpdate musi wskazywać oryginalny plik, ponieważ dopisywane dane odsyłaczy wiążą się z przesunięciami bajtowymi w jego wnętrzu. A model jest z założenia tylko do dopisywania: każdy zapis przyrostowy powiększa plik, nigdy go nie zmniejsza. Dokument stemplowany co noc będzie puchł bez granic, dopóki okresowa ponowna serializacja, czyli wczytanie go i zapisanie z powrotem przez SaveLoadedDocument, go nie zagęści. Ta sama natura tylko-do-dopisywania sprawia, że aktualizacja przyrostowa jest jedynym bezpiecznym sposobem dotknięcia dokumentu podpisanego cyfrowo, co bada artykuł o podpisach cyfrowych i PAdES. Leżąca pod spodem maszyneria odsyłaczy dostaje własne omówienie w artykule o strumieniach obiektów i aktualizacjach przyrostowych
W zapisach tylko do dopisywania kryje się pułapka, która wymyka się większości przeglądów. Oryginalne bajty zostają w pliku, czytelne dla każdego, kto zechce poszukać. Aktualizacja przyrostowa, która „zastępuje” stronę, nie usuwa starej; przesłania ją w bieżącej rewizji, podczas gdy poprzednia rewizja tam siedzi, w pełni do odzyskania. Aktualizacje przyrostowe są więc złym narzędziem do usuwania treści wrażliwych. Żeby naprawdę porzucić historię, której odbiorca nigdy nie powinien zobaczyć, potrzebujesz pełnej ponownej serializacji: LoadFromFile, a po nim SaveLoadedDocument, które wypisuje wyłącznie bieżący stan i zostawia pogrzebane rewizje za sobą
Dopasowanie warstwy do operacji
Logika wyboru jest na tyle krótka, że mieści się w głowie, a opłaca się zakodować ją jako jawną decyzję kierowania na szczycie potoku, zamiast pozwalać każdemu zadaniu improwizować własną drogę. O warstwie decyduje potrzebna operacja:
- Liczenie, inspekcja albo klasyfikacja otwiera uchwyt:
DAOpenFileReadOnly,DAGetPageCount,DACloseFile - Przenoszenie, odszyfrowywanie albo szyfrowanie całego pliku zostaje na poziomie pliku z
DACopyFile,DecryptFilealboEncryptFile - Przebudowa stron albo scalanie dokumentów wymaga pełnego wczytania:
LoadFromFile, potemInsertPagesFromDocumentalboMovePage, potemSaveLoadedDocument - Dodanie małej różnicy do ogromnego albo podpisanego pliku wywołuje
BeginIncrementalUpdatei zapisuje
Potoki mieszane dobrze robią, stawiając próg rozmiaru przed ścieżką pełnego wczytania. Wszystko powyżej kilkuset megabajtów kieruj przez warstwy Direct File, a pełne wczytanie zarezerwuj dla prawdziwej przebudowy struktury na 64-bitowym procesie roboczym z realnym budżetem pamięci. Ten próg zamienia awarię z braku pamięci w decyzję kierowania, którą widzisz i możesz stroić
Niezależnie od tego, która warstwa obsługuje zadanie, zapisuj jej wyjście pod nazwą tymczasową i zmieniaj nazwę na docelową dopiero wtedy, gdy wynik przejdzie walidację. Plik zapisany w połowie leżący pod nazwą finalną wygląda dla kolejnego etapu potoku dokładnie jak dobry, a wywołania Direct File czynią to sprawdzenie tanim: potwierdzenie wyjścia to jednolinijkowa sonda uchwytu
Direct File API jest dostarczane jako część komponentu HotPDF dla Delphi oraz C++Builder. Strona produktu linkuje pełne odniesienie do funkcji, w tym pokazane tu wywołania aktualizacji przyrostowej