Műszaki cikk

Gigabájtos PDF-ek egyesítése és darabolása Delphiben

Egy kétgigabájtos PDF egyesítése vagy darabolása a kézenfekvő módon egyszerre két dolgot visz el: az órán mérhető időt és a címteret. A kézenfekvő mód az, hogy betölti mindegyik bemenetet, elvégzi a munkát, majd kiírja a kimenetet. A betöltésnél törik el. Egy szkennelt archívum, amely 300 DPI-ről 600 DPI-re vált, megkétszerezi a lineáris felbontását, és nagyjából a négyszeresére hízik a lemezen, így ugyanaz az összeállítási feladat, amely egész évben elbírt a 400 MB-os fájlokkal, vergődni kezd abban a pillanatban, amint egy bemenet átlépi a gigabájtot — gyakran miközben nem csinál mást, mint oldalakat számol. A feladat nem lett nehezebb. Megnyitás, számolás, tartományválasztás, összefűzés: ennyi az egész. A teljes fabetöltés ekkora méretnél egyszerűen megszűnt épeszű alapértelmezés lenni. A PDF Library for Delphi, a losLab Delphihez és C++Builderhez készült PDF-könyvtára, a Direct Access rétegével válaszol erre: DA előtagú függvények családjával, amelyek mögött folyamszerű olvasó áll, és ez helyben járja be a kereszthivatkozási táblát ahelyett, hogy a teljes dokumentumot felépítené a memóriában

Hová tűnik a memória egy teljes betöltésnél?

Egy PDF „normális” betöltése azt jelenti, hogy elemzi az xref-et, minden közvetett objektumot memóriabeli fává old fel, dekódolja az objektumfolyamokat, majd az oldalfát, a betűkészleteket és a jegyzeteket kezelhető objektumokká drótozza össze. Szerkesztési munkafolyamatoknál ez a helyes alku. Egyesítéshez, daraboláshoz és vizsgálódáshoz viszont többnyire pazarlás. Egy 30 000 oldalas szkennelt archívum akár több millió közvetett objektumot is tarthat, egy darabolási feladatnak pedig ezek közül néhány százat kell elolvasnia: a kért tartományba eső oldalcsomópontokat, plusz mindazt, amire ezek hivatkoznak

A Direct Access réteg megfordítja a modellt. A DAOpenFile és a DAOpenFileReadOnly a trailert és az xref-et elemzi, azaz néhány kilobájtot a fájl végén, és egy fájlleírót ad vissza. Az objektumokat lustán olvassa be, amikor egy hívásnak szüksége van rájuk. Ennek gyakorlati következménye, hogy egy többgigabájtos fájl megnyitása körülbelül annyi ideig tart, mint egy kicsié, a memóriaigény pedig ahhoz igazodik, amit megérint, nem ahhoz, amit a fájl tartalmaz

A PDF Library for Delphi összehasonlítása arról, hogy egy gigabájtos PDF teljes memóriabeli objektumfává töltése miben tér el a közvetlen eléréses megnyitástól, ahol az elemzés a trailernél és az xref-nél megáll, és egy leíró szolgálja ki a lusta, objektumonkénti olvasásokat
A teljes betöltés minden közvetett objektumot dekódol, mielőtt az egyesítés elindulhatna, így a RAM és a megnyitási idő az archívummal együtt nő. A közvetlen eléréses út néhány kilobájt beolvasása után működő leírót ad vissza, és minden hívás csak a neki kellő objektumokat húzza be

Hogyan vizsgálhat meg egy hatalmas fájlt betöltés nélkül?

Az alábbi minta a könyvtár saját nagyfájlos teljesítménymérőjéből származik: nyissa meg csak olvasásra, tegye fel a kérdéseket, majd zárja be. Dokumentumfa soha nem jön létre

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;

A csak olvasható módot érdemes előnyben részesíteni, amikor csak lehet: így a beolvasási szakasz akkor is futhat, amikor más folyamatok fogják a fájlt, ráadásul dokumentálja a szándékot. Az a vizsgálati szakasz, amely véletlenül módosító függvényt hív, gyorsan elhasal ahelyett, hogy megrongálná az archívumot

A PageRef objektumleíró, nem oldalszám

A DA API-val kapcsolatos leggyakoribb hiba az, hogy oldalszámot adnak át ott, ahol a függvény PageRef értéket vár. Szinte minden oldalankénti DA-hívás az oldalobjektum hivatkozási leíróját kéri, nem az oldalszámot: a DAExtractPageText, a DARenderPageToFile, a DARotatePage és a DACapturePage mind hivatkozásra számít. Ilyet úgy kap, hogy az embernek szóló számot átfordítja a DAFindPage hívásán:

PDF Library for Delphi: az oldalszámból PageRef értékké alakítás folyamata, amelyben a DAFindPage látja el az oldalankénti közvetlen eléréses hívásokat, szemben azzal, amikor egy nyers egész szám hivatkozásként tetszőleges objektumra mutat és észrevétlenül rossz oldal szövegét adja
Minden oldalankénti közvetlen eléréses hívás a DAFindPage által előállított PageRef értéket fogyasztja, soha nem az embernek szóló számot. Ha kihagyja ezt az átfordítást, az egész szám objektumazonosítónak adja ki magát, és a rossz oldalról származó szöveg észrevétlenül eljuthat a kimenetbe
PageRef := Lib.DAFindPage(Handle, 250);          // oldalszám -> objektumleíró
if PageRef <> 0 then
begin
  Text := Lib.DAExtractPageText(Handle, PageRef, 0);
  Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;

Ha helyette a nyers 250-es számot adja át, az nem vált ki hibát. Azt az objektumot címzi meg, amely véletlenül az adott leíróérték mögött áll, ami jó napon látványosan elhasal, rossz napon viszont rossz oldalról nyer ki szöveget egy ügyfélnek szánt dokumentumba. Ha a DA réteget saját szolgáltatáskódba csomagolja, tegye kihagyhatatlanná az átfordítást: a határon fogadjon oldalszámokat, azonnal hívja a DAFindPage függvényt, és befelé már csak hivatkozásokat adjon tovább

Több száz fájl egyesítése névvel ellátott listával

Két fájlhoz elég a MergeFiles(First, Second, Output). A kötegelt összeállítás jobban skálázódik fájllistákon keresztül: regisztrálja a bemeneteket egy listanév alá, majd egyetlen menetben egyesítse a listát

PDF Library for Delphi: névvel ellátott fájllistás munkafolyamat, amelyben a januári, februári és márciusi kivonatok egyetlen listanév alá kerülnek és egy menetben egyesülnek, miközben a Fast, az alapértelmezett és a szigorú változat a struktúrafa megőrzését váltja sebességre
Több száz regisztrált bemenet egyetlen MergeFileList menetté olvad össze, amelynek eredménye ezredmásodpercek alatt ellenőrizhető egy újabb csak olvasható vizsgálattal. A változat választása munkafolyamatonkénti döntés, mert a Fast eldobja a Tagged PDF struktúrafáját
Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');

// Az eredmény olcsó ellenőrzése: ismét közvetlen elérés
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);

Az egyesítési családnak három változata van, és a különbség nem csupán a sebesség. A MergeFileListFast kihagyja a struktúrafa megőrzését, a MergeFileListStrict szigorú módot kényszerít ki, az utótag nélküli változat pedig a kiegyensúlyozott alapértelmezés. Az ebből adódó üzemeltetési szabály: ha bármelyik bemenet olyan Tagged PDF, amelynek akadálymentességi szerkezetét túl kell élnie a műveletnek — a PDF/UA célra készülő anyag a kézenfekvő eset —, nyúljon az alapértelmezett vagy a Strict változathoz, mert a Fast némán eldobja a struktúrafát. Címkézés nélküli, egyszerű szkennelt archívumoknál a Fast ingyen teljesítmény. Munkafolyamatonként döntsön, ne fejlesztői hangulat szerint, és jegyezze fel a használt változatot a feladatnaplóba

Darabolás betöltés nélkül: tartománykiemelés

A darabolás ugyanezt a betöltésmentes filozófiát követi. Az ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) közvetlenül fájlból fájlba húz ki egy oldaltartományt olyan tartománylistával, mint az '1-500', az '501-1000' vagy vesszővel elválasztott válogatások, a forrásból pedig soha nem lesz dokumentumfa. Ha egy dokumentum más okból már be van töltve, az ExtractPageRanges a jelenlegiből új memóriabeli dokumentumot állít elő, a CopyPageRanges pedig azonosító alapján egy másik betöltött dokumentumból emel át tartományokat. Összevont nyomtatási folyamok kivonatonkénti darabolásánál a fájlból fájlba forma az, amely megakadályozza, hogy egy 4 GB-os bemenet valaha is felfúvódjon a RAM-ba

Fájlok, amelyek hazudnak a saját felépítésükről

A nagyfájlos munkafolyamatok olyan gyakorisággal találkoznak sérült fájlokkal, amilyet a kisfájlos folyamatok soha nem látnak, egyszerűen azért, mert a bemenetek több rendszeren mennek keresztül. Két hibaforma külön kezelést érdemel

Először az eltolt fejlécek. A levelezési átjárók és a nyomtatási sorkezelők időnként bájtokat fűznek a PDF elejére, így a %PDF jelölő már nem a 0. eltolásnál ül, és a fájl minden xref-eltolása ugyanannyival hibás. A folyamszerű olvasó ezt észleli és jelzi (DAShiftedHeader a lapos szinten, ShiftedHeader a TSmartPDFReader osztályon), majd olvasás közben korrigál rá. A házilag írt eltolásszámítás jellemzően nem, és pontosan ezért klasszikus tünet a „minden általunk gyártott fájlon működik, az X ügyféltől jövőkön elhasal”

Másodszor a törött kereszthivatkozási táblák. A DACopyFile(InputFileName, OutputFileName, PageCount) a teljes fájlt új példányba folyamozza, miközben újraépíti az xref-et, és melléktermékként visszaadja az oldalszámot. Ha normalizáló szakaszként futtatja egy válogatós, alsóbb szintű fogyasztó elé, akkor a szeszélyes elemzési hibák egész osztályát alakítja át egyetlen kiszámítható javítási lépéssé. Ha pedig a saját szerkesztéseit kell mentenie, a DAAppendFile növekményes frissítésként írja ki őket, azaz új revíziót fűz hozzá ahelyett, hogy gigabájtokat írna újra, így a mentés költsége a változás, nem a fájl méretéhez igazodik

Kiszállítási részletek: linearizálás és összeállítás

Két szomszédos képesség teszi teljessé a nagyfájlos munkafolyamatot. Amikor az összeállított kimenetet HTTP-n át szolgálja ki böngészőbeli megtekintésre, a LinearizeFile bájttartományos folyamátvitelre rendezi át, így az első oldal még azelőtt megjelenik, hogy egy 500 MB-os csomag többi része letöltődött volna. Utolsó szakaszként futtassa, minden egyesítés után, mert bármely későbbi módosítás újra megszünteti a linearizálást. Ha pedig a csomagoknak puszta összefűzés helyett összeállításra van szükségük — mondjuk minden kivonat mögé bélyegzett fedőlapra vagy két forrásoldal egy kimeneti ívre helyezésére —, a DACapturePage bármely oldalt újrafelhasználható sablonná alakít, amelyet a DADrawCapturedPage tetszőleges téglalapba helyez el a céloldalon, továbbra is teljes dokumentumbetöltés nélkül a többgigabájtos forráson

Korlátok és ami csak olvasható marad

Maga a formátum jóval hamarabb fogy ki a helyből, mint a Direct Access. Az eltolások végig Int64 típusúak a DA rétegben, tehát a valódi plafon a rendelkezésre álló lemez és a klasszikus (nem folyam alapú) kereszthivatkozási táblák 10 jegyű xref-eltolásmezője. A többgigabájtos szkennelt archívumok a gyakorlatban nem számítanak különlegesnek, a memóriaigény pedig a fájlmérettől függetlenül korlátos marad, mert az objektumokat csak akkor olvassa be, amikor egy hívás kéri őket

Két kérdés jön elő elég gyakran ahhoz, hogy közvetlenül megválaszoljuk. Az alapértelmezett úton történő egyesítés átviszi a dokumentumszerkezetet, tehát a könyvjelzők és a hivatkozások túlélik, a Fast változat az, amely a struktúrafát váltja sebességre, és éppen ezért tartsa fenn címkézetlen bemenetekre. A biztonságos szokás az, hogy megnyitja az egyesített kimenetet, végigjárja a tartalomjegyzékét, és kiszállítás előtt szúrópróbaszerűen ellenőriz néhány belső hivatkozást. Ami a szerkesztést illeti: van egy hasznos középút a csak olvasható vizsgálat és a teljes betöltés között. Az oldalszintű műveletek közvetlenül a leírón dolgoznak — köztük a DARotatePage, a DAMovePage és a DAHidePage —, az űrlapmezők olvasásával együtt, a DAAppendFile pedig növekményes revízióként rögzíti ezeket a szerkesztéseket. A tartalomszintű szerkesztés, vagyis minden, ami az oldalon belüli rajzolási utasításokat írja újra, továbbra is a teljes dokumentumréteghez tartozik

Kapcsolódó cikkek

Ha az egyesített kimenetnek akadálymentesnek kell maradnia, a struktúrafa hátterét a Tagged PDF akadálymentességéről szóló cikk ismerteti, amely pontosan elmagyarázza, mit dobna el a Fast egyesítési változat. A szétdarabolt tartományokból való tartalomkinyeréshez lásd a szöveg-, kép- és betűkészlet-kinyerési útmutatót

A teljes Direct Access függvénylista a könyvtárral együtt érkezik; a kiadások és a próbaverziók a PDF Library for Delphi termékoldalán találhatók