Technical Article

Didelių (gigabaitinių) PDF failų sujungimas ir skaidymas „Delphi“ aplinkoje su „PDFlibPas“ Direct Access

Didelio (pavyzdžiui, 2 gigabaitų) PDF failo sujungimas arba skaidymas įprastu būdu kainuoja du dalykus vienu metu: vykdymo laiką ir adresų erdvę. Įprastas būdas yra įkelti kiekvieną įvesties failą, atlikti veiksmus ir įrašyti išvestį. Būtent įkėlimo metu viskas sugenda. Skenuotų dokumentų archyvas, kurio raiška padidinama nuo 300 iki 600 DPI, padvigubina linijinę raišką ir maždaug keturis kartus padidina failo dydį diske. Todėl ta pati programa, kuri visus metus sėkmingai apdorojo 400 MB failus, pradeda lėtėti arba strigti, kai įvesties failas viršija gigabaitą – dažnai vien bandant suskaičiuoti puslapius. Pati užduotis netapo sudėtingesnė: atidaryti, suskaičiuoti, pasirinkti diapazonus ir sujungti yra visa esmė. Pilnas dokumento medžio įkėlimas į atmintį tiesiog nustojo būti protingu numatytuoju pasirinkimu esant tokiam failo dydžiui. „PDFlibPas“ – „losLab“ PDF biblioteka, skirta „Delphi“ ir „C++Builder“, išsprendžia šią problemą naudodama tiesioginės prieigos (Direct Access) sluoksnį: funkcijų šeimą su DA priešdėliu, kurią palaiko srautinis skaitytuvas, nuskaitantis kryžminių nuorodų lentelę (xref) vietoje, užuot kūręs visą dokumento struktūrą atmintyje.

Kur sunaudojama atmintis pilno įkėlimo metu

Įkelti PDF failą „įprastai“ reiškia išanalizuoti xref struktūrą, paversti kiekvieną netiesioginį objektą į medį atmintyje, dešifruoti objektų srautus bei susieti puslapių medį, šriftus ir anotacijas į manipuliuojamus objektus. Redagavimo procesams tai yra teisingas pasirinkimas. Tačiau failų sujungimui, skaidymui ir patikrai tai dažniausiai yra išteklių švaistymas. Skenuotų dokumentų archyvas, turintis 30 000 puslapių, gali apimti milijonus netiesioginių objektų, o skaidymo užduočiai tereikia perskaityti kelis šimtus iš jų: puslapių mazgus (nodes) pasirinktame diapazone ir tai, į ką šie mazgai nurodo.

Tiesioginės prieigos sluoksnis apverčia šį modelį. Metodai DAOpenFile ir DAOpenFileReadOnly išanalizuoja tik trailer ir xref dalis – kelis kilobaitus failo pabaigoje – ir grąžina failo rankeną (handle). Objektai nuskaitomi vėluojančiu būdu (lazily) tik tada, kai konkrečiam iškvietimui jų prireikia. Praktinė pasekmė: didelio failo atidarymas užtrunka tiek pat, kiek mažo, o atminties sąnaudos priklauso tik nuo to, prie kurių dalių prisiliečiate, o ne nuo viso failo dydžio.

Didelio failo tyrimas be jo įkėlimo

Žemiau pateiktas šablonas paimtas iš pačios bibliotekos didelių failų spartos testo: atidaryti tik skaitymui, užklausti duomenų, uždaryti. Joks dokumentų medis atmintyje taip ir nesukuriamas.

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;

Kai tik įmanoma, verta rinktis režimą „tik skaitymui“: tai leidžia vykdyti patikros žingsnį, kol kiti procesai naudoja failą, ir aiškiai apibrėžia kodo paskirtį. Tyrimo žingsnis, kuris netyčia bando iškviesti keičiančiąją (mutating) funkciją, iškart praneš apie klaidą, užuot sugadinęs archyvą.

PageRef yra objekto rankena, o ne puslapio numeris

Dažniausia klaida naudojant tiesioginės prieigos (DA) API yra puslapio numerio perdavimo ten, kur funkcija tikisi PageRef. Beveik kiekvienas su puslapiais susijęs DA iškvietimas priima nuorodos rankeną į puslapio objektą, o ne puslapio numerį: DAExtractPageText, DARenderPageToFile, DARotatePage ir DACapturePage tikisi nuorodos. Ją gausite išvertę naudotojui matomą puslapio numerį per DAFindPage:

PageRef := Lib.DAFindPage(Handle, 250);          // page number -> object handle
if PageRef <> 0 then
begin
  Text := Lib.DAExtractPageText(Handle, PageRef, 0);
  Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;

Perdavus tiesioginį skaičių 250, klaidos pranešimas nebus išmestas. Sistema kreipsis į bet kurį objektą, kurio rankenos reikšmė atitinka šį skaičių – geriausiu atveju tai pasibaigs akivaizdžia klaida, o blogiausiu – išgaus tekstą iš neteisingo puslapio ir pateiks jį klientui. Jei apgaubiate DA sluoksnį savo tarnybos kode, padarykite šį vertimą privalomu: priimkite puslapių numerius ties riboženkliu, iškart iškvieskite DAFindPage ir viduje naudokite tik gautas nuorodas (refs).

Šimtų failų sujungimas naudojant pavadintą sąrašą

Dviejų failų sujungimui pakanka funkcijos MergeFiles(First, Second, Output). Grupiniam failų sujungimui labiau tinka failų sąrašai: užregistruokite įvesties failus tam tikru sąrašo pavadinimu, o tada sujunkite visą sąrašą vienu žingsniu.

Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');

// Verify the result the cheap way: direct access again
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);

Sujungimo funkcijų šeima turi tris variantus, ir skirtumas yra ne tik greitis. Metodas MergeFileListFast praleidžia struktūros medžio (structure tree) perkėlimą; MergeFileListStrict įjungia griežtą režimą, o versija be papildomo sufikso yra subalansuotas numatytasis pasirinkimas. Iš to seka taisyklė: jei kurio nors įvesties failo struktūra yra Tagged PDF, kurios prieinamumo (accessibility) medį būtina išsaugoti (pavyzdžiui, kuriant PDF/UA dokumentus), pasirinkite numatytąjį arba Strict variantą, nes Fast variantas tyliai pašalina struktūros medį. Paprastiems skenuotų dokumentų archyvams be papildomo žymėjimo Fast suteikia nemokamos spartos. Priimkite šį sprendimą kiekvienam konvejeriui atskirai ir užrašykite pasirinktą variantą užduočių žurnale.

Skaidymas be įkėlimo: diapazono išgavimas

Skaidymas remiasi tuo pačiu principu be dokumento įkėlimo. Metodas ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) perkelia puslapių diapazoną tiesiai iš vieno failo į kitą, naudojant diapazono sąrašą (pvz., '1-500', '501-1000' ar kableliais atskirtas reikšmes), o šaltinis niekada nevirsta dokumentų medžiu atmintyje. Kai dokumentas jau yra įkeltas dėl kitų priežasčių, ExtractPageRanges sukuria naują dokumentą atmintyje iš esamo, o CopyPageRanges nukopijuoja diapazonus iš kito įkelto dokumento pagal ID. Vykdant periodinių ataskaitų skaidymą iš didelių spausdinimo srautų, failo-failo (file-to-file) forma yra ta, kuri apsaugo nuo 4 GB failo išsipūtimo RAM atmintyje.

Failai su klaidinga geometrija

Didelių failų apdorojimo sistemos su pažeistais failais susiduria kur kas dažniau nei mažų failų sistemos, nes failai pereina per daugiau sistemų. Dvi klaidų formos nusipelno atskiro apdorojimo.

Pirma – pastumtos antraštės (shifted headers). Pašto serveriai ir spausdinimo procesai kartais prideda baitų PDF failo pradžioje, todėl %PDF žymė nebėra ties poslinkiu 0, o kiekvienas xref poslinkis faile tampa klaidingas tuo pačiu dydžiu. Srautinis skaitytuvas tai aptinka ir parodo (DAShiftedHeader plokščiame lygmenyje, ShiftedHeader komponente TSmartPDFReader) bei kompensuoja šį nuokrypį nuskaitymo metu. Pačių sukurta poslinkių aritmetika to paprastai nedaro – būtent dėl to kyla problemos tipo „veikia su visais mūsų sukurtais failais, bet sugenda su failais iš kliento X“.

Antra – sugadintos kryžminių nuorodų lentelės. Metodas DACopyFile(InputFileName, OutputFileName, PageCount) nukreipia visą failą į naują kopiją ir kartu iš naujo sukuria xref lentelę, o kaip papildomą rezultatą grąžina puslapių skaičių. Šios funkcijos vykdymas kaip normalizavimo žingsnio priešais reiklią sistemą paverčia atsitiktines analizės klaidas vienu nuspėjamu taisymo žingsniu. O kai reikia išsaugoti jūsų pačių atliktus pakeitimus, DAAppendFile įrašo juos kaip prieauginį atnaujinimą (incremental update) – tai reiškia naujos revizijos pridėjimą failo gale, užuot perrašius gigabaitus duomenų, todėl įrašymo laikas priklauso tik nuo pakeitimų dydžio, o ne nuo viso failo.

Pateikimo detalės: linearizavimas ir kompozicija

Dvi papildomos galimybės užbaigia didelių failų apdorojimo konvejerį. Kai sujungta išvestis pateikiama per HTTP peržiūrai naršyklėje, LinearizeFile perorganizuoja ją baitų diapazono srautiniam perdavimui (byte-range streaming), kad pirmasis puslapis būtų parodytas dar nebaigus siųsti likusios 500 MB failo dalies. Vykdykite tai kaip paskutinį etapą, po visų sujungimų, nes bet koks vėlesnis pakeitimas vėl išderins linearizavimą. O kai reikalinga puslapių kompozicija (pavyzdžiui, titulinis puslapis po kiekvienos ataskaitos arba du šaltinio puslapiai viename lape), DACapturePage paverčia bet kurį puslapį daugkartiniu šablonu, kurį DADrawCapturedPage įterpia į tikslinį puslapį bet kokiame stačiakampyje – vis dar be pilno dokumento įkėlimo iš didelio šaltinio failo.

Apribojimai ir kas lieka tik skaitymui

Du klausimai iškyla pakankamai dažnai, kad į juos būtų atsakyta tiesiogiai. Sujungimas numatytuoju keliu perkelia dokumento struktūrą, todėl išlieka žymės (bookmarks) ir nuorodos; Fast variantas yra tas, kuris paaukoja struktūros medį vardan spartos – būtent todėl jį reikėtų naudoti tik failams be papildomo žymėjimo. Saugus įprotis – atidaryti sujungtą išvestį, pereiti per jos struktūrą ir atsitiktine tvarka patikrinti kelias vidines nuorodas prieš išsiunčiant failą. Kalbant apie redagavimą: yra patogus tarpinis variantas tarp tyrimo tik skaitymui ir pilno dokumento įkėlimo. Puslapio lygio operacijos veikia tiesiogiai su rankena (tarp jų DARotatePage, DAMovePage ir DAHidePage, taip pat formų laukų skaitymas), o DAAppendFile išsaugo šiuos pakeitimus kaip prieauginę reviziją. Turinio lygio redagavimas, t. y. bet kas, kas keičia pačius puslapio braižymo operatorius, vis dar priklauso pilno dokumento lygmeniui.

Susiję straipsniai

Jei sujungta išvestis privalo išlikti prieinama (accessible), struktūros medžio pagrindai aprašyti straipsnyje Tagged PDF prieinamumo struktūra, kuriame paaiškinta, ką Fast sujungimo variantas pašalintų. Norėdami išgauti turinį iš išskaidytų diapazonų, skaitykite teksto, paveikslėlių ir šriftų išgavimo vadovą.

Pilnas tiesioginės prieigos funkcijų sąrašas platinamas kartu su biblioteka; versijas ir bandomuosius failus rasite puslapyje PDFlibPas produkto puslapyje.