مقاله فنی

handleهای شیء صفحه PDFium پس از Transform کهنه می‌شوند در Delphi

وقتی FPDFPage_TransFormWithClip یک صفحه را بازمی‌نویسد، هر handle FPDF_PAGEOBJECTای که از قبل نگه دارید همچنان parse از پیش از transform را توصیف می‌کند. PDFium Component برای Delphi و C++Builder این را داخل TransformPageContent حل می‌کند، که text page را unload می‌کند، محتوا را دوباره تولید می‌کند، سپس صفحه را reload می‌کند پس پرسش‌های بعدی مختصات جدید را می‌بینند

علامت آرام است. یک مقیاس ۰٫۹ برای اضافه کردن یک حاشیه چاپ اعمال می‌کنید، سپس PageObjectInfo را می‌خوانید و دقیقاً همان اعدادی را که پیش از فراخوانی گرفتید می‌گیرید. هیچ exception، هیچ کد خطا، هیچ‌چیز در یک log. این شکستی متفاوت از text page کش‌شده است که در مقاله text pageهای کهنه پس از یک ویرایش شرح داده شده: آنجا کش یک handle واحد FPDF_TEXTPAGE است که می‌توانید رها و بازسازی کنید، اینجا مشکل هر handle شیء صفحه در متغیرهای خودتان است، به‌علاوه یک دسته از getterها که شکست را از طریق یک کد بازگشتی که اغلب فراخوانندگان دور می‌ریزند گزارش می‌دهند

چرا مرزهای شیء صفحه بدون خطا کهنه می‌شوند؟

چون یک handle شیء صفحه یک pointer به یک نمایش parse‌شده از یک content stream خاص است، و یک transform سراسر-صفحه آن content stream را با یکی جدید جایگزین می‌کند. PDFium در امتداد call stack شما برای پچ کردن handleها نمی‌گردد. یک گراف شیء تازه می‌سازد و قدیمی را دقیقاً همان‌طور که بود رها می‌کند، پس یک خواندن در برابر handle قدیمی یک خواندن کاملاً معتبر از یک ساختار است که دیگر با چیزی که فایل می‌گوید مطابقت ندارد

ISO 32000-1 §7.8.2 content stream را به‌عنوان دنباله operatorهایی که یک صفحه را رسم می‌کنند تعریف می‌کند، و §8.3.3 تعریف می‌کند چگونه ماتریس تبدیل فعلی فضای کاربر را به فضای دستگاه نگاشت می‌کند. یک transform سطح-صفحه با پیچیدن و بازنویسی آن operatorها بیان می‌شود، نه با ویرایش مختصات به‌ازای هر شیء در جا. پس مختصاتی که اشیاء حمل می‌کنند ممکن است اصلاً تغییر نکنند؛ چیزی که تغییر می‌کند ماتریسی است که هنگام رسم آن‌ها نیرو دارد. هر handleای که زیر ماتریس قدیمی parse شده به سؤالات هندسه زیر ماتریس قدیمی پاسخ می‌دهد، و بدون شکایت به آن‌ها پاسخ می‌دهد

FPDFPage_TransFormWithClip واقعاً چه چیزی را بازمی‌نویسد

صفحه را بازمی‌نویسد، نه snapshotهای شما را. FPDFPage_TransFormWithClip یک FS_MATRIX و یک مستطیل clip FS_RECTF می‌گیرد و هر دو را روی کل محتوای صفحه اعمال می‌کند. برای حاشیه‌ها، مقیاس‌دهی imposition، و نرمال‌سازی یک صفحه با اندازه عجیب در برابر یک box هدف فراخوانی درستی است. اگر انتظار دارید handleهای موجود همراهی کنند فراخوانی اشتباهی است که به آن دست بزنید، و همچنین ارزش به‌یاد‌داشتن دارد که فقط محتوای صفحه را لمس می‌کند: annotationها لایه‌ای جدا هستند و به TransformPageAnnotations نیاز دارند، که همان شش ضریب ماتریس را به FPDFPage_TransformAnnots forward می‌کند

var
  Info: TPdfPageObjectInfo;
  Scale: FS_MATRIX;
  Clip: TPdfRectangle;
begin
  Pdf.PageNumber:= 1;
  Info:= Pdf.PageObjectInfo(0);           // snapshot taken before the transform

  Scale.a:= 0.9;   Scale.b:= 0.0;
  Scale.c:= 0.0;   Scale.d:= 0.9;
  Scale.e:= 29.7;  Scale.f:= 42.0;        // 5% margin, A4 in points
  Clip:= Pdf.GetPageBox(pbMedia);
  Pdf.TransformPageContent(Scale, Clip);

  // Info.Bounds still holds pre-transform geometry, and Info.Handle now
  // points into a page that TransformPageContent has already replaced
end;

ترتیب تازه‌سازی‌ای که TransformPageContent استفاده می‌کند

چهار گام، به این ترتیب: unload کردن text page، transform، تولید محتوا، reload صفحه. TPdf.TransformPageContent دقیقاً همان دنباله را اجرا می‌کند. CheckPageActive را فراخوانی می‌کند، ماتریس و clip را در شکل‌های رکورد بومی خودشان کپی می‌کند، UnloadTextPage را فراخوانی می‌کند، سپس FPDFPage_TransFormWithClip، سپس UpdatePage، که wrapper اطراف FPDFPage_GenerateContent است، و در نهایت ReloadPage

هر گام جای خودش را کسب می‌کند. UnloadTextPage اول می‌آید چون FPDF_TEXTPAGE کش‌شده boxهای کاراکتر محاسبه‌شده زیر ماتریس قدیمی را نگه می‌دارد، و همچنین فهرست web-link مشتق‌شده و هر جلسه find در حال پیشرفتی که از آن ساخته شده را رها می‌کند. FPDFPage_GenerateContent باید پیش از reload اجرا شود، چون transform در صفحه حافظه‌ای زندگی می‌کند تا زمانی که به content stream سریالایز شود، و در غیر این صورت یک reload stream تغییرنیافته را دوباره parse می‌کرد. ReloadPage با FPDF_LoadPage در برابر اندیس صفحه فعلی پایان می‌یابد، که تنها چیزی است که واقعاً یک گراف شیء تازه به شما می‌دهد

// After the transform, re-enumerate. Do not reuse anything captured earlier.
var
  I: Integer;
  Info: TPdfPageObjectInfo;
begin
  Pdf.TransformPageContent(Scale, Clip);   // unload text page, transform,
                                           // generate content, reload page
  for I:= 0 to Pdf.ObjectCount- 1 do
  begin
    Info:= Pdf.PageObjectInfo(I);          // handle and bounds from the new parse
    if Info.Bounds.Right> PageWidth then
      Log('object '+ IntToStr(I)+ ' still overflows after scaling');
  end;
end;

یک جزئیات در ReloadPage ارزش کپی کردن دارد اگر خودتان هرگز این دنباله را بنویسید. ابتدا صفحه جدید را بارگذاری می‌کند و فقط پس از آن آن را به فیلد commit می‌کند، پس یک بارگذاری صفحه که شکست می‌خورد صفحه بومی فعلی و هر کش مشتق‌شده آن را دست‌نخورده رها می‌کند به‌جای انداختن شما در یک وضعیت نیمه‌فروپاشیده. reload کردن رایگان نیست — شما برای یک re-parse کامل صفحه هزینه می‌دهید — اما یک‌بار به‌ازای هر transform پرداخت می‌شود، نه یک‌بار به‌ازای هر پرسش، و هیچ جایگزین درست ارزان‌تری وجود ندارد

handleها را در سراسر reload حمل نکنید

پس از reload، handleهای قدیمی صرفاً کهنه نیستند، آویزان‌اند. FPDF_PAGE قبلی بسته شده، و مقادیر FPDF_PAGEOBJECT که به آن تعلق داشتند pointerهایی به حافظه آزادشده هستند. TPdfPageObjectInfo handle بومی را در فیلد Handle خودش ارائه می‌دهد، که واقعاً برای دادن مستقیم یک شیء به یک فراخوانی سطح‌پایین‌تر مفید است، و به همان اندازه واقعاً خطرناک برای نگه داشتن در یک فیلد form یا یک فهرست در سراسر عملیاتی که صفحه را reload می‌کند. با یک رکورد snapshot طوری رفتار کنید که فقط تا فراخوانی بعدی که محتوا را دوباره تولید می‌کند معتبر است، در همان روحیه قوانین مالکیتی که در یادداشت‌های ABI و ایمنی حافظه در مرز PDFium بحث شده

آیا یک getter می‌تواند شکست بخورد و همچنان مثل داده معتبر به‌نظر برسد؟

بله، و این نیمه دوم همان مسئله است. FPDFPageObj_GetRotatedBounds و FPDFPageObj_GetIsActive getterهای out-parameter هستند: یک flag موفقیت int برمی‌گردانند و پاسخ واقعی را در یک آرگومان مرجع می‌نویسند. هر دو می‌توانند برای شیئی که ساخته شده اما صفحه‌اش هنوز دوباره parse نشده FALSE برگردانند. وقتی آن رخ می‌دهد پارامتر out دست‌نخورده رها می‌شود، و یک رکورد Pascal مقداردهی‌اولیه‌شده با Default(TPdfPageObjectInfo) همه‌صفر است، پس فراخواننده یک چهارضلعی با چهار نقطه در مبدأ و یک flag Active برابر False می‌بیند. یک فراخوانی ناموفق خاموش به داده قابل‌قبول‌به‌نظر ارتقا یافته

TPdfPageObjectInfo به این با sentinelهای صریح پاسخ می‌دهد. HasRotatedBounds نتیجه فراخوانی FPDFPageObj_GetRotatedBounds را حمل می‌کند، HasActiveState نتیجه FPDFPageObj_GetIsActive را حمل می‌کند، و فیلدهای هندسه و وضعیت فقط زمانی نوشته می‌شوند که sentinel متناظر True باشد. همان شکل در سراسر رکورد برای سایر getterهای out-parameter تکرار می‌شود، پس HasMatrix، HasFillColor، HasStrokeColor، و HasStrokeWidth همه همان چیز را معنا می‌دهند: فراخوانی بومی موفق شد و فیلد همسایه معنادار است

Info:= Pdf.PageObjectInfo(I);

if Info.HasRotatedBounds then
  // RotatedBounds is array [1..4] of TPdfPoint, in draw order
  UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
          Info.RotatedBounds[3], Info.RotatedBounds[4])
else
  // the native call failed; fall back to the axis-aligned rectangle
  UseRect(Info.Bounds);

if Info.HasActiveState and (not Info.Active) then
  SkipObject(I);         // genuinely inactive
// if HasActiveState is False, the object state is unknown, not inactive

این الگو به هر getter PDFiumای که قرارداد کد-بازگشتی-به‌علاوه-پارامتر-out را دنبال می‌کند تعمیم می‌یابد، و تعداد زیادی از آن‌ها هست. اگر یک wrapper آن قرارداد را در یک نتیجه تابع ساده فرو بریزد، تنها سیگنالی را که "پاسخ صفر است" را از "هیچ پاسخی نیست" جدا می‌کند دور ریخته. حمل یک boolean اضافی به‌ازای هر فیلد یک بایت هزینه دارد و کل یک دسته باگ را حذف می‌کند که در آن یک رکورد پیش‌فرض با یک اندازه‌گیری اشتباه گرفته می‌شود

جایی که این همچنان گاز می‌گیرد

سه محدودیت صادقانه. اول، تازه‌سازی به‌ازای هر صفحه است: صفحه دو را transform کنید و هر handleای که برای صفحه یک نگه داشته‌اید تحت‌تأثیر قرار نمی‌گیرد، اما اکنون دو صفحه دارید که در زمان‌های مختلف parse شده‌اند و به عهده شماست به یاد داشته باشید کدام snapshot از کدام آمده. دوم، پایداری اندیس در سراسر یک بازتولید محتوا تضمین نمی‌شود — پس از reload، اندیس ۳ هرچیزی است که اندیس ۳ در parse جدید است، پس اشیاء را با نوع و هندسه‌شان دوباره شناسایی کنید به‌جای فرض اینکه موقعیت‌ها ثابت مانده‌اند. سوم، مستطیل clip در FPDFPage_TransFormWithClip روی محتوای صفحه اعمال می‌شود و هیچ‌کدام از boxهای صفحه را دوباره اندازه نمی‌کند؛ اگر محتوا را برای ساختن یک حاشیه پایین بیاورید، MediaBox همچنان همان اندازه‌ای است که همیشه بوده، و یک viewer برگه اصلی را با نقاشی کوچک‌شده داخل آن نشان می‌دهد. هیچ‌کدام از این‌ها غیرمعمول نیست — پیامد عادی یک API C است که pointerهایی به وضعیت parse‌شده می‌دهد و طول عمر را به فراخواننده وامی‌گذارد. رفع همان چیزی است که در همه‌جای دیگر کار می‌کند: دقیقاً تعریف کنید یک snapshot کِی منقضی می‌شود، در آن مرز تازه کنید، و هرگز اجازه ندهید یک فراخوانی ناموفق خود را به‌جای یک مقدار جا بزند

اگر به‌طور کلی‌تر روی رفتار ماتریس کار می‌کنید، ترتیب ضرب که تعیین می‌کند یک transform کجا فرود می‌آید در مقاله prepend، append و pivot با ماتریس‌ها پوشش داده شده. APIهای transform و شیء صفحه که اینجا شرح داده شدند همراه با PDFium Component برای Delphi و C++Builder عرضه می‌شوند، که صفحه محصولش مرجع کامل رکورد snapshot شیء صفحه و فیلدهای sentinel آن را حمل می‌کند