Technický článek

Zpracování velkých PDF v Delphi pomocí HotPDF Direct File API

Spočítat stránky v 1,4GB naskenovaném archivu by mělo být levné. Zavolejte na ten soubor LoadFromFile a levné to být přestane: HotPDF naparsuje data cross-reference a pro každý z několika set tisíc nepřímých objektů dokumentu sestaví objekt v paměti, a 32bitový worker narazí na strop adresního prostoru 2 GB někde uprostřed tohoto parsování. Operace, kterou jste chtěli, počet stránek, žádný z těchto objektů vůbec nepotřebovala. Potřebovala strom stránek a nic jiného. Tato mezera, mezi tím, co si úloha žádá, a tím, co dodá plné načtení, je celý důvod, proč Direct File API existuje

Direct File API dává Delphi a C++Builderu přístup k PDF na úrovni souboru: počty stránek, kopie, dešifrování, přírůstková připojení, to vše čte z disku jen to, co skutečně potřebuje, místo aby v RAM rekonstruovalo celý model dokumentu. Dovednost spočívá v tom spárovat každou úlohu s nejlehčí úrovní, která ji dokáže vyřešit. Trefíte-li se, služba drží plochou spotřebu paměti bez ohledu na velikost vstupu. Netrefíte-li se, první předimenzovaný soubor worker položí

Diagram směrování úloh pro HotPDF Direct File API v Delphi: sondy handle jen pro čtení, kopírování a šifrování celého souboru, inkrementální připojení a úplná načtení do paměti, seřazená podle paměťového profilu
Přiřaďte každé operaci nejlehčí úroveň, která ji dokáže obsloužit, a držte rezidentní paměť rovnou bez ohledu na velikost vstupu

Co vás stojí plné načtení

LoadFromFile není nepřítel. Svou paměť si zaslouží: jakmile je strom v RAM, máte náhodný přístup ke každé stránce a každému objektu, což je přesně to, co vyžadují InsertPagesFromDocument, MovePage a opětovná serializace přes SaveLoadedDocument. Pro skutečné přestavování neexistuje zkratka; abyste dokument mohli přeuspořádat, musíte jej držet celý

Potíže začínají ve chvíli, kdy velikost vstupu nemáte pod kontrolou. Nahrávky od zákazníků, výstupy skenerů a archivy staré deset let ignorují cokoli, co předpokládal váš testovací korpus. Načítejte každý vstup bezpodmínečně a váš strop paměti určí jediný největší soubor, který kdy někdo pošle. Doba parsování sleduje počet objektů a rezidentní paměť se ustálí na násobku velikosti souboru, jakmile se započtou struktury objektů a dekódované streamy, takže gigabajt na disku může znamenat několik gigabajtů v paměti

Rekompilace pro 64 bitů zvedne strop adresního prostoru, ale účet ponechá nedotčený. Worker stále spálí sekundy CPU a v RAM násobek velikosti souboru, aby zodpověděl otázku, na kterou by struktura samotného souboru mohla odpovědět za milisekundy. Při souběžnosti se matematika obrátí proti vám: čtyři velká načtení běžící najednou sdílí jeden rozpočet paměti a propustnost se propadne přesně ve chvíli, kdy je fronta nejhlubší a vy si to nejméně můžete dovolit

Čtení souboru přes handle

Úroveň jen pro čtení otevře soubor jako handle, zodpoví strukturální otázky o něm a zavře jej. Žádný strom objektů, žádné vykreslování stránek, žádná paměť, která by rostla se vstupem

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;

Tuto úroveň udržují poctivou tři návyky. Zaprvé kontrolujte návratovou hodnotu. Nekladný handle znamená, že otevření selhalo, a vypálit DAGetPageCount na mrtvý handle je typ chyby, která zůstane skrytá až do dne, kdy zákazník pošle poškozený soubor. Zadruhé párujte každé úspěšné otevření s DACloseFile uvnitř bloku finally; služba, která uniká handly, nespadne, jen prohnívá, což je horší. Zatřetí respektujte, co parametr hesla skutečně dělá. DAOpenFileReadOnly jej přijímá, ale u šifrovaných vstupů potichu sklouzne k plnému parsování jen kvůli přečtení počtu stránek, takže záruka ploché paměti se vypaří. Chráněné soubory nejdřív pošlete přes DecryptFile a zbytek pipeline zůstane levný

Stejná sonda zároveň slouží jako třídicí brána. Soubory se objevují chybně označené, nahrané jen z poloviny, nebo přejmenované z úplně jiného formátu, a kontrola DAOpenFileReadOnly to všechno odmítne hned u vstupních dveří během milisekund, s chybou přišpendlenou k viníkovi. Alternativou je nechat odpadní soubor projet hluboko do workeru fronty a nechat jej vybuchnout tam, kde rozmotání, který vstup to způsobil, může stát celé odpoledne

Kopírování, dešifrování a šifrování celých souborů

Druhá úroveň přesouvá a transformuje celé soubory, aniž by kdy odhalila jejich vnitřek. Toto jsou volání, o která se příjmové pipeline opírají nejvíc

// Strukturální kopie: ověření a přesun bez parsování stromu objektů
Status := Pdf.DACopyFile('incoming\statement.pdf', 'verified\statement.pdf');
LogDirectFileStatus('copy', Status);

// Dešifrování během kopírování: cesta Direct File k chráněným vstupům
Status := Pdf.DecryptFile('incoming\protected.pdf',
  'verified\plain.pdf', 'batch-password');
LogDirectFileStatus('decrypt-copy', Status);

// Šifrování během kopírování: ochrana výstupu bez plného načtení
Status := Pdf.EncryptFile('verified\statement.pdf',
  'outbound\statement.pdf', 'owner-secret', '', aes256, [prPrint]);
LogDirectFileStatus('encrypt-copy', Status);

Každé volání si svoje místo zaslouží. DACopyFile je ověřená kopie z karanténního adresáře do spravovaného úložiště: cestou otevírá a indexuje strukturu PDF, takže useknutý nebo ne-PDF vstup selže rovnou tady, ne o tři fáze dál po proudu. DecryptFile zapisuje dešifrovanou kopii po přímé přepisovací cestě AES-256, která se vyhýbá stromu objektů, kdykoli to vstup dovolí, tedy velkosouborový protějšek k toku dešifrování load-and-resave popsanému v článku o šifrování AES-256. EncryptFile běží stejný pohyb obráceně a při kopírování na úrovni souboru aplikuje ochranu heslem s parametry typu klíče a oprávnění, které už používá cesta v paměti

Připojování změn místo přepisování

Přírůstková aktualizace, definovaná v ISO 32000-1 §7.5.6, je třetí úroveň. Původní bajty zůstávají na disku tam, kde jsou, a jakékoli nové nebo změněné objekty se připojí za ně, následované čerstvou sekcí cross-reference, která se zřetězí zpět do originálu. U 900MB archivu, kterému se má přidat jediná stránka, je cenou za zápis jen delta, ne celý soubor

Anatomie inkrementální aktualizace PDF produkované HotPDF: původní bajty zůstávají nedotčené, připojuje se delta nových objektů a zřetězená sekce křížových odkazů a dřívější revize zůstávají obnovitelné, dokud je úplný přepis SaveLoadedDocument nezahodí
Přírůstková uložení připojí jen svou deltu a zachovají předchozí revize, takže kompaktace je samostatný úmyslný přepis
// Připojí auditní stránku k velkému archivu bez jeho přepsání
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');  // původní bajty + delta

Zde záleží na dvou bodech disciplíny. BeginIncrementalUpdate musí ukazovat na původní soubor, protože připojená data cross-reference se zpětně zřetězují na offsety bajtů uvnitř něj. A model je záměrně append-only: každé přírůstkové uložení soubor zvětší, nikdy jej nezmenší. Dokument orazítkovávaný každou noc poroste bez omezení, dokud jej periodická opětovná serializace, tedy načtení a zápis zpět přes SaveLoadedDocument, nezhutní. Stejná append-only povaha je tím, co dělá z přírůstkové aktualizace jediný bezpečný způsob, jak se dotknout digitálně podepsaného dokumentu, což je omezení rozebrané v článku o digitálních podpisech a PAdES. Mechanismus cross-reference, který za tím stojí, dostává vlastní zpracování v článku o object streamech a přírůstkových aktualizacích

V append-only uloženích je past, která proklouzne kolem většiny revizí. Původní bajty zůstávají v souboru, čitelné pro každého, kdo je ochotný se podívat. Přírůstková aktualizace, která stránku „nahradí", tu starou nesmaže; jen ji v aktuální revizi překryje, zatímco předchozí revize tam zůstává, plně obnovitelná. Přírůstkové aktualizace jsou proto špatný nástroj na odstraňování citlivého obsahu. Abyste opravdu zahodili historii, kterou by příjemce neměl nikdy vidět, potřebujete plnou opětovnou serializaci: LoadFromFile následované SaveLoadedDocument, které zapíše jen aktuální stav a pohřbené revize nechá za sebou

Přiřazení úrovně k operaci

Logika výběru je dost krátká na to, aby se vešla do hlavy, a vyplatí se ji zakódovat jako explicitní směrovací rozhodnutí na začátku pipeline, místo aby si každá úloha improvizovala vlastní cestu. Úroveň určuje operace, kterou potřebujete:

  • Počítání, kontrola nebo klasifikace otevře handle: DAOpenFileReadOnly, DAGetPageCount, DACloseFile
  • Přesun, dešifrování nebo šifrování celého souboru zůstává na úrovni souboru s DACopyFile, DecryptFile nebo EncryptFile
  • Přestavování stránek nebo slučování dokumentů potřebuje plné načtení: LoadFromFile, pak InsertPagesFromDocument nebo MovePage, pak SaveLoadedDocument
  • Přidání malé delty k obřímu nebo podepsanému souboru volá BeginIncrementalUpdate a uloží

Smíšené pipeline udělají dobře, když před cestu plného načtení postaví práh podle velikosti. Cokoli přes pár set megabajtů posílejte přes úrovně Direct File a plné načtení si rezervujte pro skutečné přestavování na 64bitovém workeru se skutečným rozpočtem paměti. Práh promění pád kvůli nedostatku paměti na směrovací rozhodnutí, které vidíte a můžete ladit

Ať už úlohu obsluhuje kterákoli úroveň, zapisujte její výstup pod dočasným názvem a přejmenujte jej na místo teprve poté, co výsledek projde ověřením. Napůl zapsaný soubor sedící pod finálním názvem vypadá pro další fázi pipeline úplně stejně jako dobrý, a volání Direct File dělají kontrolu levnou: ověření výstupu je jednořádková sonda pomocí handle

Direct File API je součástí HotPDF Delphi Component pro Delphi a C++Builder. Produktová stránka odkazuje na úplnou referenci funkcí, včetně zde ukázaných volání pro přírůstkovou aktualizaci