Technický článek

Slučování a rozdělování gigabajtových PDF v Delphi přes PDF Library for Delphi

Sloučit nebo rozdělit dvougigabajtové PDF tím zjevným způsobem vás stojí dvě věci naráz: reálný čas a adresní prostor. Ten zjevný způsob zní: načti každý vstup, odveď práci, zapiš výstup. Rozbíjí se to u načítání. Archiv skenů, který přejde z 300 na 600 DPI, zdvojnásobí lineární rozlišení a na disku objem zhruba zečtyřnásobí, takže tatáž kompletační úloha, jež celý rok zvládala soubory o 400 MB, začne šrotovat ve chvíli, kdy vstup překročí gigabajt, často aniž by dělala víc, než že počítá stránky. Úloha sama se nijak nezkomplikovala. Otevřít, spočítat, vybrat rozsahy, spojit, to je celé. Načítání celého stromu prostě při té velikosti přestalo být rozumným výchozím řešením. PDF Library for Delphi, knihovna PDF od losLab pro Delphi a C++Builder, na to odpovídá vrstvou Direct Access: rodinou funkcí s předponou DA, za nimiž stojí proudový čtecí modul procházející tabulku křížových odkazů na místě namísto stavby celého dokumentu v paměti

Kam se při plném načtení poděje paměť

Načíst PDF „normálně“ znamená rozebrat xref, vyřešit každý nepřímý objekt do stromu v paměti, dekódovat objektové streamy a zapojit strom stránek, písma i anotace do objektů, s nimiž lze manipulovat. Pro editační postupy je to správný obchod. Pro slučování, dělení a inspekci je to většinou plýtvání. Archiv skenů o 30 000 stránkách může držet miliony nepřímých objektů a úloha dělení jich potřebuje přečíst pár set: uzly stránek v požadovaném rozsahu plus to, na co tyto uzly odkazují

Vrstva Direct Access model obrací. DAOpenFile a DAOpenFileReadOnly rozeberou trailer a xref, tedy pár kilobajtů na konci souboru, a vrátí handle souboru. Objekty se natahují líně, až když je nějaké volání potřebuje. Praktickým důsledkem je, že otevření mnohagigabajtového souboru trvá zhruba stejně dlouho jako otevření malého a spotřeba paměti sleduje to, čeho se dotknete, ne to, co soubor obsahuje

Srovnání PDF Library for Delphi: načtení gigabajtového PDF do úplného stromu objektů v paměti proti otevření s přímým přístupem, kde parsování zastane u traileru a xref a handle obsluhuje líné čtení po objektech
Úplné načtení dekóduje každý nepřímý objekt dřív, než může sloučení začít, takže RAM a čas otevření rostou s archivem. Cesta přímého přístupu vrátí funkční handle po přečtení kilobajtů a nechává každé volání táhnout jen objekty, které potřebuje

Sondáž obřího souboru bez jeho načtení

Vzor níže pochází z benchmarku velkých souborů dodávaného s knihovnou: otevřít jen pro čtení, klást otázky, zavřít. Žádný strom dokumentu nikdy nevznikne

var
  Lib: TPDFlib;
  Handle, Pages: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Handle := Lib.DAOpenFileReadOnly('archive-2025.pdf', '');
    if Handle = 0 then
      raise Exception.Create('Direct access open failed');
    Pages := Lib.DAGetPageCount(Handle);
    Writeln('pages : ', Pages);
    Writeln('title : ', Lib.DAGetInformation(Handle, 'Title'));
    Lib.DACloseFile(Handle);
  finally
    Lib.Free;
  end;
end;

Režim jen pro čtení stojí za to upřednostnit vždy, když to jde: umožní příjmové fázi běžet i ve chvíli, kdy soubor drží jiné procesy, a dokumentuje záměr. Sondovací fáze, která omylem zavolá měnící funkci, selže rychle, místo aby archiv poškodila

PageRef je handle objektu, ne číslo stránky

Naprosto nejčastější chybou u DA API je předání čísla stránky tam, kde funkce očekává PageRef. Téměř každé volání DA pracující s jednou stránkou bere referenční handle objektu stránky, nikoli číslo stránky: DAExtractPageText, DARenderPageToFile, DARotatePage i DACapturePage očekávají ref. Získáte jej převodem lidsky srozumitelného čísla přes DAFindPage:

PDF Library for Delphi: Tok překladu čísla strany na PageRef ukazující DAFindPage živící volání přímého přístupu na stranu, proti surové celočíselné referenci dopadající na libovolný objekt a produkující tichý text ze špatné strany
Každé volání přímého přístupu na stránku spotřebuje PageRef vyrobenou DAFindPage, nikdy číslo určené lidem. Vynechání tohoto převodu nechá celé číslo vydávat se za id objektu a text ze špatné stránky může odjet neviditelně
PageRef := Lib.DAFindPage(Handle, 250);          // číslo stránky -> handle objektu
if PageRef <> 0 then
begin
  Text := Lib.DAExtractPageText(Handle, PageRef, 0);
  Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;

Předání surového čísla 250 místo toho nevyvolá chybu. Zaadresuje jakýkoli objekt, který za tou hodnotou handlu shodou okolností sedí, což v dobrý den viditelně selže a ve špatný den vytáhne text z nesprávné stránky do dokumentu určeného zákazníkovi. Pokud si vrstvu DA obalíte vlastním servisním kódem, znemožněte přeskočení převodu: na hranici přijímejte čísla stránek, hned volejte DAFindPage a uvnitř předávejte už jen refy

Slučování stovek souborů pomocí pojmenovaného seznamu

U dvou souborů stačí MergeFiles(First, Second, Output). Dávkové kompletování se lépe škáluje přes seznamy souborů: zaregistrujte vstupy pod názvem seznamu a pak seznam sloučte jedním průchodem

PDF Library for Delphi: Workflow pojmenovaného seznamu souborů: výpisy za leden, únor a březen se registrují pod jedním názvem seznamu a sloučí v jednom průchodu, přičemž varianty Fast, výchozí a strict obětují zachování stromu struktury za rychlost
Stovky registrovaných vstupů se sloučí do jediného průchodu MergeFileList, jehož výsledek se ověří v milisekundách dalším read-only probe. Varianta je rozhodnutí pro každou pipeline zvlášť, protože Fast zahodí strom struktury Tagged PDF
Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');

// Ověřte výsledek levnou cestou: znovu přímý přístup
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);

Rodina slučovacích funkcí má tři varianty a rozdíl není jen v rychlosti. MergeFileListFast vynechá zachování stromu struktury; MergeFileListStrict vynucuje striktní režim; verze bez přípony je vyvážené výchozí nastavení. Provozní pravidlo, které z toho plyne: je-li kterýkoli vstup Tagged PDF, jehož struktura pro zpřístupnění musí přežít, tedy typicky cokoli vyráběného pro PDF/UA, sáhněte po výchozí nebo striktní variantě, protože Fast strom struktury tiše zahodí. U prostých archivů skenů bez tagování je Fast výkon zadarmo. Rozhodujte se podle pipeline, ne podle nálady vývojáře, a použitou variantu zapište do protokolu úlohy

Dělení bez načítání: extrakce rozsahů

Dělení sleduje tutéž filozofii bez načítání. ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) vytáhne rozsah stránek přímo ze souboru do souboru, se seznamem rozsahů jako '1-500', '501-1000' nebo s výběry oddělenými čárkami, a ze zdroje se nikdy nestane strom dokumentu. Když už je dokument z jiných důvodů načtený, ExtractPageRanges vytvoří z toho aktuálního nový dokument v paměti a CopyPageRanges přetáhne rozsahy z jiného načteného dokumentu podle jeho ID. Pro dělení konsolidovaných tiskových dávek na jednotlivé výpisy je právě forma soubor-do-souboru ta, která zabrání 4 GB vstupu nafouknout se do RAM

Soubory, které lžou o své geometrii

Pipeline pro velké soubory potkávají poškozené soubory v míře, jakou pipeline pro malé soubory nikdy nezažijí, prostě proto, že vstupy prošly více systémy. Dva tvary selhání si zaslouží výslovné ošetření

Zaprvé posunuté hlavičky. Poštovní brány a tiskové spoolery někdy před PDF předřadí bajty, takže značka %PDF už nesedí na offsetu 0 a každý offset v xref je o tutéž hodnotu vedle. Proudový čtecí modul to rozpozná a zveřejní (DAShiftedHeader na ploché úrovni, ShiftedHeader u TSmartPDFReader) a při čtení to kompenzuje. Doma spočítaná aritmetika offsetů to obvykle nedělá, a proto je klasickým příznakem věta „funguje na každém souboru, který vyrobíme, a padá na souborech od zákazníka X“

Zadruhé rozbité tabulky křížových odkazů. DACopyFile(InputFileName, OutputFileName, PageCount) proudově přenese celý soubor do nové kopie a přitom xref přestaví, přičemž jako vedlejší produkt vrátí počet stránek. Zařadíte-li ji jako normalizační fázi před vybíravého odběratele dále v řetězci, promění se tím celá třída nahodilých chyb při parsování v jeden předvídatelný opravný krok. A když je potřeba uložit vaše vlastní úpravy, DAAppendFile je zapíše jako inkrementální aktualizaci, tedy připojí novou revizi místo přepisu gigabajtů, čímž udrží náklady ukládání úměrné změně, a ne velikosti souboru

Detaily doručení: linearizace a kompozice

Pipeline pro velké soubory dotvářejí dvě sousední schopnosti. Když se sestavený výstup servíruje přes HTTP k prohlížení v prohlížeči, LinearizeFile jej přeorganizuje pro proudové čtení po bajtových rozsazích, takže se první stránka zobrazí dřív, než se dostáhne zbytek 500 MB balíku. Spouštějte to jako poslední fázi, až po veškerém slučování, protože každá pozdější úprava soubor opět delinearizuje. A když balíky vyžadují kompozici, ne prosté spojení, řekněme krycí list orazítkovaný pod každý výpis nebo dvě zdrojové stránky umístěné na jeden výstupní list, promění DACapturePage libovolnou stránku ve znovupoužitelnou šablonu, kterou DADrawCapturedPage umístí na cílovou stránku do libovolného obdélníku, stále bez plného načtení mnohagigabajtového zdroje

Limity a co zůstává jen pro čtení

Formát sám dojde k hranicím dávno před Direct Access. Offsety jsou napříč celou vrstvou DA typu Int64, takže skutečnými stropy jsou dostupné místo na disku a desetimístné pole offsetu v klasických (nestreamových) tabulkách křížových odkazů. Archivy skenů o mnoha gigabajtech jsou v praxi nezajímavá rutina a paměť zůstává ohraničená bez ohledu na velikost souboru, protože objekty se čtou jen tehdy, když si o ně nějaké volání řekne

Dvě otázky padají dost často na to, aby si zasloužily přímou odpověď. Slučování výchozí cestou přenáší strukturu dokumentu, takže záložky i odkazy přežijí; varianta Fast je ta, která strom struktury vyměňuje za rychlost, což je celý důvod, proč ji vyhradit netagovaným vstupům. Bezpečným zvykem je sloučený výstup otevřít, projít jeho osnovu a namátkově zkontrolovat pár vnitřních odkazů, než jej vypustíte. A pokud jde o úpravy: mezi sondáží jen pro čtení a plným načtením leží užitečná střední cesta. Operace na úrovni stránky pracují přímo s handlem, mimo jiné DARotatePage, DAMovePage a DAHidePage, spolu se čtením polí formuláře, a DAAppendFile tyto úpravy uloží jako inkrementální revizi. Editace na úrovni obsahu, tedy cokoli, co přepisuje vykreslovací operátory uvnitř stránky, stále patří do vrstvy plného dokumentu

Související články

Musí-li váš sloučený výstup zůstat přístupný, pozadí ke stromu struktury najdete v článku o zpřístupnění pomocí Tagged PDF, který přesně vysvětluje, co by varianta slučování Fast zahodila. K vytahování obsahu z rozsahů, které rozdělíte, se hodí průvodce extrakcí textu, obrázků a písem

Kompletní seznam funkcí Direct Access se dodává spolu s knihovnou; edice a zkušební verze ke stažení najdete na produktové stránce PDF Library for Delphi