در PDFium Component برای Delphi، روشن کردن BrotliEnabled یا IsolatePerDocument در TPdfLibraryConfiguration سابقاً build مربوط به Skia همراهشده را بدون هیچ خطایی به renderer یعنی AGG سواپ میکرد، چون هر دو گزینه FPDF_LIBRARY_CONFIG را به نسخهای میبرند که PDFium مقدار m_RendererType را عیناً میخواند. از v3.123.0 renderer پیشفرض همان پیشفرض خود DLL میماند، و از v3.125.0 درخواست Skia یا Fontations ای که DLL نتواند جوابش را بدهد یک EPdfError قابل-گرفتن میدهد بهجای کشتن پروسه
هیچکدام از دو باگ خودش را اعلام نکرد. اولی صفحههایی تولید میکرد که سالم به نظر میرسیدند، فقط با یک rasterizer دیگر رندر شده بودند، با anti-aliasing و لبههای متنی کمی متفاوت از build ای که شپ کرده و تست کرده بودید. دومی خودش را اعلام کرد، با صدای بلند، با انداختن پروسه میزبان از داخل مقداردهی اولیه بومی. هر دو از یک جا میآیند: یک ساختار C نسخهدار که فیلدهایش فقط وقتی میشمارند که شماره نسخه بگوید میشمارند، و مقدار صفرش هم «تنظیمنشده» نیست بلکه یک انتخاب واقعی است
FPDF_LIBRARY_CONFIG چطور تصمیم میگیرد PDFium از کدام renderer استفاده کند؟
FPDF_InitLibraryWithConfig فقط وقتی سراغ m_RendererType میرود که فیلد Version ساختار 4 یا بالاتر باشد، و از آن نسخه به بعد مقدار را عیناً همانطور که نوشته شده به کار میبرد. زیر نسخه 4 PDFium فیلد را نادیده میگیرد و پیشفرض build را برمیدارد، که در buildهای کامپایلشده با PDF_USE_SKIA مقدار Skia است و بقیه جاها AGG
هر فیلد بعدی همان الگو را دنبال میکند. ساختار دانهدانه یک قابلیت در هر بار بزرگ شد، و هر قابلیت همراه یک شماره نسخه جدید آمد. PDFium Component ساختار بومی را در LoadLibrary از TPdfLibraryConfiguration شما میسازد و نسخه را فقط تا جایی بالا میبرد که گزینههای ستشده شما لازم دارند
| نسخه ساختار | فیلدی که اضافه میکند | ستشده توسط |
|---|---|---|
| 2 | m_pIsolate، m_v8EmbedderSlot | همیشه نوشته میشود؛ V8Isolate، V8EmbedderSlot |
| 3 | m_pPlatform | وقتی V8Platform برابر nil نباشد |
| 4 | m_RendererType | وقتی Renderer چیزی جز prpDefault باشد |
| 5 | m_FontLibraryType | وقتی FontBackend چیزی جز pfbpDefault باشد |
| 6 | m_BrotliEnabled | وقتی BrotliEnabled = True |
| 7 | m_IsolatePerDocument | وقتی IsolatePerDocument = True |
تله در دو سطر آخر است. نسخهها تجمعیاند: یک ساختار نسخه 6 هم یک ساختار نسخه 4 است هم نسخه 5، پس PDFium مقدار m_RendererType و m_FontLibraryType را میخواند حتی اگر شما فقط Brotli خواسته باشید. هر چه در آن لحظه در آن دو فیلد باشد renderer و font backend میشود، چه بخواهیدشان انتخاب کرده باشید چه نه
چرا روشن کردن Brotli renderer را به AGG میبرد؟
قبل از v3.123.0، PDFium Component برای prpDefault مقدار FPDF_RENDERERTYPE_AGG را داخل m_RendererType مینوشت، پس هر پیکربندیای که ساختار را به نسخه 6 یا 7 میرساند AGG را روی یک build مبتنی بر Skia تحمیل میکرد. runtimeهای pdfium.dll و pdfium.v8.dll که با کامپوننت شپ میشوند buildهای Skia هستند، پس این مستقیم به deployment پیشفرض میخورد، نه به یک حالت عجیبوغریب
آن مپ کردن موقع نوشتن بیضرر به نظر میرسید. در نسخه 2 یا 3 فیلد هرگز خوانده نمیشود، پس prpDefault واقعاً یعنی «هر چه DLL انجام میدهد». لحظهای که BrotliEnabled (نسخه 6) یا IsolatePerDocument (نسخه 7) وارد ماجرا شد، همان کد «بیترجیح» را به یک درخواست صریح AGG تبدیل میکرد. هیچ چیز شکست نمیخورد. PDFium عادی مقداردهی اولیه میشد، هر صفحه را رندر میکرد، و هیچ کد خطایی برنمیگرداند، چون از دید خودش فراخواننده AGG خواسته بود و AGG گرفته بود
یک hash پیکسلی سواپ را جایی آشکار میکند که اسکرینشات نمیکند. رندر صفحه اول همان سند نمونه زیر سه پیکربندی این نتیجه را داد:
- پیکربندی پیشفرض: مقدار hash برابر
502D77C3711B4ACF -
BrotliEnabled= True با رها کردنRendererرویprpDefault: مقدار hash برابرF75B5EB4728ADE87 -
prpAggصریح: مقدار hash برابرF75B5EB4728ADE87، عین اجرای Brotli
فیکس در v3.123.0 تابع عمومی PdfNativeRendererType است که یک TPdfRendererPreference را به مقداری که در m_RendererType نوشته میشود حل میکند. prpAgg و prpSkia یک-به-یک مپ میشوند. prpDefault حالا وقتی DLL بارگذاریشده مقدار FPDF_RenderPageSkia را export کند Skia مپ میشود و بقیه حالتها AGG. آن export زیر همان شرط PDF_USE_SKIA کامپایل میشود که خود پیشفرض Skia، که آن را به تنها ویژگی buildی تبدیل میکند که از بیرون DLL قابل مشاهده است. بعد از فیکس پیکربندی Brotli همان hash پیکربندی پیشفرض را تولید میکند
font backend هرگز همین مشکل را نداشت. مقدار m_FontLibraryType از نسخه 5 به بعد خوانده میشود، و مقدار صفرش یعنی FPDF_FONTBACKENDTYPE_FREETYPE هم همان پیشفرض PDFium است وقتی فیلد اصلاً خوانده نشود. پس نوشتن FreeType برای pfbpDefault پیشفرض بومی را دقیقاً بازتولید میکند. مقادیر صفر همیشه غلط نیستند، فقط هرگز خودکار درست نیستند
با v3.123.0 یا بعدتر، کد راهاندازیای که بهطور طبیعی مینویسید حالا همان را میکند که میگوید:
uses
PDFium;
procedure ConfigurePdfiumAtStartup;
var
Config: TPdfLibraryConfiguration;
begin
// باید قبل از هر چیزی که کتابخانه بومی را load میکند اجرا شود
Config := TPdfLibraryConfiguration.Default;
Config.BrotliEnabled := True; // مقدار FPDF_LIBRARY_CONFIG را به نسخه 6 میبرد
// مقدار Renderer روی prpDefault میماند: روی buildهایی که export میکنند
// مقدار FPDF_RenderPageSkia به Skia و روی buildهای فقط-AGG به AGG حل میشود
SetLength(Config.UserFontPaths, 1);
Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
ConfigurePdfLibrary(Config);
end;
یادتان باشد BrotliEnabled فقط وقتی streamهای /BrotliDecode مربوط به PDF 2.0 را قابل-رمزگشایی میکند که خود DLL با PDF_ENABLE_BROTLI ساخته شده باشد. فلگ یک درخواست است، و روی buildی بدون پشتیبانی Brotli هیچ اثری ندارد. مقدار TPdfLibraryConfiguration.Hardened همان Default است جز اینکه AllowMachineTime برابر False است، که خواندن ساعت واقعی را از JavaScript سند میبندد؛ نقطه شروع معقولی برای پردازش سمت-سرور فایلهای نامطمئن است
وقتی backend ای را درخواست کنید که DLL ندارد چه میشود؟
PDFium برای renderer یا font backend ای که در build نیست خطا برنمیگرداند: FPDF_InitLibraryWithConfig یک CHECK بومی را شکست میدهد، که در Windows بهشکل یک breakpoint exception بیرون میزند و بدون یک structured exception handler دور فراخوانی، پروسه را خاتمه میدهد. هدر همین را میگوید، هشدار میدهد که یک مقدار پشتیبانینشده «به همین شکل با یک crash فوری شکست میخورد»
دو حالت ملموس یک build فقط-AGG هستند که FPDF_RENDERERTYPE_SKIA میگیرد، و یک build بدون Fontations که FPDF_FONTBACKENDTYPE_FONTATIONS میگیرد. runtime Skia همراهشده در گروه دوم است: با Skia رندر میکند اما برای فونتها از FreeType استفاده میکند. درخواست prpSkia همراه pfbpFontations مقابل آن در سمت دلفی مقدار External exception 80000003 تولید میکرد. وقتی دیباگر یا یک exception handler اتفاقاً آن را بگیرد، وضعیت همچنان غیرقابل-بازیابی است:
- PDFium نیمه-مقداردهی-اولیه رها میشود
- پیکربندی در-سطح-پروسه از قبل مهر شده، پس
ConfigurePdfLibraryاز پیکربندی اصلاحشده سر باز میزند - تلاش دوباره با پیکربندی متفاوت در همان پروسه دیگر ممکن نیست
این همان شکست معکوس باگ Brotli است. آنجا فیلد مقداری را حمل میکرد که هیچکس انتخابش نکرده بود و PDFium بیسروصدا میپذیرفتش. اینجا فیلد مقداری را حمل میکند که فراخواننده عمداً انتخابش کرده و PDFium هیچ بحثی سرش نمیکند. هر دو مشکلاتیاند که یک wrapper باید قبل از فراخوانی بومی حلشان کند، چون بعدش چیزی برای گرفتن نمانده
PDFium Component چطور Skia و Fontations را پیشچک میکند
از v3.125.0 به بعد، LoadLibrary پیکربندی را بعد از bind شدن exportهای DLL و قبل از صدا زدن FPDF_InitLibraryWithConfig اعتبارسنجی میکند، و یک renderer یا font backend پشتیبانینشده را به یک EPdfError با پیامی تبدیل میکند که تنظیم متخلف و جایگزینها را نام میبرد. DLL unload میشود و پیکربندی بازمهر میشود، پس فراخواننده میتواند تنظیمات دیگری بردارد و دوباره load کند
خود تصمیم در تابع خالص PdfLibraryConfigurationSupportError زندگی میکند، که پیکربندی بهعلاوه دو Boolean را میگیرد که build را توصیف میکنند و وقتی ترکیب امن است یک رشته خالی برمیگرداند. چون هیچ state بومی را لمس نمیکند، میتوانید از تستهای خودتان با هر ترکیب قابلیت صداش کنید. داخل LoadLibrary آن دو Boolean از انواع مختلف شواهد میآیند و سطح اعتماد متفاوتی میارزند:
- Skia از حضور export یعنی
FPDF_RenderPageSkiaتشخیص داده میشود، همان سیگنالی کهPdfNativeRendererTypeاستفاده میکند. export و renderer مربوط به Skia زیر یک شرط کامپایل میشوند، پس چک دقیق است - Fontations export مخصوص خودش را ندارد. تنها ردی که میگذارد crateهای فونت Rust است که داخل باینری میکشد، پس PDFium Component فایل کتابخانه بارگذاریشده را برای نامهای crate یعنی
skrifaوread-fonts(وread_fonts) اسکن میکند. اسکن فقط وقتیpfbpFontationsدرخواست شده اجرا میشود، و فایلی که نشود خواند «بدون Fontations» حساب میشود
چک Fontations یک heuristic است و میتواند در یک جهت غلط دربیاید: یک build مبتنی بر Fontations که از هر یک از آن رشتهها لخت شده باشد رد میشود حتی اگر میتوانست کار کند. آن معامله عمداً بسته شد. یک رد غلط هزینهاش یک exception است که میتوانید بگیرید و یک fallback به FreeType. یک پذیرش غلط هزینهاش پروسه است
باز-مهر شدن به اندازه خود چک مهم است. LoadLibrary پیکربندی را همان اول load مهر میکند، پس بدون reset یک رد قابلیت ConfigurePdfLibrary را میگذاشت به هر تلاش دوبارهای با EPdfError جواب میداد «پیکربندی کتابخانه PDFium از قبل مهر شده است». مسیر رد اول UnloadLibrary را صدا میزند؛ فراخوانی FPDF_DestroyLibrary اش در آن نقطه امن است چون PDFium هنوز مقداردهی اولیه نشده و بلافاصله برمیگردد. بقیه شکستهای load، مثل DLL مفقود یا ناهمخوانی معماری، مهر را نگه میدارند، پس یک حلقه تلاش-مجدد باید این دو را از هم تشخیص بدهد:
uses
SysUtils, PDFium;
function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
Config: TPdfLibraryConfiguration;
begin
Config := TPdfLibraryConfiguration.Default;
Config.Renderer := prpSkia;
ConfigurePdfLibrary(Config);
try
PDFium.LoadLibrary; // واجد-یونیت: مقدار Windows.LoadLibrary همنام است
Result := prpSkia;
except
on E: EPdfError do
begin
// یک رد قابلیت DLL را unload و پیکربندی را بازمهر میکند.
// DLL ای که اصلاً load نشد مهرشده میماند: تلاش دوباره فایده ندارد
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
به PDFium.LoadLibrary صریح توجه کنید. در یونیتی که هم از Windows یا Winapi.Windows استفاده میکند، یک LoadLibrary بدون واجد-یونیت به هر یونیتی حل میشود که در clauses استفادهها آخر آمده؛ وقتی آن تابع Win32 است، فراخوانی بدون-آرگومان با یک خطای تعداد-آرگومان کامپایل نمیشود که هیچ چیزی درباره PDFium نمیگوید
اعتبارسنجی که حتی زودتر میافتد
مقدار ConfigurePdfLibrary بعضی ترکیبها را قبل از اینکه اصلاً DLLی در ماجرا باشد رد میکند، همه با EPdfError. یک FontBackend صریح، از جمله pfbpFreeType، Renderer = prpSkia میخواهد، چون PDFium فقط برای renderer مربوط به Skia سراغ font backend میرود. IsolatePerDocument میخواهد V8Isolate برابر nil باشد، چون PDFium بهازای هر سند isolate خودش را میسازد و اگر شما هم یکی دستش بدهید یک CHECK بومی را شکست میدهد. رشتههای خالی در UserFontPaths رد میشوند. و هر فراخوانی بعد از اولین تلاش load با «پیکربندی کتابخانه PDFium از قبل مهر شده است» شکست میخورد
آن قاعده آخر یک نتیجه عملی دارد: نمیتوانید اول DLL را probe کنید و بعدش پیکربندیاش کنید. GetSkiaRenderCapabilities و V8FeaturesAvailable و باز کردن یک سند و بیشتر بقیه نقاط ورود در درون LoadLibrary را صدا میزنند، که پیکربندی را همانجا مهر میکند. صدا زدن UnloadLibrary بعداً هم آن را دوباره باز نمیکند. اول پیکربندی کنید، بعد load کنید، بعد سؤال بپرسید، که دقیقاً ترتیبی است که یک روتین عیبیابی باید دنبال کند:
uses
SysUtils, PDFium, FPdfView;
function DescribePdfiumState: string;
var
Config: TPdfLibraryConfiguration;
Renderer: string;
begin
Config := GetPdfLibraryConfiguration; // یک کپی، امن برای بازرسی
if not PDFium.Loaded then
begin
if PdfLibraryConfigurationSealed then
Exit('PDFium failed to load; configuration is sealed');
Exit('PDFium not loaded; configuration can still change');
end;
// همان حلوتقلیبی که LoadLibrary موقع ساختن FPDF_LIBRARY_CONFIG اعمال کرد
if PdfNativeRendererType(Config.Renderer,
GetSkiaRenderCapabilities.PageRender) = FPDF_RENDERERTYPE_SKIA then
Renderer := 'Skia'
else
Renderer := 'AGG';
Result := Format('Renderer=%s Brotli=%s IsolatePerDocument=%s',
[Renderer, BoolToStr(Config.BrotliEnabled, True),
BoolToStr(Config.IsolatePerDocument, True)]);
end;
لاگ کردن آن یک خط در راهاندازی ارزان است، و اولین چیزی است که در یک تیکت پشتیبانی میخواهید که میگوید «متن روی سرور متفاوت به نظر میرسد». PDFium.Loaded به همان دلیل LoadLibrary واجد-یونیت است: داخل یک متد فرم یا کامپوننت، یک Loaded لخت به TComponent.Loaded bind میشود
دو راهی که یک ساختار پیکربندی C نسخهدار خراب میشود
هر ساختار پیکربندی نسخهدار، چه FPDF_LIBRARY_CONFIG باشد چه یک رکورد cbSize مربوط به Win32 چه یک ABI افزونه، به دو شکل متقارن شکست میخورد، و یک wrapper باید در مقابل هر دو نگهبانی کند. اولی پر کردن یک فیلد در حالی که نسخه خیلی پایین مانده؛ دومی بالا بردن نسخه در حالی که یک فیلد روی مقدار صفر مانده که کتابخانه آن را یک انتخاب عمدی میخواند
- فیلد ست شده، نسخه خیلی پایین. مقدار
m_BrotliEnabled= 1 را داخل یک ساختار نسخه 2 بنویسید و PDFium هرگز سراغش نمیرود. فراخوانی موفق میشود و streamهای Brotli قابل-رمزگشایی نمیمانند. دفاع، استنتاج نسخه از فیلدهایی است که واقعاً در استفادهاند، که همان کاری است کهLoadLibraryمیکند، نه هاردکد کردن یکی - نسخه بهاندازه کافی بالا، فیلد صفر یعنی چیزی. نسخه را به 6 برسانید و هر فیلدی تا نسخه 6 حالا زنده است. مقدار
FillCharمقدارm_RendererTypeرا صفر میکند که یعنیFPDF_RENDERERTYPE_AGG، یک renderer واقعی، نه «تنظیمنشده». دفاع این است که هر فیلدی که نسخه انتخابی پوشش میدهد با یک مقدار عمدی نوشته شود، و «پیشفرض» مقابل build واقعی حل شود نه اینکه فرضش کنید
برای مقادیری که میتوانند callee را کرش کنند یک قاعده سوم دنبال میشود: قبل از فراخوانی مقابل چیزی که باینری میتواند انجام دهد اعتبارسنجیشان کنید، با قویترین شواهد موجود، و در کد و مستندات صادق باشید وقتی آن شواهد یک heuristic است. یک نماد exportشده اثبات است. یک نام crate در جدول رشتهها یک حدس خوب
مرجع سریع: پیکربندی کتابخانه در PDFium Component
- مقدار
ConfigurePdfLibraryرا یک بار، قبل از هر چیزی که DLL را load میکند، صدا بزنید؛ هر کوئری قابلیت یا load سندی آن را مهر میکند - اگر
BrotliEnabledیاIsolatePerDocumentرا ست میکنید و از runtimeهای همراهشده انتظار خروجی Skia دارید به v3.123.0 یا بعدتر ارتقا بدهید - مقدار
Rendererرا رویprpDefaultرها کنید مگر اینکه rasterizer خاصی لازم داشته باشید؛ حالا در هر نسخه ساختاری به پیشفرض build حل میشود - از
PdfNativeRendererTypeباGetSkiaRenderCapabilities.PageRenderاستفاده کنید تا لاگ کنید کدام renderer واقعاً فعال است - برای
prpSkiaروی یک DLL فقط-AGG یاpfbpFontationsروی یک DLL بدون-Fontations در v3.125.0 یا بعدتر انتظارEPdfErrorداشته باشید، نه crash - بعد از یک رد قابلیت، مقدار
PdfLibraryConfigurationSealedبرابر False است و میتوانید دوباره پیکربندی کنید؛ بعد از یک load ناموفق DLL همچنان True میماند - تشخیص Fontations را heuristic بگیرید و یک fallback به FreeType نگه دارید
- مقدارهای
PDFium.LoadLibraryوPDFium.Loadedرا با نام یونیت بنویسید تا از برخورد نام با Win32 وTComponentدر امان باشید
اگر DLL قبل از اینکه پیکربندی اصلاً اهمیت پیدا کند شکست بخورد، از عیبیابی شکستهای load مربوط به DLL یعنی PDFium در دلفی شروع کنید، و برای اینکه کامپوننت روی هر پلتفرم چطور باینری درست را پیدا میکند load کردن کتابخانه بومی PDFium روی هر target را ببینید. وقتی renderer سر و مر شد، کش رندر و تاکتیکهای زوم روان پوشش میدهد چطور رندر صفحه را در یک viewer سریع نگه دارید
PDFium Component موتور PDFium را برای Delphi و C++Builder با چکهای پیکربندی مثل اینها wrap میکند، تا مقداردهی اولیه بومی بهشکل یک exception در پاسکال شکست بخورد که میتوانید هندلش کنید نه یک خروج پروسه. جزئیات محصول و دانلودها در صفحه محصول PDFium Component برای Delphi است