Teknisk artikel

HotPDF sidextraheringsprestanda i Delphi

Två minuter för att kopiera tre sidor från en 40-sidig PDF är inte ett problem med prestandajustering. Det är en signal om att fel API-väg används. När jag först såg denna tidsåtgång på ett sidkopieringsexempel i HotPDF Component var min instinkt att titta på dokumentstrukturen först och koden sedan. Den ordningen visade sig spela roll

Vad som faktiskt var långsamt

PDF:en ifråga var ett 40-sidigt referensdokument med ett icke-trivialt sidträd: multipla mellanliggande /Pages-noder istället för en enda platt array. Den ursprungliga exempelkoden anropade LoadFromFile, byggde sedan ett nytt dokument med BeginDoc, loopade över valda sidnummer, och vid varje iteration laddades källdokumentet igen från disken för att dra en sida. Det är den fulla tolkningskostnaden multiplicerad med hur många sidor du vill ha. En fil på 12 MB slog mot disken sex gånger för en tresidig extrahering eftersom ingen tittade på huruvida filen behövde förbli öppen över iterationerna

Den andra bidragande faktorn var osynlig i koden: HotPDF:s LoadFromFile löser upp hela korsreferenstabellen och dekomprimerar varje objektström vid inläsning. Det är rätt beteende för ett dokument du är på väg att modifiera, men det är mer arbete än du behöver om du bara vill ha sidantal och en delmängd av sidorna. För skrivskyddad tillgång till struktur undviker DAOpenFileReadOnly att deserialisera det fulla objektträdet, vilket spelar roll på komprimerade filer med stora bildresurser

Ingen av dessa är en biblioteksbugg. Båda är anropare som väljer ett API designat för ett jobb och använder det för ett annat

Att använda InsertPagesFromDocument för sidextrahering

Den rätta vägen för att kopiera ett intervall av sidor från ett HotPDF-dokument in i ett annat är InsertPagesFromDocument, anropad efter LoadFromFile på källan. Du laddar källan en gång, laddar eller skapar destinationen en gång, flyttar sidorna, och sparar. Källan stannar i minnet över alla sidinsättningarna:

procedure ExtractPages(const SourceFile, DestFile: string;
  const PageRange: string);
var
  Source, Dest: THotPDF;
begin
  Source := THotPDF.Create(nil);
  Dest   := THotPDF.Create(nil);
  try
    // Load source once: full parse happens here and only here
    Source.LoadFromFile(SourceFile);

    // Build a minimal destination document
    Dest.FileName := DestFile;
    Dest.BeginDoc;

    // Copy the requested range; '1-3' inserts pages 1 through 3
    // starting at position 1 in the destination
    Dest.InsertPagesFromDocument(Source, PageRange, 1);

    Dest.EndDoc;
  finally
    Source.Free;
    Dest.Free;
  end;
end;

Parametern PageRange accepterar samma format som kommandoradsexemplet: en kommaseparerad lista med sidnummer eller intervall såsom '1-3' eller '1,5,7-9'. Sidor är 1-baserade. InsertPagesFromDocument kopierar innehållsströmmar, resursordböcker och sidgeometri utan att röra metadata, bokmärken eller inbäddade filbilagor såvida de inte refereras från de kopierade sidorna. För en tresidig extrahering från ett 40-sidigt dokument är det ett litet working set

Tidsåtgången på samma 12 MB fil som tidigare kördes i två minuter: under 1,5 sekunder med detta mönster. Merparten av den tiden är det enskilda anropet LoadFromFile. Dokumentstrukturen är irrelevant när objekttabellen är upplöst första gången

När LoadFromFile är för mycket: Direct File API

Om du bara behöver räkna sidor, inspektera dokumentinformation, eller kopiera en fil utan att röra dess innehåll, undviker Direct File API hela tolkningen fullständigt. DAOpenFileReadOnly mappar korsreferenstabellen utan att dekomprimera objektströmmar, så sidräkning är O(xref storlek) snarare än O(filstorlek):

procedure InspectPDF(const FileName: string);
var
  Pdf: THotPDF;
  Handle, PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Handle := Pdf.DAOpenFileReadOnly(FileName, '');
    if Handle <= 0 then
      Exit;
    try
      PageCount := Pdf.DAGetPageCount(Handle);
      Writeln('Pages: ', PageCount);

      // DACopyFile is a byte-preserving copy, no re-serialization
      Pdf.DACopyFile(FileName, 'archive-copy.pdf');
    finally
      Pdf.DACloseFile(Handle);
    end;
  finally
    Pdf.Free;
  end;
end;

Brasklappen: DAOpenFileReadOnly accepterar en lösenordsparameter men faller tillbaka till en full tolkning för krypterad inmatning, eftersom dekryptering kräver att objektträdet löser upp krypteringsordboken. Om dina källfiler är krypterade, dekryptera dem först med DecryptFile för att få en okrypterad kopia, öppna sedan den med Direct File API. Filnivå-funktionen DecryptFile tar en direkt AES-256 omskrivningsväg för standardkryptering och är snabbare än LoadFromFile följt av SaveLoadedDocument för stora filer, eftersom den inte bygger upp den fulla in-memory objektmodellen

Minne vid batchbearbetning

Batchjobb som bearbetar dussintals filer i en loop har ett mönster som ser korrekt ut men ackumulerar minne: skapa THotPDF inuti loopen, anropa LoadFromFile, utföra arbete, anropa Free. Det är strukturellt okej. Problemet uppstår när det inre arbetet allokerar temporära objekt (scratch objects), fångar undantag och lämnar dessa temporära objekt levande på felvägar. Delphis minneshanterare komprimerar inte, så hundra felvägs-läckor över en batchkörning kan driva upp minnet tillräckligt högt för att bromsa allokering för allt annat

Fixen är inte exotisk. Varje THotPDF och varje mellanliggande TStream eller TBitmap som deltar i PDF-arbete hör hemma i ett try/finally-block där Free är den sista satsen. Sätt lokala pekare till nil före tryfinally-grenen säkert kan använda if Assigned(x) then x.Free när initieringen misslyckas halvvägs. Detta är standard Delphi ägarskapsdisciplin och det är hela historien för den här klassen av problem

En till sak att kontrollera i batchsammanhang: AddImage registrerar bilder i en intern lista som består under hela livslängden för THotPDF-instansen. Om du återanvänder en och samma instans över många dokument genom att anropa LoadFromFile upprepade gånger, stannar bildregistreringar från tidigare dokument kvar i listan. Antingen skapar du en färsk instans per dokument eller anropar sökvägen för att rensa bildlistan mellan dokument

Att mäta innan du ändrar något

Innan du sträcker dig efter något av dessa mönster, mät. Delphis TStopwatch från System.Diagnostics lindar (wraps) QueryPerformanceCounter och är tillräckligt exakt för wall-clock profilering av fil-I/O. Linda LoadFromFile ensamt och se hur lång tid det står för. Om det är 90 % av den totala tiden, är lösningen Direct File API eller att minska antalet gånger du tolkar samma fil. Om det är under 20 % ligger flaskhalsen någon annanstans och du jagar fel sak

Tvåminuters-extraheringen som startade detta inlägg visade sig vara helt och hållet mönstret med upprepade inladdningar. Dokumentstrukturen bidrog inte med något; ett platt sidträd skulle ha körts på samma sätt. Att byta till en enda LoadFromFile följd av ett anrop av InsertPagesFromDocument drog ner det till 1,3 sekunder på samma hårdvara utan att röra något annat

De sidmanipulerings-API:er som visas här är en del av HotPDF Component för Delphi och C++Builder