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
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:
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
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