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 در بر دارد
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 میگیرید:
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 کنید
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 هستند