مقاله فنی

کولاژ اسکن دوطرفه در Delphi: ادغام تودرتوی PDF

CollateDocumentsEx در کتابخانه PDF دلفی PDFlibPas چند سند باز را در یک سند تودرتو ادغام می‌کند. در هر دور، GroupSize صفحه از هر منبع اضافه می‌کند، فهرست بازه صفحات به ازای هر منبع را می‌پذیرد و یک بازه نزولی مانند 3-1 را معکوس‌سازی آن منبع در نظر می‌گیرد. یک فراخوانی، یک دسته روی و یک دسته پشت معکوس‌شده را به ترتیب خواندنی تبدیل می‌کند

سناریوی پشت این API کاملاً عادی و بسیار رایج است. یک اسکنر ورق‌خور با مسیر یک‌طرفه کل دسته را رو به پایین اسکن می‌کند، سپس اپراتور دسته را برمی‌گرداند و دوباره اجرا می‌کند. نتیجه دو PDF است: صفحات رو به ترتیب، صفحات پشت به‌صورت معکوس. کاربر یک فایل واحد می‌خواهد: صفحه ۱ روی، صفحه ۱ پشت، صفحه ۲ روی و به همین ترتیب. این مقاله درباره مسئله ترتیب‌دهی و دام تکرار منابعی است که زیر آن نهفته. اگر دغدغه شما توان خام اتصال است، به ادغام سریع PDF با جابه‌جایی مرجع در سطح بایت مراجعه کنید؛ اگر ورودی‌ها آن‌قدر بزرگ هستند که در حافظه جا نمی‌شوند، به ادغام و تقسیم PDFهای گیگابایتی با دسترسی مستقیم مراجعه کنید

اسکنر دو دسته تولید می‌کند، یکی از آن‌ها برعکس

کولاژ (Collation) همان ادغام نیست. یک ادغام بازه‌های صفحه را به‌هم متصل می‌کند؛ یک کولاژ آن‌ها را تودرتو می‌کند، و الگوی تودرتوسازی ویژگی دستگاه فیزیکی‌ای است که ورودی را تولید کرده. اگر الگو اشتباه باشد، فایل کمی اشتباه نیست، اصلاً غیرقابل‌خواندن است: هر صفحه دوم متعلق به برگه دیگری است. سه متغیر تقریباً همه موارد واقعی را توصیف می‌کند: چند منبع در چرخش هستند، در هر دور چند صفحه از هر منبع می‌آید، و آیا لازم است یکی از منابع معکوس خوانده شود. CollateDocuments دو مورد اول را با یک آرایه ساده از handleهای سند و یک عدد صحیح GroupSize پوشش می‌دهد. CollateDocumentsEx مورد سوم را با پذیرفتن فهرستی از بازه‌های صفحه جدا شده با semicolon اضافه می‌کند، یک قطعه به ازای هر منبع، جایی که قطعه خالی یعنی همه صفحات آن منبع و یک بازه نزولی آن را معکوس می‌کند. هر دو تابع به انتهای سند فعلاً انتخاب‌شده اضافه می‌کنند و در صورت موفقیت ۱ و در صورت هرگونه رد شدن ۰ برمی‌گردانند

چرا کولاژ ساده‌لوحانه حجم فایل را چند برابر می‌کند؟

چون نگاشت import که شماره اشیاء منبع را به شماره اشیاء مقصد نگاشت می‌کند در هر فراخوانی کپی از نو ساخته می‌شود، و هرچیزی که از بیش از یک تکه قابل‌دسترسی باشد به ازای هر تکه یک‌بار import می‌شود. در داخل PDFlibPas، TPDFDocument.CopyPagesFromDoc در ابتدای هر فراخوانی NewIndObjList خود را ریست می‌کند. آن فهرست تنها حافظه‌ای است که کپی‌کننده از آنچه قبلاً منتقل کرده دارد. اگر یک‌بار با بازه ده‌صفحه‌ای فراخوانی شود، یک فونت مشترک بین هر ده صفحه یک‌بار embed می‌شود. اگر ده‌بار با یک صفحه در هر بار فراخوانی شود، همان فونت ده‌بار embed می‌شود. این موضوع برای اسکن‌ها بسیار مهم‌تر از اسناد متنی است، چون یک صفحه اسکن‌شده یک XObject تصویری بزرگ منفرد است و اشیاء مشترک همان‌هایی هستند که وزن واقعی دارند: یک پروفایل ICC embed شده، یک زنجیره /DecodeParms مشترک، یک form XObject مهر یا واترمارک که روی هر برگه اعمال شده، فونت لایه متن OCR. راه بدیهی برای نوشتن یک کولاژ round-robin یک حلقه روی دورها است، و آن حلقه دقیقاً همان حالت آسیب‌شناختی است

// Do not do this. Each CopyPageRanges call rebuilds the import map,
// so anything the two sources share internally is imported once per
// round instead of once per source.
var
  RoundIndex: Integer;
begin
  for RoundIndex := 1 to 12 do
  begin
    PDF.CopyPageRanges(Fronts, IntToStr(RoundIndex));
    PDF.CopyPageRanges(Backs, IntToStr(13 - RoundIndex));
  end;
end;

دوازده دور، دو منبع، بیست‌وچهار نگاشت import. هیچ هشداری داده نمی‌شود. ترتیب صفحات درست است، هر صفحه رندر می‌شود، و تنها علامت این است که فایل چندین برابر بزرگ‌تر از مجموع ورودی‌هایش شده. در یک batch job ۳۰۰صفحه‌ای، این ضریب یک خطای گرد کردن نیست، بلکه تفاوت میان یک آرشیو متناسب با بودجه نگه‌داری و آرشیوی است که متناسب نیست

یک‌بار import کن، سپس درخت صفحه را بازآرایی کن

راه‌حل جدا کردن دو دغدغه‌ای است که حلقه ساده‌لوحانه در هم ادغام کرده بود. کپی کردن تعیین می‌کند چه اشیائی در مقصد وجود دارند؛ ترتیب‌دهی تعیین می‌کند صفحات کجای درخت صفحه قرار می‌گیرند. CollateDocumentsEx هر منبع را دقیقاً یک‌بار، در یک فراخوانی واحد CopyPagesFromDoc با بازه کامل آن منبع کپی می‌کند، بنابراین هر منبع یک نگاشت import می‌گیرد و منابع مشترک یک‌بار نوشته می‌شوند. تودرتوسازی تنها پس از فرود همه منابع رخ می‌دهد، و این کار به‌طور کامل از طریق TPDFPageTree.MovePage انجام می‌شود

جابه‌جایی صفحات از این نظر که اینجا مهم است، رایگان است. ISO 32000-1 §7.7.3 درخت صفحه را به‌صورت ساختاری متوازن از دیکشنری‌های node تعریف می‌کند که آرایه‌های /Kids آن‌ها ارجاعات indirect نگه می‌دارند، با /Count که تعداد برگ‌ها را در هر node حمل می‌کند. جابه‌جا کردن یک صفحه یعنی حذف یک ارجاع indirect از یک آرایه /Kids، درج آن در آرایه دیگر، تنظیم هر دو مقدار /Count، و بازتنظیم /Parent صفحه. هیچ content stream دست نمی‌خورد، هیچ منبعی تکرار نمی‌شود، هیچ شیئی ساخته نمی‌شود. شیء صفحه شماره شیء خود را حفظ می‌کند، و همین دلیل آن است که شماره اشیاء ثابت می‌ماند، همان‌طور که در جایگزینی صفحات با حفظ شماره اشیاء نیز چنین است. یک جزئیات دیگر هست که یک جابه‌جایی ساده‌لوحانه صفحه آن را اشتباه می‌گیرد و MovePage اشتباه نمی‌گیرد. ISO 32000-1 §7.7.3.4 اجازه می‌دهد /Resources، /MediaBox، /CropBox و /Rotate از یک node اجدادی به‌جای بیان صریح روی صفحه به ارث برسند. صفحه‌ای که منابع خود را از node A به ارث می‌برد و سپس زیر node B منتقل می‌شود، به‌طور خاموش چیزی متفاوت یا هیچ‌چیز را به ارث می‌برد. بنابراین MovePage مقدار به‌ارث‌رسیده را تحلیل می‌کند و پیش از جابه‌جایی آن را روی دیکشنری صفحه می‌نویسد، تا صفحه ویژگی‌های خودش را در طول جابه‌جایی حمل کند

مرحله بازآرایی دقیقاً چه کاری انجام می‌دهد؟

یک selection sort در برابر معنای insert-at اجرا می‌کند. ترتیب مطلوب نسبت به بلوک ابتدا محاسبه می‌شود: منابع را به‌ترتیب چرخش پیمایش کن، تا GroupSize اندیس از هر کدام بردار، منبعی را که تمام شده رد کن، تا زمانی که همه صفحات جای‌گذاری شوند تکرار کن. این یک جایگشت روی بلوک اضافه‌شده تولید می‌کند. اعمال آن بخش دشوار است، چون MovePage یک insert است نه یک swap، بنابراین هر جابه‌جایی همه‌چیز بین موقعیت قدیم و موقعیت جدید را یک واحد جابه‌جا می‌کند

پیاده‌سازی یک آرایه Current نگه می‌دارد که مدل می‌کند هر صفحه اضافه‌شده اکنون کجا نشسته، از موقعیت K به جلو برای صفحه‌ای که باید در K باشد پویش می‌کند، جابه‌جایی را صادر می‌کند، سپس مدخل‌های آرایه را طوری می‌لغزاند که آنچه جابه‌جایی روی درخت انجام داد را منعکس کند. از نظر عملیات آرایه O(n مربع) است و از نظر کپی اشیاء صفر است، که برای این حجم کار مبادله درستی است: یک کولاژ ۵۰۰صفحه‌ای یک‌چهارم میلیون شافل عدد صحیح است و حتی یک بایت داده تصویری تکراری نیست. بازه‌های نزولی و صفحات تکراری در این مرحله نیازی به مدیریت خاص ندارند چون PLParsePageRangeList با غیرفعال بودن مرتب‌سازی و مجاز بودن تکرار فراخوانی می‌شود، بنابراین ترتیب درخواستی دست‌نخورده از parsing عبور می‌کند

بازه‌های معکوس و ادغام دوطرفه با یک فراخوانی

با بیان معکوس‌سازی به‌صورت یک بازه، حالت دوباره‌اسکن flatbed در یک فراخوانی واحد فشرده می‌شود. صفحات روی ترتیب طبیعی خود را می‌خواهند و صفحات پشت 12-1 را می‌خواهند، و قطعه اول خالی پیش از semicolon می‌گوید منبع اول همه صفحاتش را مشارکت می‌دهد

var
  PDF: TPDFlib;
  Target, Fronts, Backs: Integer;
begin
  PDF := TPDFlib.Create;
  try
    Target := PDF.NewDocument;
    if PDF.LoadFromFile('fronts.pdf', '') <> 1 then
      Exit;
    Fronts := PDF.SelectedDocument;
    if PDF.LoadFromFile('backs.pdf', '') <> 1 then
      Exit;
    Backs := PDF.SelectedDocument;
    PDF.SelectDocument(Target);
    // fronts 1..12 in order, backs scanned in reverse: F1 B12 F2 B11 ...
    if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 1 then
      PDF.SaveToFile('duplex.pdf');
  finally
    PDF.Free;
  end;
end;

دو رفتار در آن قطعه کد ارزش گفتن صریح دارند. صفحات کولاژشده به سند انتخاب‌شده اضافه می‌شوند، بنابراین سندی که با NewDocument ساخته شده صفحه خالی اولیه‌اش را پیش از آن‌ها مشارکت می‌دهد و اگر آن را نمی‌خواهید باید حذفش کنید. و منابع می‌توانند نامتوازن باشند: با GroupSize برابر ۲ روی یک منبع سه‌صفحه‌ای و یک منبع پنج‌صفحه‌ای، دورها به‌صورت A1 A2 B1 B2، سپس A3 B3 B4 وقتی A تقریباً تمام شده، سپس B5 به‌تنهایی درمی‌آیند، چون یک منبع تمام‌شده صرفاً رد می‌شود نه اینکه با padding پر شود

بازگردانی، فیلدهای فرم، و چه چیزی همراه نمی‌آید

هر آرگومان پیش از دست‌زدن به مقصد اعتبارسنجی می‌شود. یک handle سند از دست‌رفته، سند انتخاب‌شده که به‌عنوان منبع خودش ذکر شده، یک GroupSize کمتر از یک، تعداد قطعاتی که با تعداد منابع مطابقت ندارد، یک بازه که به صفحه‌ای اشاره می‌کند که منبع ندارد: همه این‌ها با مقصد بدون تغییر ۰ برمی‌گردانند. شکست هنگام کپی‌کردن حالت سخت‌تر است، و از طریق DeletePages عمومی مدیریت می‌شود نه PageTree.DeletePages خام. دلیل مشخص است. کپی با فعال بودن MergeFormData اجرا می‌شود، بنابراین تا زمانی که یک منبع بعدی شکست می‌خورد، فیلدهای فرم منبع از قبل به آرایه /AcroForm /Fields مقصد اضافه شده‌اند. حذف صفحات در سطح درخت صفحه، صفحات widget را می‌کند و آن ارجاعات فیلد را آویزان رها می‌کند؛ مسیر عمومی ارجاعات فیلد، outline و رشته مقاله را در کنار صفحات از هم جدا می‌کند

if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 0 then
  // Nothing was appended and the target is byte-identical to before.
  // 412 is the copy failure; 0 means the arguments were rejected
  // during validation, before any page was touched.
  Log(Format('collate rejected, LastErrorCode=%d', [PDF.LastErrorCode]));

با کاربرانتان درباره مرزها صادق باشید. کولاژ صفحات، annotationهای آن‌ها و فیلدهای فرم‌شان را حمل می‌کند، و فهرست فیلد AcroForm، آرایه ترتیب محاسبه و دیکشنری منابع پیش‌فرض را ادغام می‌کند. بوکمارک‌های منبع را حمل نمی‌کند: درخت outline یک دسته صفحات روی اسکن‌شده تقریباً همیشه خالی است، بنابراین در حالت دوطرفه چیزی از دست نمی‌رود، اما اگر دو سند نویسنده‌دار را کولاژ کنید outlineهایشان جا می‌ماند و باید ناوبری را خودتان بازسازی کنید. مقصدهای نام‌گذاری‌شده‌ای که فقط در کاتالوگ منبع بودند در همین موقعیت قرار دارند. پیش از قول دادن یک کولاژ بدون‌افت به مشتری، برای این موضوع برنامه‌ریزی کنید

PDFlibPas توابع کولاژ را همراه با بقیه سطح مونتاژ صفحه خود ارائه می‌دهد، بنابراین workflow اسکنر، استخراج مبتنی‌بر بازه و مسیرهای فایل بزرگ همگی پشت یک کامپوننت واحد در Delphi و C++Builder قرار دارند. مرجع کامل API و یک نسخه trial در صفحه محصول losLab Delphi PDF library موجود است