Technical Article

چینش N-up و مرتب‌سازی مجدد صفحات با PDFium

ادغام و تقسیم دو عملیات صفحه‌ای هستند که همه ابتدا به سراغشان می‌روند و کارهای زیادی را پوشش می‌دهند. اما آن‌ها همه چیز را پوشش نمی‌دهند. خانواده دیگری از کارها وجود دارد که صفحات را به جای جابه‌جایی کل فایل‌ها، مرتب می‌کند: چیدن چهار اسلاید روی یک برگ برای یک بروشور، کشیدن یک صفحه از انتهای سند به ابتدا، یا استخراج صفحات ۳، ۷ و ۱۲ در یک گزیده کوتاه بدون دست زدن به بقیه صفحات. کتابخانه PDFium سه متد را دقیقاً برای این کار ارائه می‌دهد که عملکرد هر کدام با ادغام و تقسیمی که می‌شناسید متفاوت است. این مقاله به بررسی کارهایی که انجام می‌دهند، محل قرارگیری نقاط خروجی و یک جزئیات مالکیت می‌پردازد که در عمل باعث بروز کرش شده است

این سه متد عبارتند از ImportNPagesToOne برای چینش N-up، متد MovePages برای مرتب‌سازی مجدد در محل، و ImportPagesByIndex برای استخراج زیرمجموعه. ادغام، اسناد را پشت سر هم قرار می‌دهد و تعداد صفحات را برابر با مجموع ورودی‌ها می‌کند. تقسیم، چندین فایل خروجی را از یک ورودی می‌نویسد. این سه عملیات در این بین قرار می‌گیرند: یکی از آن‌ها تعداد صفحات منبع را که در یک برگ مشترک هستند تغییر می‌دهد، یکی از آن‌ها ترتیب داخل یک سند واحد را عوض می‌کند و دیگری تعداد انگشت‌شماری از صفحات انتخابی را در سند دیگری کپی می‌نماید. دانستن اینکه کدام متد برای چیست، شما را از انجام عملیات‌های پیچیده ادغام و حذف نجات می‌دهد در حالی که یک فراخوانی ساده کار را انجام می‌دهد

چینش N-up در واقع چه کاری انجام می‌دهد

چینش (Imposition) اصطلاحی در صنعت چاپ برای چیدن چندین صفحه منبع روی یک برگ بزرگ‌تر است، به طوری که نتیجه چاپ‌شده و تا شده با ترتیب درست خوانده شود. نسخه روزمره آن بروشورهای ۲ صفحه‌ای در یک برگ، کتابچه‌های ۴ صفحه‌ای، یا صفحات تماس است که ده‌ها تصویر بندانگشتی را در یک صفحه جا می‌دهد. کتابخانه PDFium هندسه این کار را از طریق یک فراخوانی مدیریت می‌کند

function ImportNPagesToOne(
  OutputWidth, OutputHeight: Single;
  NumX, NumY               : Cardinal): TPdf;

مقادیر NumX و NumY شبکه را توصیف می‌کنند. مقدار 2, 1 دو صفحه منبع را در کنار هم قرار می‌دهد؛ مقدار 2, 2 چهار صفحه را در یک طرح‌بندی چهاربخشی بسته‌بندی می‌کند؛ مقدار 4, 3 یک صفحه تماس دوازده‌تایی می‌سازد. کتابخانه PDFium صفحات منبع را به ترتیب می‌خواند، هر کدام را برای قرار گرفتن در سلول خود کوچک می‌کند و شبکه را از چپ به راست و از بالا به پایین پر می‌کند و هر بار که شبکه فعلی پر می‌شود، یک برگ خروجی جدید را شروع می‌کند. صفحات منبع تغییر داده نمی‌شوند. آنچه دریافت می‌کنید یک سند جدید است که صفحات آن ترکیبی هستند

اندازه خروجی بر اساس نقطه است، نه پیکسل

مقادیر OutputWidth و OutputHeight واحدهای کاربر PDF هستند و یک واحد کاربر PDF برابر با یک نقطه (Point) است که یک هفتاد و دوم اینچ است. این واحد اندازه فیزیکی برگ خروجی را اعلام می‌کند و هیچ ارتباطی با پیکسل‌های صفحه نمایش یا DPI رندر ندارد. این رایج‌ترین نقطه‌ای است که یک چینش در آن اشتباه انجام می‌شود، زیرا توسعه‌دهنده‌ای که به بیت‌مپ‌ها عادت دارد به سراغ تعداد پیکسل می‌رود و به برگی به اندازه یک تمبر پستی یا یک بیلبورد می‌رسد

اعدادی که ارزش به خاطر سپردن دارند، دو اندازه صفحه‌ای هستند که بیشتر از همه استفاده خواهید کرد. ابعاد US Letter برابر با ۶۱۲ در ۷۹۲ نقطه است، زیرا ۸.۵ اینچ ضرب در ۷۲ برابر با ۶۱۲ و ۱۱ اینچ ضرب در ۷۲ برابر با ۷۹۲ است. ابعاد A4 با توجه به اندازه‌های ۲۱۰ در ۲۹۷ میلی‌متری آن، تقریباً ۵۹۵ در ۸۴۲ نقطه است. هدر خود بایندینگ این قانون را به وضوح بیان می‌کند که هر واحد یک هفتاد و دوم اینچ است و این واحد شامل ثابت PointsPerInch برابر با ۷۲ است، اگر ترجیح می‌دهید به جای نوشتن مقدار ثابت، اندازه را از روی اینچ در کد محاسبه کنید

const
  LetterW = 612.0;   // 8.5 in * 72
  LetterH = 792.0;   // 11  in * 72
var
  Source, Composite: TPdf;
begin
  Source := TPdf.Create(nil);
  Composite := nil;
  try
    Source.FileName := 'slides.pdf';
    Source.Active := True;

    // Four source pages per Letter sheet, 2 by 2 grid.
    Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
    if Composite = nil then
      raise Exception.Create('PDFium rejected the imposition arguments');

    Composite.SaveAs('slides-4up.pdf');
  finally
    Composite.Free;   // see the next section: this is mandatory
    Source.Free;
  end;
end;

هندل بازگردانده‌شده برای آزاد کردن در اختیار شماست

امضا را دوباره بخوانید. متد ImportNPagesToOne یک TPdf برمی‌گرداند، نه یک مقدار Boolean. آن مقدار بازگشتی یک هندل سند کاملاً جدید است که به طور جداگانه از منبع تخصیص داده شده و فراخوان‌کننده مالک آن است. شیء منبع TPdf که متد را روی آن فراخوانی کرده‌اید دست‌نخورده باقی می‌ماند و همچنان مالک هندل خود است؛ سند ترکیبی یک شیء دوم و مستقل است. اگر اجازه دهید TPdf بازگردانده‌شده بدون آزاد شدن از محدوده خارج شود، یک سند کامل PDFium را نشت می‌دهید

اشتباه خطرناک‌تر در جهت دیگر رخ می‌دهد. در زیر کار، این متد از طریق FPDF_ImportNPagesToOne یک FPDF_DOCUMENT تازه از PDFium درخواست می‌کند، سپس آن هندل خام را درون TPdf بازگردانده‌شده می‌پیچد تا طول عمر بسته‌بند بر طول عمر هندل حاکم باشد. از آن نقطه به بعد، دقیقاً یک مالک برای هندل وجود دارد و دقیقاً یک مکان وجود دارد که باید در آن بسته شود: زمانی که شیء بازگردانده‌شده را Free می‌کنید. یک مسیر خطای بی‌دقت که هم بسته‌بند را آزاد می‌کند و هم FPDF_CloseDocument را روی هندل خام به دست آمده فراخوانی می‌نماید، همان سند PDFium را دو بار می‌بندد. این یک آزادکننده دوگانه (Double-free) است و همان باگ خاصی است که یک بار گریبان‌گیر یک فراخوان‌کننده در اینجا شد. قانونی که از آن جلوگیری می‌کند کوتاه است. سند را فقط در یک مسیر ببندید، با آزاد کردن TPdf که متد به شما تحویل داده است، و هرگز فراتر از بسته‌بند نروید تا هندلی را که قبلاً پذیرفته است ببندید

دو نتیجه از این موضوع حاصل می‌شود. اول اینکه، وقتی PDFium آرگومان‌ها را رد می‌کند، مانند صفر در هر یک از محورهای شبکه یا شکست در تخصیص، متد مقدار nil را برمی‌گرداند، بنابراین یک بررسی nil قبل از دست زدن به نتیجه لازم است. دوم اینکه، متغیر خروجی خود را قبل از بلوک try به nil مقداردهی اولیه کنید و آن را در بخش finally آزاد سازید، همان‌طور که در نمونه بالا انجام شده است، تا خرابی در میانه کار باعث نشود که یک مرجع تعریف‌نشده را آزاد کنید یا کلاً از آزاد کردن آن چشم‌پوشی نمایید

مرتب‌سازی مجدد صفحات بدون بازنویسی آن‌ها

مرتب‌سازی مجدد، یک سند را در محل تغییر می‌دهد. متد MovePages مجموعه‌ای از صفحات را از موقعیت‌های فعلی خود برمی‌دارد و در یک مقصد قرار می‌دهد و همه چیزهای دیگر را در اطراف بلوک جابه‌جایی تغییر می‌دهد تا تعداد صفحات ثابت بماند

function MovePages(
  const PageIndices: array of Integer;
  DestPageIndex    : Integer): Boolean;

نمایه‌ها مبتنی بر صفر هستند. PageIndices صفحات مورد نظر برای جابه‌جایی را به ترتیبی که باید قرار گیرند فهرست می‌کند و DestPageIndex نمایه‌ای است که اولین صفحه جابه‌جاشده پس از استقرار جابه‌جایی روی آن قرار می‌گیرد. از آنجا که PDFium صفحات را به جای کپی کردن و فشرده‌سازی مجدد محتوای آن‌ها جابه‌جا می‌کند، این عملیات ارزان و بدون اتلاف اطلاعات است: اشیاء صفحه جریان‌ها، منابع و یکپارچگی خود را حفظ می‌کنند. این فراخوانی پشت یک پنل صفحه drag-to-reorder قرار دارد، جایی که کاربر یک تصویر بندانگشتی را به خانه جدیدی می‌کشد و شما ترتیب جدید را با یک جابه‌جایی ثبت می‌کنید. در صورتی که یک نمایه خارج از محدوده باشد، مقدار False بازگردانده می‌شود، بنابراین به جای فرض اینکه مرتب‌سازی انجام شده است، نتیجه را اعتبارسنجی کنید

var
  Doc: TPdf;
begin
  Doc := TPdf.Create(nil);
  try
    Doc.FileName := 'report.pdf';
    Doc.Active := True;

    // Move the last page (index 4 in a 5-page file) to the very front.
    if not Doc.MovePages([4], 0) then
      raise Exception.Create('MovePages rejected the index');

    Doc.SaveAs('report-reordered.pdf');
  finally
    Doc.Free;
  end;
end;

استخراج یک زیرمجموعه با نمایه

عملیات سوم، مجموعه صریحی از صفحات را از یک سند به سند دیگر کپی می‌کند. متد ImportPagesByIndex سند منبع و یک آرایه نمایه مبتنی بر صفر را می‌گیرد و آن صفحات را در سند هدف در موقعیت انتخابی قرار می‌دهد

function ImportPagesByIndex(
  Source           : TPdf;
  const PageIndices: array of Integer;
  InsertAt         : Integer= 0): Boolean;

شما آن را روی سند هدف فراخوانی می‌کنید و منبع را به عنوان اولین آرگومان پاس می‌دهید. PageIndices صفحات منبع را برای استخراج، به ترتیبی که می‌خواهید نام می‌برد؛ پارامتر InsertAt موقعیت مبتنی بر صفر در هدف است که اولین صفحه واردشده در آن قرار می‌گیرد، بنابراین مقدار 0 آن‌ها را قبل از اولین صفحه موجود قرار می‌دهد و تعداد صفحات فعلی سند هدف در انتها اضافه می‌شود. یک آرایه خالی تمام صفحات را وارد می‌کند که این فراخوانی در صورت نیاز، یک کپی کامل می‌سازد. اگر هر نمایه‌ای در منبع خارج از محدوده باشد، مقدار False بازگردانده می‌شود

var
  Source, Excerpt: TPdf;
begin
  Source := TPdf.Create(nil);
  Excerpt := TPdf.Create(nil);
  try
    Source.FileName := 'manual.pdf';
    Source.Active := True;
    Excerpt.CreateDocument;   // start an empty target

    // Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
    if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
      raise Exception.Create('A requested page index is out of range');

    Excerpt.SaveAs('manual-excerpt.pdf');
  finally
    Excerpt.Free;
    Source.Free;
  end;
end;

جمع‌بندی تمیز

شکل کلی در هر سه متد یکسان است: منبع را با تنظیم FileName و تغییر Active به True باز کنید، عملیات را انجام دهید، با SaveAs ذخیره کنید و آنچه را که مالک آن هستید آزاد کنید. یک شاخه‌ای که نیاز به دقت دارد این است که کدام فراخوانی‌ها یک سند جدید را تخصیص می‌دهند. متد MovePages سندی را که قبلاً در اختیار دارید تغییر می‌دهد، بنابراین یک شیء برای آزاد کردن وجود دارد. متد ImportPagesByIndex در هدفی می‌نویسد که خودتان ایجاد کرده‌اید، بنابراین منبع و هدفی را که باز کرده‌اید آزاد می‌کنید. متد ImportNPagesToOne یک مورد خاص است، زیرا سند جدید مقدار بازگشتی متد است نه چیزی که خودتان ساخته‌اید، و فراموش کردن اینکه این یک هندل جداگانه و متعلق به فراخوان‌کننده است، باعث بروز نشت و آزادکننده دوگانه می‌شود. نتیجه را به nil مقداردهی اولیه کنید، آن را پس از فراخوانی بررسی کرده و در یک مسیر واحد آزاد نمایید

اگر کاری که در واقع دارید ترکیب کل فایل‌ها به جای مرتب‌سازی مجدد صفحات است، به ادغام چندین فایل PDF در یک سند مراجعه کنید. اگر برعکس است، یعنی شکستن یک سند به چندین فایل، به تقسیم اسناد PDF به چندین فایل مراجعه نمایید. روش‌های چینش و مرتب‌سازی مجدد که در اینجا توضیح داده شد به عنوان بخشی از PDFium Component برای Delphi و C++Builder همراه با APIهای بارگذاری، رندر و ویرایش که در بخش‌های دیگر این وبلاگ پوشش داده شده‌اند، عرضه می‌شود