PDFlibPas może otworzyć lokalny PDF przez ograniczony, tylko do odczytu widok mapowany w pamięci: LoadFromMappedFile i DAOpenMappedFile utrzymują dokładnie jedno przesuwne okno pliku, remapują je na żądanie i dostarczają każdy fragment obiektu przez odczyty z absolutnym offsetem. Biblioteka PDF dla Delphi nigdy nie przechowuje całego źródła w pamięci, więc zużycie przestrzeni adresowej pozostaje stałe, gdy plik rośnie. Ten projekt służy jednemu obciążeniu: wielogigabajtowym PDF-om, w których parser zakończył ładowanie, ale nadal wraca do dysku obiekt po obiekcie i fragment strumienia po fragmencie strumienia
Dlaczego rozproszone odczyty pozostają kosztowne po załadowaniu PDF-a?
Załadowanie PDF-a nie kończy jego odczytu, a przy pliku wielogigabajtowym właśnie ta różnica pochłania czas. Tabela odsyłaczy albo strumień odsyłaczy (ISO 32000-1 §7.5.4 i §7.5.8) rejestruje tylko miejsce rozpoczęcia każdego obiektu pośredniego. Bajty pojawiają się później, gdy renderowana jest strona, dekodowany jest program fontu albo wyodrębniany jest osadzony strumień pliku (ISO 32000-1 §7.11.4). Archiwum o rozmiarze 2 GB z dziesiątkami tysięcy obiektów zamienia się w dziesiątki tysięcy małych, nieuporządkowanych odczytów, a podczas ładowania nie wiadomo o żadnym z nich
Dotychczasowa ścieżka tych odczytów prowadziła przez współdzielone Seek, a następnie Read na jednym strumieniu pozycyjnym, i zawodziła jednocześnie na dwa sposoby. Każdy fragment płacił za odczyt pliku, nawet gdy strona była już w pamięci podręcznej systemu operacyjnego, a kursor był współdzielonym zmiennym stanem, więc lokalny plik i źródło zakresowe za progresywnym ładowaniem zakresów PDF z prefetch nie mogły uruchamiać tego samego kodu parsera bez walki o pozycję. PDFlibPas rozwiązuje oba problemy, podnosząc odczyt z absolutnym offsetem z poziomu optymalizacji do poziomu kontraktu
Co gwarantuje TPDFReadAtStream?
TPDFReadAtStream gwarantuje odczyt z absolutnego offsetu, który ani nie zależy od logicznego kursora strumienia, ani go nie zmienia. To abstrakcyjny potomek TStream z dokładnie jedną metodą wirtualną, a oba źródła niezależne od kursora w bibliotece po nim dziedziczą: TReadOnlyMappedFileStream dla plików lokalnych i TByteRangeStream dla zdalnych źródeł obsługujących zakresy. Czytnik fragmentu obiektu raz sprawdza, czy jego źródłem jest TPDFReadAtStream, a gdy nie jest, wraca do starej sekwencji seek-then-read, więc zwykły strumień pliku albo strumień pamięci nadal działa bez zmian
type
// Strumienie tylko do odczytu, których odczyty absolutne omijają współdzielone Seek i Read
TPDFReadAtStream = class(TStream)
public
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; virtual; abstract;
end;
// Okienkowy dostęp tylko do odczytu do jednego pliku lokalnego
TReadOnlyMappedFileStream = class(TPDFReadAtStream)
private
FMemoryMapped: Boolean;
public
constructor Create(const FileName: WideString; WindowSize: Int64 = 0);
function GetStats: TPDFMappedFileStats;
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; override;
property MemoryMapped: Boolean read FMemoryMapped;
end;
Różnica jest ważniejsza, niż sugeruje sygnatura. ReadAt używa przekazanego offsetu i pozostawia Position dokładnie tam, gdzie była, dzięki czemu zagnieżdżone poziomy parsera mogą wykonywać odczyty bez zapisywania i przywracania pozycji wokół każdego wywołania. TReadOnlyMappedFileStream nadal implementuje Read, Seek i Size jak każdy inny TStream, Seek ogranicza logiczną pozycję do zakresu pliku, a Write zawsze zwraca 0, ponieważ źródło jest otwierane tylko do odczytu
Otwieranie PDF-a przez widok mapowany w Delphi
Dwa jawne punkty wejścia otwierają źródło mapowane i żaden nie zmienia zachowania używanych już punktów wejścia. LoadFromMappedFile ładuje i wybiera dokument, a DAOpenMappedFile zwraca uchwyt Direct Access do tego samego pliku — właśnie ten tryb jest potrzebny przy scalaniu i dzieleniu wielogigabajtowych PDF-ów przez Direct Access. LoadFromFile i DAOpenFile zachowują dotychczasową semantykę współdzielenia pliku, błędów i zgodności, więc nic nie zmienia się dla wywołujących, którzy nie włączą nowej ścieżki. Oba mapowane punkty wejścia przyjmują żądany WindowSize w bajtach i maskę bitową Options, a dla każdego z nich akceptują wartość 0
var
Pdf: TPDFlib;
Payload: AnsiString;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
// WindowSize 0 wybiera domyślne 64 MiB; mapowanie jest tutaj wymagane
if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
[Pdf.LastErrorCode]);
// Odroczone wyodrębnianie przechodzi teraz po oknach mapowania zamiast wykonywać seek
Payload := Pdf.GetEmbeddedFileContentToString(1);
if Pdf.GetMappedFileInfo(Info) = 1 then
Writeln(Info);
finally
Pdf.Free;
end;
end;
Co naprawdę wymusza PDF_MAPPED_FILE_REQUIRE_MAPPING?
PDF_MAPPED_FILE_REQUIRE_MAPPING zamienia cichy fallback w natychmiastową, możliwą do zdiagnozowania awarię w chwili otwarcia. Przy Options pozostawionym na 0 oba punkty wejścia akceptują fallback do strumienia pliku tylko do odczytu: jeśli platforma nie ma kodu mapowania albo wywołanie mapowania się nie powiedzie, dokument nadal się otwiera, a każdy odczyt przechodzi przez zwykły strumień pliku. Po ustawieniu flagi PDFlibPas akceptuje wejście tylko wtedy, gdy utworzono pierwszy widok, i zgłasza odmowę przez LastErrorCode 401 zamiast ładować dokument, który po cichu zachowywałby się dokładnie jak stara ścieżka
W systemie Windows strumień mapowany otwiera drugi uchwyt tylko do odczytu z FILE_SHARE_READ, FILE_SHARE_WRITE i FILE_SHARE_DELETE oraz FILE_FLAG_RANDOM_ACCESS, tworzy na nim mapowanie PAGE_READONLY i mapuje pierwsze okno w konstruktorze. Wczesne wykonanie mapowania jest sednem rozwiązania: awaria typu „mapowanie wymagane” pojawia się w LoadFromMappedFile, a nie przy pierwszym leniwym odczycie obiektu w połowie zadania renderowania. Trzeba jednak jasno określić granicę gwarancji. Kod mapowania jest kompilowany wyłącznie dla celów Windows, a plik o rozmiarze zero nigdy nie próbuje mapowania, więc PDF_MAPPED_FILE_REQUIRE_MAPPING jest żądaniem, które może prawidłowo zakończyć się niepowodzeniem, a nie przenośną obietnicą. Ujemny WindowSize albo dowolny bit w Options poza jedyną udokumentowaną wartością jest od razu odrzucany z tym samym błędem 401
Jedno okno remapowane do granularity alokacji
Zawsze zachowywany jest tylko jeden widok i właśnie to uniezależnia zużycie przestrzeni adresowej od rozmiaru pliku. WindowSize równe 0 wybiera 64 MiB, wartość poniżej systemowej granularity alokacji jest podnoszona do tej granularity, wartość powyżej 1 GiB jest ograniczana, a wynik zaokrąglany w górę do całkowitej liczby jednostek granularity — na Windows 65536 bajtów, chyba że GetSystemInfo zgłosi inną wartość dwAllocationGranularity. Gdy odczyt wypada poza bieżący widok, PDFlibPas go odmapowuje, wyrównuje żądany offset w dół do granularity i mapuje tam nowe okno. Ostatnie okno jest ograniczane do fizycznego rozmiaru pliku, więc widok nigdy nie wychodzi za koniec pliku
Pojedynczy odczyt może przejść przez dowolną liczbę okien: pętla kopiuje tyle, ile dostarcza bieżący widok, remapuje go i kontynuuje, a żądanie wychodzące poza koniec zwraca skróconą liczbę bajtów zamiast kończyć się błędem. PDFlibPas celowo nie przekazuje wskaźnika do widoku, ponieważ następny odczyt przekraczający granicę okna go unieważniłby i żaden wywołujący nie mógłby rozsądnie się przed tym zabezpieczyć. Zmapowane bajty są kopiowane bezpośrednio do buforów należących do parsera, co usuwa dodatkowy bufor wejściowy pliku i przełączanie pozycji, ale biblioteka nie obiecuje zero-copy dla końcowego magazynu parsera. Okienkowanie po stronie odczytu dobrze współpracuje też z zapisem, ponieważ przesuwanie referencji bajt po bajcie podczas szybkiego scalania PDF-a wypuszcza bajty obiektów, gdy mapowane źródło je dostarcza. Kompromis rozmiaru okna jest oczywisty: mniejsze okno zajmuje mniej przestrzeni adresowej i jest częściej remapowane, co zwykle jest właściwym wyborem w procesie 32-bitowym
Co chroni blokada i co raportuje GetMappedFileInfo
Jedna sekcja krytyczna obejmuje widok mapowany, kursor pliku używany przez fallback, logiczną pozycję i statystyki, a podział między dwie metody odczytu wynika z tego wprost. ReadAt pobiera blokadę i wywołuje wewnętrzny czytnik bez blokady; Read pobiera tę samą blokadę, wywołuje ten sam wewnętrzny czytnik dla bieżącej logicznej pozycji, a następnie ją przesuwa. Ponowne użycie funkcji wewnętrznej zamiast publicznej ReadAt zapobiega rekursywnemu blokowaniu, a utrzymanie blokady przez całą pętlę kopiowania zapewnia poprawność remapowania jednego okna przy równoległych wywołaniach. Przed portowaniem warto znać jeden szczegół Free Pascala: unit FPC Windows deklaruje własny rekord o nazwie TCriticalSection, dlatego pole i jego tworzenie muszą być zapisane jako SyncObjs.TCriticalSection. Delphi bez problemu kompiluje formę bez kwalifikatora, a FPC rozwiązuje ją do rekordu bez Create, Enter ani Leave
var
Pdf: TPDFlib;
Handle, PageRef: Integer;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
Handle := Pdf.DAOpenMappedFile('archive-2026.pdf', '',
16 * 1024 * 1024, PDF_MAPPED_FILE_REQUIRE_MAPPING);
if Handle = 0 then
Exit;
try
PageRef := Pdf.DAFindPage(Handle, 1);
Writeln(Pdf.DAExtractPageText(Handle, PageRef, 0));
// {"memoryMapped":true,"fileSize":...,"remapCount":...}
if Pdf.DAGetMappedFileInfo(Handle, Info) = 1 then
Writeln(Info);
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
memoryMappedma wartość false, gdy aktywny jest przenośny fallback do strumienia pliku, i jest jedynym polem dowodzącym, że mapowanie nigdy nie zostało ustanowionewindowSizeto efektywne wyrównane okno, a nie wartość żądana przez użytkownika, natomiastmappedBytesjest mniejsze od niego w końcowym okniemappedOffsetto wyrównany do alokacji początek zachowanego widoku albo -1, gdy żaden widok nie jest obecnie aktywnyreadCallszlicza pomyślne żądania odczytu mieszczące się w zakresie,bytesReadzlicza bajty skopiowane do wywołujących, aremapCountobejmuje również widok początkowy
Ukierunkowane testy regresyjne obejmują absolutne odczyty przez granicę okna, zachowanie logicznego kursora, krótkie odczyty na końcu, nieprawidłowe offsety, odrzucone zapisy, remapowanie między rozdzielonymi oknami, odroczone wyodrębnianie nieskompresowanego załącznika o rozmiarze 220 KB oraz unieważnianie statystyk po DACloseFile; bezgłowe pakiety Win32 i Win64 wykryły po 1467 testów i przeszły je wszystkie bez pominiętych, nieudanych, błędnych ani przeciekających wyników. Jeśli pracujesz z wielogigabajtowymi PDF-ami w Delphi lub C++Builder, a profiler nadal wskazuje odczyty pliku zamiast parsowania, mapowane punkty wejścia warto zmierzyć przez jedno popołudnie, a GetMappedFileInfo powie, czy rzeczywiście uzyskano mapowanie. Pełna dokumentacja API i wersja trial znajdują się na stronie biblioteki PDF dla Delphi PDFlibPas