Technical Article

مهر‌های صفحه قابل استفاده مجدد از طریق Form XObjects با PDFium

مهر زدن یک واترمارک یا یک لوگو روی هر صفحه از یک سند شبیه به یک کار پنج دقیقه‌ای به نظر می‌رسد تا زمانی که نتیجه را در یک بازرس اندازه فایل (file-size inspector) باز کنید. رویکرد بدیهی این است که صفحات را پیمایش کنید و در هر کدام، همان اشیاء متنی یا تصویری را دوباره بسازید. این کار از نظر بصری جواب می‌دهد، اما به روشی اسراف‌کارانه است که انباشته می‌شود. یک واترمارک مورب «DRAFT» که مستقیماً روی یک گزارش صد صفحه‌ای کشیده شده، صد کپی از همان مسیر و داده‌های متنی است که در جریان‌های محتوا قرار گرفته‌اند و فایل ذخیره‌شده تک‌تک آن‌ها را حمل می‌کند.

یک Form XObject ساختاری است که PDF برای جلوگیری دقیق از این موضوع ارائه می‌دهد. این شیء یک بخش از محتوای قابل استفاده مجدد، یک صفحه کامل یا یک قالب کوچک را در یک شیء نام‌گذاری شده واحد بسته‌بندی می‌کند که می‌تواند بارها در موقعیت‌های مختلف رنگ‌آمیزی شود. محتوا یک بار در فایل زندگی می‌کند. هر صفحه‌ای که مهر را می‌خواهد یک دستورالعمل کوتاه دارد که می‌گوید «XObject N را در اینجا با این تبدیل رنگ‌آمیزی کن.» یک واترمارک صد صفحه‌ای سپس به جای صد شیء، یک شیء محتوایی را به فایل اضافه می‌کند و این تفاوت بین سندی است که به صورت خطی با تعداد صفحاتش رشد می‌کند و سندی که این‌گونه نیست. واترمارک‌ها، مهرهای لوگو، قالب‌های شماره صفحه و مهر و موم‌ها همگی یک شکل از مشکل هستند و Form XObject ابزار مناسبی برای تک‌تک آن‌ها است.

چرا یک شیء ذخیره‌شده بهتر از صد بار ترسیم مجدد است

صرفه‌جویی ساختاری است، نه آرایشی. یک صفحه PDF با اجرای جریان محتوای خود، یعنی توالی از اپراتورهای ترسیم، رندر می‌شود. هنگامی که یک مهر را برای هر صفحه دوباره ترسیم می‌کنید، توالی کامل اپراتور برای آن مهر را به جریان هر صفحه اضافه می‌نمایید و بایت‌ها به تعداد صفحات شما تکرار می‌شوند. یک Form XObject آن اپراتورها را به یک جریان واحد منتقل می‌کند که یک بار در سند ذخیره می‌شود. ارجاعی که یک صفحه جداگانه نگه می‌دارد کوچک است: یک ماتریس تبدیل (transformation matrix) را اعمال می‌کند، XObject را فراخوانی می‌کند و حالت را بازیابی می‌نماید. تعداد صفحات دیگر هزینه اثر هنری را ضرب نمی‌کند.

این matters most when the stamp is heavy. A vector seal with hundreds of path segments, or a logo bitmap, is expensive to store. Stored once and referenced, the heavy part is paid for a single time and the per-page overhead is a few bytes of invocation. The visual result on the page is identical to a direct redraw, which is the point. The reader cannot tell the difference; the file size very much can.

ضبط یک صفحه در یک XObject

پی‌دی‌اف‌یوم شیء قابل استفاده مجدد را از یک صفحه موجود می‌سازد. منبع صفحه‌ای در سندی است که باز کرده‌اید، یک PDF کوچک تک‌صفحه‌ای که شامل هیچ‌چیز جز اثر هنری واترمارک شما نیست، یا یک صفحه خاص از یک فایل بزرگ‌تر. CreateXObjectFromPage محتوای آن صفحه منبع را در یک هندل قابل استفاده مجدد که متعلق به سند مقصد (سندی که در حال مهر زدن آن هستید) است، ضبط می‌کند.

var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
begin
  Dest := TPdf.Create;
  Stamp := TPdf.Create;
  try
    Dest.LoadFromFile('Report.pdf');
    Stamp.LoadFromFile('Watermark.pdf');

    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not build the stamp XObject');

امضا به صورت CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject است. این متد در صورت بروز خطا به جای ایجاد استثنا، مقدار nil را برمی‌گرداند، بنابراین بررسی صریح بالا اختیاری نیست. هندلی که بازمی‌گردد یک TPdfXObject متعلق به شماست و دو محدودیت طول عمر مرتبط با آن، بخشی از کل این تمرین است که افراد را غافلگیر می‌کند، بنابراین در بخش زیر به آن‌ها پرداخته می‌شود.

قرار دادن مهر روی یک صفحه

یک XObject ضبط‌شده به تنهایی هیچ کاری انجام نمی‌دهد. برای نمایان کردن آن، شما کپی از آن را با InsertFormObjectFromXObject در صفحه فعلی سند وارد می‌کنید. آن فراخوانی شیء صفحه زیرین، یعنی یک FPDF_PAGEOBJECT را برمی‌گرداند و هندل بازگردانده شده نحوه تعیین موقعیت قرارگیری است. بدون تغییر (transform)، مهر در مبدأ مختصات خود صفحه منبع فرود می‌آید که به ندرت همان جایی است که شما می‌خواهید.

از آنجا که InsertFormObjectFromXObject در هر فراخوانی یک کپی را وارد کرده و هر بار یک شیء صفحه جدید را پس می‌دهد، می‌توانید همان XObject را چندین بار در یک صفحه با تبدیل‌های مختلف رسم کنید و محتوای ذخیره‌شده همچنان یک بار در فایل حساب می‌شود. یک لوگو در گوشه و یک واترمارک کم‌رنگ کل صفحه می‌توانند از یک شیء ضبط‌شده یکسان حاصل شوند.

var
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
begin
  PageObj := Dest.InsertFormObjectFromXObject(XObject);
  if PageObj = nil then
    raise Exception.Create('Insert failed on this page');

  M := TPdfMatrix.Create;
  try
    M.Scale(0.7, 0.7);
    M.Translate(200, 500);
    FPDFPageObj_SetMatrix(PageObj, M.Handle);
  finally
    M.Free;
  end;
end;

یک جزئیات مالکیت، پاک‌سازی را ایمن می‌کند. شیء صفحه پس از وارد شدن، متعلق به صفحه است، نه به XObject. آزاد کردن XObject در مراحل بعدی، موقعیت‌های قرارگیری را که قبلاً ایجاد کرده‌اید باطل نمی‌کند. این همان چیزی است که به ترتیبِ ساخت-قراردهی-آزادکردن توصیف‌شده در زیر اجازه کار می‌دهد.

قانون طول عمر هندل که افراد با آن به مشکل می‌خورند

دو محدودیت بر هندل XObject حاکم است و نادیده گرفتن هر کدام خطایی را ایجاد می‌کند که به نظر می‌رسد با علت آن بی‌ارتباط است. اول اینکه سند منبع باید در لحظه فراخوانی CreateXObjectFromPage فعال باشد. ضبط محتوا، صفحه منبع را از سند منبع زنده می‌خواند، بنابراین آن سند و صفحه آن باید در زمان ساخت هندل باز و معتبر باشند. دوم، و این همان چیزی است که مردم را شگفت‌زده می‌کند، هندل باید قبل از بسته شدن صفحه منبع، و در عمل قبل از بستن یا آزاد کردن سند منبعی که از آن آمده است، آزاد شود.

دلیل آن این است که XObject ارجاعی به ساختاری است که سند منبع هنوز مالک آن است. این یک کپی جدا شده و مستقل نیست که بتوانید پس از رفتن منبع با خود حمل کنید. ابتدا منبع را ببندید تا هندل به سمت محتوایی که از بین رفته اشاره کند، بنابراین آزاد کردن آن در مراحل بعدی یا هر استفاده دیگری از آن، روی حافظه‌ای عمل می‌کند که دیگر معتبر نیست. علامت آن علامت کلاسیک برای یک هندل معلق (dangling handle) است: خطای دسترسی در زمان خاموش شدن، یا خرابی متناوب که بر اساس ترتیب تخصیص حرکت می‌کند، با پشته‌ای که به کد پاک‌سازی اشاره می‌کند تا خطی که واقعاً باعث مشکل شده است. راه حل ترتیب کار است، نه کدنویسی تدافعی. XObject را بسازید، آن را در هر صفحه‌ای که نیاز دارد وارد کنید، XObject را آزاد کنید و تنها پس از آن سند منبع را ببندید. مخرب TPdfXObject هندل زیرین پی‌دی‌اف‌یوم را برای شما آزاد می‌کند، بنابراین آزاد کردن بسته‌بند در زمان مناسب کل مسئولیت شماست.

ماتریس و معنی شش عدد آن

موقعیت‌دهی یک تبدیل همگرای دوبعدی (2D affine transform) است، همان چیزی که PDF در همه جا برای موقعیت‌دهی محتوا استفاده می‌کند (ISO 32000-1، بخش ۸.۳.۴). این شامل شش عدد است که به صورت a, b, c, d, e, f نوشته می‌شوند و پی‌دی‌اف‌یوم آن‌ها را به عنوان رکورد FS_MATRIX نشان می‌دهد. آن‌ها یک نقطه را از فضای خود شیء به فضای صفحه نگاشت می‌کنند:

// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)

شما می‌توانید آن شش مقدار را به صورت دستی پر کنید، اما ترکیب دستی آن‌ها جایی است که چرخش به خطا می‌رود، زیرا چرخش هر چهار مورد a, b, c, d را با هم مخلوط می‌کند. بسته‌بند TPdfMatrix عملیات متداول را برای شما ترکیب می‌کند و همان‌طور که پیش می‌رود پس‌ضرب می‌کند، بنابراین Translate ،Scale و Rotate به ترتیبی که آن‌ها را فراخوانی می‌کنید زنجیره می‌شوند. یک واترمارک مورب یک چرخش و به دنبال آن یک انتقال برای مرکز‌دهی مجدد آن است؛ یک لوگو در گوشه یک مقیاس و به دنبال آن یک انتقال است. هنگامی که ماتریس آماده شد، مقدار خام آن را به FPDFPageObj_SetMatrix(PageObj, M.Handle) بدهید، جایی که M.Handle همان FS_MATRIX زیرین است. متد سطح پایین‌تر FPDFPageObj_Transform که شش مقدار را مستقیماً به صورت double می‌گیرد، در صورتی که ترجیح می‌دهید اعداد را پاس دهید تا اینکه یک بسته‌بند بسازید، در دسترس است.

مهر زدن بر هر صفحه با ترتیب درست

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

procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  i: Integer;
begin
  Dest := TPdf.Create;
  Stamp := TPdf.Create;
  try
    Dest.LoadFromFile(ASource);
    Stamp.LoadFromFile(AStamp);

    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not capture the stamp page');
    try
      for i := 0 to Dest.PageCount - 1 do
      begin
        Dest.CurrentPageIndex := i;
        PageObj := Dest.InsertFormObjectFromXObject(XObject);
        if PageObj = nil then
          Continue;

        M := TPdfMatrix.Create;
        try
          M.Rotate(45);
          M.Translate(150, 100);
          FPDFPageObj_SetMatrix(PageObj, M.Handle);
        finally
          M.Free;
        end;
      end;
    finally
      XObject.Free;
    end;

    Dest.SaveLoadedDocument(AOutput);
  finally
    Stamp.Free;
    Dest.Free;
  end;
end;

شکل بلاک‌های try کار واقعی را انجام می‌دهد. بخش داخلی finally شیء XObject را قبل از اینکه کنترل بتواند به بخش خارجی finally که Stamp را آزاد می‌کند برسد، آزاد می‌نماید، بنابراین هندل همیشه در حالی که منبع آن هنوز زنده است آزاد می‌شود، حتی اگر استثنایی در میان حلقه رخ دهد. این تو در تویی را درست انجام دهید تا قانون طول عمر خودش مراقب خودش باشد. (از هر انتخاب‌گر صفحه فعلی که بیلد شما ارائه می‌دهد استفاده کنید؛ بدنه حلقه در هر دو حالت یکسان است.)

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