مقاله فنی

merge و split کردن PDFهای gigabyteی در Delphi با PDF Library for Delphi

merge یا split کردن یک PDF دوگیگابایتی به راه بدیهی، هم‌زمان دو چیز برایتان هزینه دارد: wall-clock time و address space. راه بدیهی این است که هر input را load کنید، کار را انجام دهید، output را بنویسید. loading جایی است که می‌شکند. یک scan archive که از ۳۰۰ به ۶۰۰ DPI حرکت می‌کند resolution خطی‌اش دو برابر و تقریباً روی disk چهار برابر می‌شود، پس همان job assembly که تمام سال fileهای ۴۰۰ مگابایتی را handle می‌کرد، لحظه‌ای که یک input از یک گیگابایت عبور کند شروع به thrash کردن می‌کند، اغلب در حالی که بیشتر از count کردن pageها کاری نمی‌کند. task هرگز سخت‌تر نشد. open، count، pick range، concatenate تمام آن است. full-tree loading ساده‌اند در آن size یک default معقول بودن را متوقف کرد. PDF Library for Delphi، کتابخانهٔ PDF شرکت losLab برای Delphi و C++Builder، با لایهٔ Direct Access خود به این پاسخ می‌دهد: یک خانواده از functionهای دارای prefix DA که پشتیبانی می‌شوند توسط یک streaming reader که cross-reference table را در جای خود طی می‌کند به‌جای ساختن کل document در memory

کجا memory در یک full load می‌رود

load کردن یک PDF به‌شکل «معمولی» یعنی parse کردن xref، resolve کردن هر indirect object به یک tree در-memory، decode کردن object streamها، و wiring کردن page tree، fontها و annotationها به objectهایی که می‌توانید manipulate کنید. برای workflowهای editing آن trade درست است. برای کار merge، split و inspection بیشترش waste است. یک scan archive ۳۰٬۰۰۰-pageی ممکن است میلیون‌ها indirect object نگه دارد، و یک job split نیاز دارد چند صدتایشان را بخواند: nodeهای page در range درخواست‌شده، به‌علاوهٔ هرآنچه آن nodeها reference می‌دهند

لایهٔ Direct Access مدل را معکوس می‌کند. DAOpenFile و DAOpenFileReadOnly trailer و xref را، چند کیلوبایت در انتهای file، parse می‌کنند و یک file handle برمی‌گردانند. objectها به‌طرز تنبل fetch می‌شوند وقتی یک call به آن‌ها نیاز دارد. نتیجهٔ عملی این است که باز کردن یک file چندگیگابایتی تقریباً به همان اندازهٔ باز کردن یک file کوچک طول می‌کشد، و memory آنچه را touch می‌کنید track می‌کند نه آنچه file در بر دارد

مقایسه PDF Library for Delphi بین بارگذاری یک PDF گیگابایتی در درخت شیء کاملاً درون‌حافظه‌ای و باز کردن آن با دسترسی مستقیم، که در آن تجزیه در trailer و xref متوقف می‌ماند و یک handle خواندن‌های تنبل تک‌شیء را سرو می‌کند
بارگذاری کامل هر شیء غیرمستقیم را پیش از شروع ادغام دیکود می‌کند، پس RAM و زمان باز شدن با آرشیو مقیاس می‌یابد. مسیر دسترسی مستقیم پس از خواندن چند کیلوبایت یک هندلِ کارا برمی‌گرداند و به هر فراخوانی اجازه می‌دهد فقط اشیایی را که نیاز دارد بکشد

probing کردن یک file عظیم بدون load کردنش

pattern زیر از benchmark بزرگ-file خود کتابخانه می‌آید: read-only باز کنید، سوال بپرسید، close کنید. هیچ document treeای هرگز وجود ندارد

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;

حالت read-only هرگاه بتوانید ارزش ترجیح‌داشتن دارد: به stage intake اجازه می‌دهد در حالی اجرا شود که processهای دیگر file را نگه داشته‌اند، و intent را مستند می‌کند. یک stage probe که تصادفاً یک function mutating صدا می‌زند به‌جای خراب‌کردن archive، fast fail می‌شود

PageRef یک object handle است، نه یک page number

اشتباه رایج‌ترین با DA API، پاس‌کردن یک page number آنجا است که یک function یک PageRef انتظار دارد. تقریباً هر DA call per-page یک reference handle به page object به‌جای یک page number می‌گیرد: DAExtractPageText، DARenderPageToFile، DARotatePage و DACapturePage همگی یک ref انتظار دارند. یکی را با ترجمهٔ number رو‌به‌انسان از طریق DAFindPage می‌گیرید:

PDF Library for Delphi: جریان ترجمه شماره صفحه به PageRef که DAFindPage فراخوانی‌های دسترسی مستقیم تک‌صفحه را تغذیه می‌کند، در برابر ref عددی خام که روی شیء دلخواه می‌نشیند و متن اشتباه بی‌صدا در صفحه نامربوط تولید می‌کند
هر فراخوانی دسترسی مستقیمِ هر-صفحه یک PageRef تولیدشده توسط DAFindPage را مصرف می‌کند، هرگز عددِ رو به انسان را نه. پریدن از آن ترجمه باعث می‌شود عدد صحیح خود را id شیء جا بزند و متن صفحه اشتباه می‌تواند نادیدنی ارسال شود
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;

پاس‌کردن number خام ۲۵۰ به‌جای آن errorای raise نمی‌کند. به هر objectای که اتفاقاً پشت آن handle value نشسته address می‌کند، که یک روز خوب به‌طور مرئی fail می‌شود و یک روز بد text را از page اشتباه به یک document رو‌به‌مشتری extract می‌کند. اگر لایهٔ DA را در code service خودتان wrap می‌کنید، ترجمه را غیرممکن به skip کردن کنید: page numberها را در boundary بپذیرید، DAFindPage را فوراً صدا بزنید، و فقط refها را در داخل پاس بدهید

merge کردن صدها file با یک list نام‌دار

برای دو file، MergeFiles(First, Second, Output) کافی است. assembly batchی بهتر از طریق file listها scale می‌شود: inputها را زیر یک list name ثبت کنید، سپس list را در یک pass merge کنید

PDF Library for Delphi: گردش کار فهرست فایل نام‌دار که در آن صورت‌حساب‌های ژانویه، فوریه و مارس زیر یک نام فهرست ثبت و در یک گذر ادغام می‌شوند، با واریانت‌های Fast و پیش‌فرض و strict که حفظ درخت ساختار را با سرعت معامله می‌کنند
صدها ورودی ثبت‌شده در یک گذر واحد MergeFileList فرو می‌ریزند که نتیجه‌اش در چند میلی‌ثانیه از طریق پروب دیگری فقط‌خواندنی راستی‌آزمایی می‌شود. انتخاب واریانت تصمیمی خط‌لوله‌ای است، چون Fast درخت ساختار Tagged PDF را کنار می‌گذارد
Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');

// نتیجه را به روش ارزان بررسی کن: دوباره direct access
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);

خانوادهٔ merge سه variant دارد، و تفاوت فقط speed نیست. MergeFileListFast از حفظ structure tree عبور می‌کند؛ MergeFileListStrict حالت strict را enforce می‌کند؛ version بدون suffix، default متعادل است. rule عملیاتی که بیرون می‌افتد: اگر هر inputای یک Tagged PDF است که structure accessibilityاش باید دوام بیاورد، هر چیزی برای PDF/UA تولیدشده مورد بدیهی، به default یا variant Strict بروید، چون Fast بی‌صدا structure tree را drop می‌کند. برای scan archiveهای plain بدون tagging، Fast performance رایگان است. per pipeline تصمیم بگیرید، نه per developer mood، و variant استفاده‌شده را در job log ضبط کنید

split کردن بدون loading: range extraction

split کردن از همان philosophy بدون-load پیروی می‌کند. ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) یک page range را مستقیم از file به file می‌کشد، با یک range list مانند '1-500'، '501-1000'، یا selectionهای comma-separated، و source هرگز یک document tree نمی‌شود. وقتی یک document به دلایل دیگر از قبل loaded است، ExtractPageRanges یک document در-memory جدید از فعلی تولید می‌کند، و CopyPageRanges rangeها را از یک document loaded دیگر بر اساس ID می‌کشد. برای splitting per-statement از print streamهای consolidated، form file-to-file آنی است که یک input ۴ گیگابایتی را از inflate شدن به RAM نگه می‌دارد

fileهایی که دربارهٔ geometry خود دروغ می‌گویند

pipelineهای بزرگ-file با fileهای آسیب‌دیده با نرخی برخورد می‌کنند که pipelineهای کوچک-file هرگز نمی‌بینند، صرفاً چون inputها از systemهای بیشتری عبور می‌کنند. دو شکل شکست ارزش handling صریح دارند

اول، headerهای shift‌شده. mail gatewayها و print spoolerها گاهی byteهایی به یک PDF prepend می‌کنند، پس marker %PDF دیگر در offset ۰ نمی‌نشیند و هر offset در xref در file به همان مقدار اشتباه است. streaming reader این را detect می‌کند و expose می‌کند (DAShiftedHeader در level flat، ShiftedHeader روی TSmartPDFReader)، سپس در طول read برایش جبران می‌کند. arithmetic offset خانگی معمولاً نه، که چرایی آن است که «روی هر fileای که تولید می‌کنیم کار می‌کند، روی fileهای از مشتری X fail می‌شود» symptom کلاسیک است

دوم، cross-reference tableهای شکسته. DACopyFile(InputFileName, OutputFileName, PageCount) کل file را به یک copy جدید stream می‌کند در حالی که xref را rebuild می‌کند، و page count را به‌عنوان یک by-product برمی‌گرداند. اجرای آن به‌عنوان یک stage normalization در مقابل یک consumer downstream بدطیناب، یک دسته از parse failureهای متناوب را به یک step repair قابل‌پیش‌بینی تبدیل می‌کند. و وقتی editهای خودتان نیاز به save دارند، DAAppendFile آن‌ها را به‌عنوان یک incremental update می‌نویسد، یک revision جدید append می‌کند به‌جای rewrite کردن گیگابایتها، که cost save را متناسب با change نگه می‌دارد به‌جای file

detailهای delivery: linearization و composition

دو capability مجاور یک pipeline بزرگ-file را کامل می‌کنند. وقتی output assembled روی HTTP برای viewing در-browser serve می‌شود، LinearizeFile آن را برای streaming byte-range بازسازمان می‌کند تا اولین page قبل از اینکه بقیهٔ یک packet ۵۰۰ مگابایتی download را تمام کرده باشد نمایش داده شود. آن را به‌عنوان stage نهایی، بعد از همهٔ merge کردن، اجرا کنید، چون هر modification بعدی file را دوباره de-linearize می‌کند. و وقتی packetها نیاز به composition به‌جای concatenation plain دارند، بگویید یک coversheet که پشت هر statement stamp می‌شود یا دو page منبع که روی یک output sheet impose می‌شوند، DACapturePage هر pageای را به یک template قابل‌استفاده‌مجدد تبدیل می‌کند که DADrawCapturedPage روی یک page مقصد در یک مستطیل دلخواه قرار می‌دهد، همچنان بدون یک full document load روی source چندگیگابایتی

limitها و آنچه read-only می‌ماند

format خودش پیش از Direct Access از فضا خارج می‌شود. offsetها کل راه از طریق لایهٔ DA، Int64 هستند، پس سقف‌های واقعی disk در دسترس و field offset در xref با ۱۰ رقم از cross-reference tableهای classic (non-stream) هستند. scan archiveهای چندگیگابایتی در عمل غیرقابل‌توجه‌اند، و memory فارغ از اندازهٔ file محدود می‌ماند چون objectها فقط وقتی یک call برایشان می‌پرسد خوانده می‌شوند

دو سوال به‌اندازهٔ کافی رایج پیش می‌آیند که مستقیماً پاسخ داده شوند. merge کردن از طریق path پیش‌فرض document structure را به آن سو carry می‌کند، پس bookmarkها و linkها دوام می‌آورند؛ variant Fast آنی است که structure tree را بهspeed معامله می‌کند، که کل دلیلی است که آن را برای inputهای untagged reserve می‌کنیم. habit امن این است که output merged را باز کنید، outlineاش را طی کنید، و چند link داخلی را پیش از ship کردن spot-check کنید. آن editing: یک middle ground مفید بین probing read-only و یک full load هست. operationهای page-level روی handle مستقیماً کار می‌کنند، DARotatePage، DAMovePage و DAHidePage در میان آن‌ها، به‌علاوهٔ readهای form-field، و DAAppendFile آن editها را به‌عنوان یک incremental revision persist می‌کند. editing در level-content، هرآنچه marking operatorهای داخل یک page را rewrite می‌کند، همچنان به لایهٔ document کامل تعلق دارد

مقاله‌های مرتبط

اگر output merged شما باید قابل‌دسترس بماند، background structure-tree در مقالهٔ accessibility در Tagged PDF پوشش داده شده، که دقیقاً توضیح می‌دهد variant merge Fast چه را discard می‌کرد. برای کشیدن content بیرون از rangeهایی که split می‌کنید، راهنمای استخراج text، image و font را ببینید

فهرست کامل functionهای Direct Access همراه با خود کتابخانه ship می‌شود؛ editionها و trial downloadها روی صفحهٔ محصول PDF Library for Delphi هستند