PDFlibPas kan öppna en lokal PDF genom en begränsad skrivskyddad minnesmappad vy: LoadFromMappedFile och DAOpenMappedFile håller exakt ett skjutande fönster över filen, mappar om det vid behov och levererar varje objektutdrag genom läsningar på absoluta offset. Delphi PDF Library håller aldrig hela källan i minnet, så adressutrymmesanvändningen förblir plan när filen växer. Konstruktionen är gjord för en enda arbetsbelastning: gigabyte-stora PDF-filer där parsern har avslutat inläsningen men fortfarande går tillbaka till disken, objekt för objekt och strömfragment för strömfragment
Varför förblir glesa läsningar dyra efter att PDF-filen har lästs in?
Att läsa in en PDF innebär inte att all läsning är klar, och i en fil på flera gigabyte är det där skillnaden kostar tid. En korsreferenstabell eller korsreferensström (ISO 32000-1 §7.5.4 och §7.5.8) anger bara var varje indirekt objekt börjar. Bytena hämtas senare när en sida renderas, ett teckensnittsprogram avkodas eller en inbäddad filström (ISO 32000-1 §7.11.4) extraheras. Ett arkiv på 2 GB med tiotusentals objekt blir tiotusentals små, oordnade läsningar, och ingen av dem är känd vid inläsningstillfället
Den väg läsningarna tidigare tog var ett delat Seek följt av Read på en positionsbunden ström, och den fallerar åt två håll samtidigt. Varje fragment betalar för en filläsning även när sidan redan finns i operativsystemets cache, och markören är delat föränderligt tillstånd, så en lokal fil och byteintervallskällan bakom progressiv PDF-intervallinläsning med förhämtning kunde inte köra samma parserkod utan att slåss om positionen. PDFlibPas löser båda delarna genom att göra läsning på absolut offset till ett kontrakt i stället för en optimering
Vad garanterar TPDFReadAtStream?
TPDFReadAtStream garanterar en läsning på en absolut offset som varken beror på eller stör den logiska strömmarkören. Det är en abstrakt TStream-ättling med exakt en virtuell metod, och båda marköroberoende källorna i biblioteket härstammar från den: TReadOnlyMappedFileStream för lokala filer och TByteRangeStream för fjärrkällor som serveras via intervall. Läsaren av objektutdrag frågar en gång om källan är en TPDFReadAtStream och faller tillbaka till den gamla sekvensen seek-och-läs när den inte är det, så en vanlig filström eller minnesström fortsätter att fungera oförändrad
type
// Skrivskyddade strömmar vars absoluta läsningar undviker ett delat Seek plus Read
TPDFReadAtStream = class(TStream)
public
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; virtual; abstract;
end;
// Fönsterbaserad skrivskyddad åtkomst till en lokal fil
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;
Skillnaden betyder mer än signaturen antyder. ReadAt använder offseten den får och lämnar Position exakt där den var, vilket låter nästlade parsernivåer göra läsningar utan en spara-och-återställ-rörelse runt varje anrop. TReadOnlyMappedFileStream implementerar fortfarande Read, Seek och Size som vilken annan TStream som helst, Seek klampar den logiska positionen till filen och Write returnerar alltid 0 eftersom källan öppnas skrivskyddad
Öppna en PDF genom en mappad vy i Delphi
Två uttryckliga ingångspunkter öppnar en mappad källa, och ingen av dem ändrar beteendet hos de ingångspunkter du redan använder. LoadFromMappedFile läser in och väljer ett dokument; DAOpenMappedFile returnerar ett Direct Access-handtag över samma fil, vilket är läget du vill ha när du slår ihop och delar gigabyte-stora PDF-filer genom Direct Access. LoadFromFile och DAOpenFile behåller sin fildelning samt sina fel- och kompatibilitetssemantiker oförändrade, så ingenting flyttar sig för anropare som inte väljer det nya läget. Båda mappade ingångspunkterna tar en begärd WindowSize i byte och en Options-bitmask, och båda accepterar 0 för endera
var
Pdf: TPDFlib;
Payload: AnsiString;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
// WindowSize 0 väljer standardvärdet 64 MiB; mappning krävs här
if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
[Pdf.LastErrorCode]);
// Fördröjd extrahering går nu genom mappade fönster i stället för seek
Payload := Pdf.GetEmbeddedFileContentToString(1);
if Pdf.GetMappedFileInfo(Info) = 1 then
Writeln(Info);
finally
Pdf.Free;
end;
end;
Vad tvingar PDF_MAPPED_FILE_REQUIRE_MAPPING faktiskt fram?
PDF_MAPPED_FILE_REQUIRE_MAPPING omvandlar en tyst reservväg till ett omedelbart, diagnostiserbart fel vid öppningen. När Options lämnas på 0 accepterar båda ingångspunkterna en skrivskyddad filströmsreservväg: om plattformen saknar mappningskod eller om mappningsanropet misslyckas öppnas dokumentet ändå och varje läsning går via en vanlig filström. När flaggan är satt accepterar PDFlibPas bara indata när den första vyn har etablerats, och rapporterar vägran via LastErrorCode 401 i stället för att läsa in ett dokument som i tysthet fungerar exakt som den gamla vägen
I Windows öppnar den mappade strömmen ett andra skrivskyddat handtag med FILE_SHARE_READ, FILE_SHARE_WRITE och FILE_SHARE_DELETE samt FILE_FLAG_RANDOM_ACCESS, skapar en PAGE_READONLY-mappning över det och mappar det första fönstret i konstruktorn. Att mappa ivrigt är hela poängen: ett fel av typen mapping required syns vid LoadFromMappedFile, inte vid den första lata objektläsningen halvvägs genom en renderingsuppgift. Var tydlig med var garantin upphör. Mappningskoden kompileras bara för Windows-mål, och en fil med noll byte försöker aldrig mappa något alls, så PDF_MAPPED_FILE_REQUIRE_MAPPING är en begäran som legitimt kan misslyckas, inte ett portabelt löfte. Ett negativt WindowSize eller en bit i Options utöver det dokumenterade värdet avvisas direkt med samma fel 401
Ett fönster, ommappat till allokeringsgranulariteten
Endast en vy behålls någonsin, och det är det som gör adressutrymmesanvändningen oberoende av filstorleken. Ett WindowSize på 0 väljer 64 MiB; ett värde under systemets allokeringsgranularitet höjs till den; ett värde över 1 GiB kapas; och resultatet rundas upp till ett helt antal granularitetsenheter, 65536 byte i Windows om inte GetSystemInfo rapporterar ett annat dwAllocationGranularity. När en läsning hamnar utanför den aktuella vyn avmappar PDFlibPas den, justerar den begärda offseten nedåt till en granularitetsgräns och mappar ett nytt fönster där. Det sista fönstret klampar mot den fysiska filstorleken, så vyn sträcker sig aldrig förbi filens slut
En enskild läsning kan korsa hur många fönster som helst: loopen kopierar det som den aktuella vyn kan leverera, mappar om och fortsätter, och en begäran som löper utanför slutet returnerar ett kort antal i stället för att misslyckas. Det PDFlibPas medvetet inte gör är att ge dig en pekare in i vyn, eftersom nästa läsning över ett fönster gör den ogiltig och ingen anropare rimligen kan skydda sig mot det. Mappade byte kopieras direkt till parserägda destinationsbuffertar, vilket tar bort den extra filindatabufferten och positionsväxlingen, men biblioteket gör inget anspråk på nollkopiering för den slutliga parserlagringen. Fönsterläsning samverkar även med skrivsidan, eftersom bytebaserad referensförskjutning under en snabb PDF-sammanslagning strömmar ut objektbyte medan den mappade källan strömmar in dem. Avvägningen för fönsterstorleken är uppenbar: ett mindre fönster använder mindre adressutrymme och mappar om oftare, vilket vanligtvis är rätt val i en 32-bitarsprocess
Vad skyddar låset, och vad rapporterar GetMappedFileInfo?
Ett enda kritiskt avsnitt täcker den mappade vyn, reservfilmarkören, den logiska positionen och statistiken, och uppdelningen mellan de två läsmetoderna följer direkt av det. ReadAt tar låset och anropar den låsfria interna läsaren; Read tar samma lås, anropar samma interna läsare vid den aktuella logiska positionen och flyttar sedan fram den. Att återanvända den interna funktionen i stället för den publika ReadAt undviker rekursiv låsning, och att hålla låset under hela kopieringsloopen är det som gör en ommappning till ett enda fönster korrekt vid samtidiga anrop. En Free Pascal-detalj är värd att känna till före en portning: FPC-enheten Windows deklarerar en egen post med namnet TCriticalSection, så fältet och dess konstruktion måste skrivas som SyncObjs.TCriticalSection. Delphi kompilerar den okvalificerade formen utan problem; FPC löser den till en post utan Create, Enter eller 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;
memoryMappedär false när den portabla filströmsreservvägen är aktiv, och det är det enda fältet som bevisar att ingen mappning någonsin etableradeswindowSizeär det effektiva justerade fönstret snarare än värdet du begärde, ochmappedBytesär mindre än det i slutfönstretmappedOffsetär den allokeringsjusterade starten för den kvarhållna vyn, eller -1 när ingen vy är aktiv för tillfälletreadCallsräknar lyckade läsbegäranden inom intervallet,bytesReadräknar byte som kopierats till anropare ochremapCountinkluderar den första vyn
Riktade regressioner täcker absoluta läsningar över fönstergränser, bevarad logisk markör, korta läsningar vid slutet, ogiltiga offset, avvisade skrivningar, ommappning mellan åtskilda fönster, fördröjd extrahering av en 220 KB inkompressibel bilaga och statistik som blir ogiltig efter DACloseFile; de huvudlösa Win32- och Win64-sviterna upptäckte vardera 1467 tester och klarade alla utan ignorerade, misslyckade, felaktiga eller läckta resultat. Om du arbetar med gigabyte-stora PDF-filer i Delphi eller C++Builder och profileraren fortsätter att peka på filläsningar i stället för parsing är de mappade filingångspunkterna värda en eftermiddag av mätningar, och GetMappedFileInfo talar om ifall du faktiskt fick en mappning. Den fullständiga API-referensen och en testversion finns på sidan för PDFlibPas Delphi PDF Library