مقاله فنی

بارگذاری بازه بایت برای PDFهای embed شده در Delphi با PDFium

PDFium Component می‌تواند یک PDF را که داخل یک بافر بزرگ‌تر زندگی می‌کند مستقیماً از یک بازه بایت باز کند. overload LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) یک پنجره را در جا آدرس‌دهی می‌کند، پس هیچ Copy مقدماتی لازم نیست. در عوض از شما می‌خواهد یک قانون را بفهمید: وقتی Buffered برابر False است، آرایه پشتیبان قرض گرفته شده، نه کپی شده

این مکانیزمی متفاوت از رویکرد callback-محور است که در streaming کردن PDFهای بزرگ روی تقاضا با PDFium VCL شرح داده شده، که یک reader FPDF_FILEACCESS به PDFium می‌دهد و می‌گذارد بلوک‌ها را همان‌طور که نیاز دارد از دیسک بکشد. آن یکی برای اسنادی است که برای نگه‌داشتن در RAM خیلی بزرگ‌اند. این یکی برای اسنادی است که از قبل در RAM هستند، در یک offset معلوم داخل چیز دیگری نشسته‌اند. این دو مکمل یکدیگرند، و بخش آخر توضیح می‌دهد کدام موقعیت به کدام تعلق دارد

کپی ۴۰ مگابایتی‌ای که هیچ‌کس درخواستش نکرده

این سناریو هرجا PDFها داخل فرمت‌های دیگر سفر می‌کنند ظاهر می‌شود. یک mail store بدنه پیام‌ها و پیوست‌ها را در یک رکورد نگه می‌دارد. یک container آرشیو یک manifest، چند تصویر و یک PDF را به‌هم می‌چسباند. یک پروتکل سیم سفارشی یک سند را پشت یک هدر length-prefixed فریم می‌کند. در هر مورد شما با نگه‌داشتن یک TBytes بزرگ و دانستن اینکه PDF از بایت ۱٬۱۸۲٬۳۳۶ شروع می‌شود و ۳۱۲ کیلوبایت طول دارد پایان می‌یابید

پیش از وجود overload بازه بایت، پاسخ اصطلاحی Copy(Data, Index, Count) بود، که یک آرایه دوم تخصیص می‌دهد و پنجره را با memcpy داخل آن می‌کند. سپس آن برش را به LoadDocument با Buffered = True می‌دهید، که آن را دوباره داخل بافر خصوصی کامپوننت کپی می‌کند. دو کپی از همان بایت‌ها، یکی از آن‌ها تشریفات محض، و روی یک اسکن mailbox بزرگ به‌ازای هر پیام تکرار می‌شود. overload بازه بایت کپی اول را بی‌قید‌وشرط و دومی را اختیاری حذف می‌کند

overload بازه بایت واقعاً چه می‌کند

overload طبق طراحی نازک است: اعتبارسنجی می‌کند، یک pointer محاسبه می‌کند، و به فرم pointer LoadDocument که کل خانواده از قبل از آن عبور می‌کند تفویض می‌کند. Index صفر-پایه است، Count یک طول بایت است، و Buffered دقیقاً همان‌طور که در سایر overloadها می‌کند به‌طور پیش‌فرض True است. LoadDocument(const Data: TBytes; Buffered: Boolean) تک-آرگومان اکنون خودش فقط یک فراخوانی به این یکی با Index = 0 و Count = Length(Data) است، پس یک مسیر اعتبارسنجی وجود دارد نه دو تا

فراخوانی آن شبیه کدی است که از قبل می‌نوشتید، منهای برش

var
  Frame: TBytes;          // whole container record, tens of megabytes
  Offset, Size: Integer;
begin
  Frame := LoadContainerRecord('mailbox.dat');
  LocateEmbeddedPdf(Frame, Offset, Size);   // your container parser

  // No Copy(Frame, Offset, Size) here - the window is addressed in place
  Pdf.LoadDocument(Frame, Offset, Size, True);
  try
    RenderPreview(Pdf);
  finally
    Pdf.UnloadDocument;
  end;
end;

چرا Index به‌علاوه Count بررسی مرز را overflow می‌کند؟

چون Index و Count هر دو Integer هستند، و مجموع دو مقدار Integer مثبت بزرگ لزوماً یک Integer مثبت بزرگ نیست. این هسته فنی overload است، و یک جای واحدی است که یک بررسی طبیعی‌به‌نظر یک سوراخ ایمنی حافظه است. فرم بدیهی اشتباه است

// WRONG: Index + Count is evaluated in Integer and can wrap negative
if Index + Count <= Length(Data) then
  DataPtr := @Data[Index];

// RIGHT: reject signs first, then bound each term separately,
// with the only arithmetic done as a subtraction that cannot wrap
Check(Index >= 0,  'PDF byte range index cannot be negative');
Check(Count >= 0,  'PDF byte range count cannot be negative');
Check(Index <= Length(Data), 'PDF byte range index exceeds data length');
Check(Count <= Length(Data) - Index, 'PDF byte range exceeds data length');

حالت شکست را جلو ببرید. Index = 2000000000 و Count = 2000000000 را بگیرید. مجموع واقعی آن‌ها چهار میلیارد است، اما در حساب علامت‌دار ۳۲بیتی نتیجه دقیقاً به منفی ۲۹۴٬۹۶۷٬۲۹۶ می‌پیچد. آن مقدار به‌راحتی کمتر از Length(Data) است، پس بررسی اشتباه عبور می‌کند، @Data[Index] بسیار بیرون از آرایه گرفته می‌شود، و PDFium یک pointer وحشی به‌علاوه یک طول دو-گیگابایتی می‌گیرد. آنچه دنبال می‌شود در یک روز خوب یک access violation و در یک روز بد parsing خاموش حافظه بی‌ربط process است

ترتیب درست این را با هرگز جمع نکردن رفع می‌کند. منفی‌ها پیش از آنکه هرچیزی اندیس‌گذاری شود رد می‌شوند، پس @Data[Index] هرگز نمی‌تواند زیر آرایه گرفته شود. سپس Index به‌تنهایی در برابر Length(Data) محدود می‌شود، که تضمین می‌کند Length(Data) - Index یک Integer غیرمنفی است. فقط پس از آن Count در برابر آن باقیمانده مقایسه می‌شود. هر مقدار میانی داخل بازه قابل‌نمایش می‌ماند، پس هیچ پیکربندی build نمی‌تواند نتیجه را تغییر دهد. وسوسه نشوید به بررسی overflow با {$Q+} به‌عنوان تور ایمنی هم اعتماد کنید: build های release معمولاً با آن خاموش عرضه می‌شوند، و حتی وقتی روشن است شما یک باگ ایمنی حافظه را به یک EIntOverflow که از میان یک رویه اعتبارسنجی فرار می‌کند تبدیل کرده‌اید. PDFium Component حساب طول غیرقابل‌اعتماد را همان‌طور رفتار می‌کند که با باقی مرز رفتار می‌کند، انضباطی که به‌طور گسترده‌تر در سخت‌کردن ABI PDFium VCL و ایمنی حافظه در Delphi پوشش داده شده

چرا یک پنجره طول-صفر باید nil بگذرد؟

چون @Data[Index] برای هر Indexای که اعتبارسنجی می‌پذیرد یک عبارت قانونی نیست. Index = Length(Data) با Count = 0 یک پنجره خالی کاملاً خوش‌فرم در انتهای بافر است، و یک TBytes خالی Index = 0 را روی آرایه‌ای می‌دهد که اصلاً هیچ عنصر صفری ندارد. گرفتن آدرس در هر دو حالت از انتها فراتر اندیس می‌زند، یا یک آرایه پویای nil را dereference می‌کند. پس overload شاخه می‌زند: Count = 0 یک pointer nil می‌دهد، هر تعداد دیگری @Data[Index] می‌دهد. nil سپس به overload pointer جریان می‌یابد، که نگهبان خودش وقتی اندازه صفر است یک pointer nil را می‌پذیرد، و بارگذاری به خطای معمولی "Cannot load PDF document" ختم می‌شود نه یک access violation. یک فراخواننده که یک پنجره صفر-بایتی را از یک container ناقص محاسبه کرده یک EPdfError تمیز و قابل‌گرفتن مانند هر ورودی بد دیگری می‌گیرد

قرضی یا کپی‌شده: چیزی که Buffered تصمیم می‌گیرد

Buffered قرارداد مالکیت را انتخاب می‌کند، و تنها پارامتری است اینجا که پیامدهایی فراتر از فراخوانی دارد. با Buffered = True، PDFium Component پنجره انتخاب‌شده را، و فقط پنجره را، پیش از بارگذاری داخل بافر داخلی خودش کپی می‌کند. container ۴۰ مگابایتی کپی نمی‌شود؛ PDF ۳۱۲ کیلوبایتی کپی می‌شود. به‌محض بازگشت LoadDocument می‌توانید بلافاصله container را آزاد، دوباره‌استفاده یا رونویسی کنید، چون کامپوننت دیگر به آن ارجاع نمی‌دهد. این پیش‌فرض و انتخاب درست برای تقریباً هر کد است

Buffered = False مستقیماً @Data[Index] را به FPDF_LoadMemDocument64 می‌دهد، و PDFium آن pointer را برای طول عمر سند نگه می‌دارد به‌جای کپی کردن بایت‌ها. این بارگذاری را بدون تخصیص می‌کند، و کل TBytes پشتیبان را یک منبع قرضی می‌کند. باید تا اجرای UnloadDocument یا شدن Active به False زنده و بدون‌تغییر بماند. نه فقط پنجره، کل آرایه: یک آرایه پویا به‌عنوان یک واحد reference-counted است، و اجازه دادن به آخرین ارجاع که در هر جایی از کد شما برود حافظه‌ای را که PDFium هنوز می‌خواند آزاد می‌کند. تنظیم Length روی آن به همان اندازه مرگبار است، چون یک reallocation می‌تواند بلوک را جابه‌جا کند. این را در مستندات API خودتان هرجا چنین بارگذاری‌ای را ارائه می‌دهید بیان کنید، در همان روحیه هر مرز قرض-در-برابر-مالکیت دیگری در کد Pascal؛ حالت شکست یکسان با خطرات aliasing است که در نشت FillChar و رشته نتیجه در Delphi شرح داده شده، جایی که یک بافر مالکیت‌دار به‌نظر می‌رسد و نیست

type
  TFrameSession = class
  private
    FFrame: TBytes;   // owns the backing storage for as long as FPdf is loaded
    FPdf: TPdf;
  public
    procedure OpenEmbedded(Offset, Size: Integer);
    destructor Destroy; override;
  end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  // Buffered = False: FFrame must outlive the loaded document
  FPdf.LoadDocument(FFrame, Offset, Size, False);
end;

destructor TFrameSession.Destroy;
begin
  FPdf.UnloadDocument;   // release the borrow first
  FFrame := nil;         // only now may the storage go
  inherited;
end;

وقتی پنجره بازه بایت ابزار اشتباه است

درباره مرز صادق باشید. overload بازه بایت فرض می‌کند container از قبل کاملاً در حافظه است، و Count یک Integer است، پس یک پنجره واحد نمی‌تواند از دو گیگابایت فراتر رود. اگر container یک آرشیو ۶ گیگابایتی روی دیسک باشد، یا از طریق یک socket برسد که نمی‌توانید rewind کنید، این overload نمی‌تواند به شما کمک کند و خواندن کل چیز در TBytes فقط برای آدرس‌دهی یک پنجره داخل آن کل هدف را شکست می‌دهد. آنجاست که دقیقاً مسیر FPDF_FILEACCESS تعلق دارد، و مقاله streaming روی تقاضا نشان می‌دهد چگونه یک نمای offset-shifted از یک فایل را به‌عنوان یک منبع سند سفارشی ارائه دهید. به همان اندازه، اگر بایت‌های embed شده پیش از دیدن PDFium نیاز به تبدیل داشته باشند، decompression، رمزگشایی، یک گام unwrapping، آنگاه یک کپی واقعی اجتناب‌ناپذیر است و Buffered = True روی آرایه تبدیل‌شده پاسخ صادقانه است. پنجره بازه بایت دقیقاً در یک شکل سود می‌دهد: بایت‌های PDF پیوسته، بدون‌تغییر، از قبل resident، در یک offset معلوم

اگر دارید این را برای یک viewer، یک پنل preview یا یک pipeline دریافت batch ارزیابی می‌کنید، overload بازه بایت و loader streaming دو تا از استراتژی‌های بارگذاری هستند که PDFium Component در کنار بارگذاری‌های فایل، stream و pointer خام عرضه می‌کند. سطح کامل API، مجوزدهی و پشتیبانی نسخه Delphi و C++Builder روی صفحه محصول PDFium Component مستند شده