مقاله فنی

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

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

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

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

ایمپوزیشن اصطلاح پیش از چاپ برای چیدمان چندین صفحه منبع در یک برگه بزرگ‌تر است تا نتیجه چاپ‌شده و تا شده با ترتیب صحیح خوانده شود. نسخه روزمره آن جزوه 2 صفحه‌ای در یک برگه (2-up)، فرمول‌بندی جزوه 4 صفحه‌ای (4-up)، یا برگه تماس (contact sheet) است که ده‌ها تصویر کوچک را در یک صفحه جا می‌دهد. PDFium هندسه این کار را از طریق یک فراخوانی مدیریت می‌کند:

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

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

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

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

اعدادی که ارزش به خاطر سپردن دارند، دو اندازه صفحه‌ای هستند که بیشتر از آن‌ها استفاده خواهید کرد. US Letter برابر 612 در 792 پوینت است، زیرا 8.5 اینچ ضربدر 72 می‌شود 612 و 11 اینچ ضربدر 72 می‌شود 792. قطع A4 تقریباً 595 در 842 پوینت است، از ابعاد 210 در 297 میلی‌متری خود. هدر خود بایندینگ این قانون را به وضوح بیان می‌کند که یک واحد برابر با یک هفتاد و دوم اینچ است، و این یونیت یک ثابت PointsPerInch برابر با 72 را ارائه می‌دهد در صورتی که ترجیح می‌دهید به جای نوشتن مقدار عددی، اندازه را از اینچ در کد محاسبه کنید

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 نشت (leak) می‌کند

اشتباه خطرناک‌تر مسیر برعکس را می‌رود. در لایه زیرین، این متد از طریق 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 صفحات را به جای کپی کردن و فشرده‌سازی مجدد محتوای آن‌ها تغییر مکان می‌دهد، این عملیات ارزان و بدون افت کیفیت است: اشیاء صفحه استریم‌های خود، منابع خود و وفاداری خود را حفظ می‌کنند. این همان فراخوانی پشت یک پنل صفحه است که با کشیدن و رها کردن مرتب می‌شود، جایی که کاربر یک تصویر بندانگشتی را به اسلات جدیدی می‌کشد و شما سفارش جدید را با یک حرکت ثبت می‌کنید. زمانی که ایندکس خارج از محدوده باشد این متد 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 برمی‌گرداند

اینجا است که تضاد با جداسازی (split) اهمیت پیدا می‌کند. جداسازی فایل‌های جداگانه‌ای می‌نویسد، یک عملیات که خروجی‌های زیادی را روی دیسک تولید می‌کند. ImportPagesByIndex شکل برعکس این کار را انجام می‌دهد: این متد مجموعه‌ای از صفحات انتخابی را در یک سند هدف واحد در حافظه جمع‌آوری می‌کند، که سپس شما آن را یک بار ذخیره می‌کنید. زمانی که وظیفه شما این است: "صفحات 3، 7 و 12 را به عنوان یک فایل PDF کوتاه به من بده"، این مسیر مستقیم است، و FPDF_ImportPagesByIndex را در لایه زیرین بسته‌بندی می‌کند

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;

کنار هم قرار دادن تمیز آن

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

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