مقاله فنی

rendering چند-engineی PDF در Delphi: راهنمای PDF Library for Delphi

سه rasterizer می‌توانند همان PDF را بخوانند و دربارهٔ آنچه می‌گوید مخالفت کنند. engine built-in در PDF Library for Delphi آنی است که بدون file اضافی ship می‌شود و هر چیزی را به‌شکل شایسته‌ای render می‌کند، که چرایی آن است که slot پیش‌فرض را earn می‌کند. Cairo یک pipeline متفاوت برای transparency و anti-aliasing می‌آورد، و تمایل دارد آنی باشد که مردم وقتی soft maskها یا blend modeها جای دیگری اشتباه درمی‌آیند به سراغش می‌روند. PDFium code rendering در Chrome را حمل می‌کند، پس pageای که در یک browser درست به‌نظر می‌رسد معمولاً تحت PDFium هم درست به‌نظر می‌رسد، به بهای یک DLL بزرگ و یک bitness که اصرار دارد match باشد. هیچ‌یک از سه‌تا به‌طور انتزاعی درست نیستند. درستی per document است، و تنها راه honest برای فهمیدن اینکه کدام engine یک corpus داده‌شده را handle می‌کند، اجرای آن corpus از طریق هر کدام از آن‌هاست

این، دلیلی برای در نظر گرفتن engine به‌عنوان یک انتخاب runtime به‌جای build-time است. PDF Library for Delphi، کتابخانهٔ PDF در Delphi و C++Builder از losLab، هر سه را پشت یک rendering surface واحد می‌گذارد تا تصمیم یک integer به‌جای یک code branch هزینه داشته باشد. بقیهٔ ماجرا به انتخاب امن بین آن‌ها، تایید اینکه یک binary deployed واقعاً کدام engineها را حمل می‌کند، و نگه‌داشتن rendering state از مسموم‌کردن بی‌صداjob بعدی برمی‌گردد

سه rasterizer پشت یک call surface

کتابخانه engineهای خود را شماره‌گذاری می‌کند. engine ۱ renderer built-in است، پیش‌فرض، با optionهای smoothing در GDI+ روی Windows. engine ۲ Cairo است و engine ۳ PDFium، هر دو در runtime از طریق SelectRenderer انتخاب می‌شوند. دو engine خارجی از DLLهایی load می‌شوند که pathشان را قبل از انتخاب‌کردن با SetCairoFileName و SetPDFiumFileName تأمین می‌کنید. هر engineای که active باشد، کار از طریق همان callها می‌رود: RenderPageToFile، RenderPageToStream، RenderDocumentToFile. سوییچ کردن engineها یک number را move می‌کند؛ بقیهٔ code rendering شما هرگز متوجه نمی‌شود

مدل destination فراتر از bitmap می‌رود. class renderer همچنین metafileها (WMF، EMF، EMF+)، EPS، device context مستقیم، printerها و HTML5 را target می‌کند، با Cairo و PDFium که فقط وقتی compile شده‌اند به‌عنوان destination اضافه ظاهر می‌شوند. output رستری جایی است که سه engine به‌وضوح‌ترین اختلاف دارند، پس آن است که exampleهای اینجا استفاده می‌کنند

سه موتور رندر PDF پشت یک سطح فراخوانی: SelectRenderer میان موتور داخلی، Cairo و PDFium سوییچ می‌کند در حالی که کد برنامه همان توابع رندر را صدا می‌زند
SelectRenderer با یک عدد صحیح، کار را میان موتورهای داخلی و Cairo و PDFium جابه‌جا می‌کند. کد برنامه به فراخوانی RenderPageToFile و امثال آن ادامه می‌دهد فارغ از اینکه کدام موتور پیکسل‌ها را ساخته است

هرگز وجود یک engine را فرض نکنید: در startup probing کنید

Cairo و PDFium featureهای conditional compilation هستند، که یعنی یک binary می‌تواند کاملاً بدون آن‌ها build شود. وقتی آن می‌شود، درخواست engine ۲ یا ۳ چیزی raise نمی‌کند. SelectRenderer ساده‌اند یک value غیر از IDای که درخواست کردید برمی‌گرداند، و codeای که return value را نادیده می‌گیرد با هر engineای که از قبل active بود به render کردن ادامه می‌دهد. دفاع یک startup probe است که از هر engine می‌خواهد خودش را معرفی کند و پاسخ را ضبط می‌کند:

function ProbeEngines(PDF: TPDFlib): string;
begin
  Result := 'built-in';                        // engine 1 همیشه حاضر است
  if (PDF.SetCairoFileName('cairo.dll') = 1) and (PDF.SelectRenderer(2) = 2) then
    Result := Result + ', cairo';
  if (PDF.SetPDFiumFileName('pdfium.dll') = 1) and (PDF.SelectRenderer(3) = 3) then
    Result := Result + ', pdfium';
  PDF.SelectRenderer(1);                       // پیش از کار واقعی پیش‌فرض را بازیابی کن
end;

آن probe را یک‌بار در startup اجرا کنید و نتیجه‌اش را کنار هر render job در log بنویسید. رایج‌ترین سوال وقتی یک مشتری یک تفاوت rendering گزارش می‌کند این است که installation او واقعاً کدام engineها را دارد، و یک پاسخ یک‌خطی نشسته در log آن را بدون یک session remote-desktop تسویه می‌کند. یک side effect مفید: اگر خود SetPDFiumFileName ۰ برگرداند، از قبل می‌دانید مشکل DLL است (path اشتباه، bitness اشتباه، یک dependency مفقود) به‌جای یک binary compile‌شده بدون پشتیبانی PDFium، چون call path قبل از آنکه SelectRenderer اصلاً اجرا شود چیزی resolve نکرد

ده format خروجی پشت یک integer در Options

parameter Options در callهای render، encoding خروجی را انتخاب می‌کند: ۰ BMP، ۱ JPEG، ۲ WMF، ۳ EMF، ۴ EPS، ۵ PNG، ۶ GIF، ۷ TIFF، ۸ EMF+ و ۹ HTML5. PNG (5) default معقول برای previewها و page imageهای archival است. JPEG (1)، جفت‌شده با SetJPEGQuality، انتخاب بهتری برای scanهای photographic است که در آن file size بیش از edgeهای crisp مهم است

یک format نیازمندی دربارهٔ stream target پنهان می‌کند. path در BMP اول دادهٔ image را می‌نویسد، سپس به offset 0x26 برمی‌گردد تا fieldهای resolution در header را patch کند. آن را به یک stream forward-only، یک wrapper فشرده‌سازی یا یک network socket نشانه بگیرید، و call به شکلی fail می‌شود که شبیه یک engine fault خوانده می‌شود ولی نیست. وقتی یک target غیرقابل‌seek اجتناب‌ناپذیر است، به‌جای آن PNG render کنید، یا BMP را از طریق یک memory stream stage کنید و وقتی کامل شد آن را forward copy کنید

DPIای که پاس می‌دهید DPIای نیست که می‌گیرید

هر render call یک argument DPI می‌گیرد، اما resolutionی که واقعاً می‌گیرید آن value ضرب در render scale سراسری است. SetRenderScale از ۱.۰ شروع می‌شود، و یک‌بار تغییرش دادید factor جدید بی‌صدا به هر render بعدی روی آن instance اعمال می‌شود:

PDF.SetRenderScale(2.0);                    // هر render بعدی دو برابر می‌شود
PDF.RenderPageToFile(150, 1, 5, 'p1.png');  // عملاً 300 DPI
PDF.SetRenderScale(1.0);                    // ریست کن، وگرنه thumbnailهایت غول‌پیکر می‌رسند

همان چسبناکی به SetRenderCropType و تنظیم JPEG quality اعمال می‌شود. در یک service که thumbnail، preview و imageهای print-resolution را از یک instance shared تولید می‌کند، این تنظیمات به‌جا‌مانده واقعاً پشت ticketهای گاه‌به‌گاه «thumbnailها ناگهان ۴۰ MB شدند» هستند. دو راه clean بیرون: state مربوط را در بالای هر operation reset کنید، یا یک instance مجزا به هر profile خروجی اختصاص دهید تا هیچ‌چیز بین آن‌ها leak نکند

PDF Library for Delphi: فلوچارت کاوش موتور در راه‌اندازی: هر renderer مسیر DLL و پاسخ SelectRenderer خود را تأیید می‌کند و سپس خلاصه در دسترس بودن در کنار هر کار رندر لاگ می‌شود
فراخوانی path ناموفق، DLL را محکوم می‌کند، در حالی که نتیجه ناهمخوان SelectRenderer یعنی باینری هرگز موتور را در کامپایل نیاورده است. پروب یک بار اجرا می‌شود و خلاصه تک‌خطی‌اش بیشتر پرسش‌های رندر مشتریان را حل می‌کند

tune کردن engine پیش‌فرض پیش از رسیدن به یکی دیگر

یک سهم غافلگیرکننده از درخواست‌های «ما به یک engine متفاوت نیاز داریم» مشکل تنظیمات در لباس مبدل درمی‌آیند. renderer built-in رفتار smoothing خود را از طریق SetGDIPlusOptions و خانوادهٔ گسترده‌تر SetRenderOptions expose می‌کند، و SetGDIPlusFileName به شما اجازه می‌دهد آن را به یک runtime خاص در GDI+ هدایت کنید وقتی یک محیط deployment یک runtime غیرعادی ship می‌کند. line art jagged در DPI پایین، text فازی در thumbnailها، banding در سراسر gradientها: همهٔ این‌ها به آن knobها پاسخ می‌دهند، و turn کردنشان هیچ هزینه‌ای در installer ندارد. افزودن Cairo یا PDFium، در مقابل، یعنی ship کردن DLLهای بیشتر، track کردن یک variant bitness دوم یا سوم، و مالک‌شدن تعهد به update کردن آن‌ها

پس یک شکایت کیفیت یک ترتیب طبیعی از operationها دارد. اول آن را در DPI و scale دقیق مشتری reproduce کنید، چون نیمی از وقت تفاوت یک‌بار آن‌ها match شوند تبخیر می‌شود. بعد optionهای smoothing engine built-in را امتحان کنید. فقط آن‌گاه page را side by side بین engineها با هر متغیر دیگر ثابت نگه‌داشته بگذارید: آن را به PNG از طریق engineهای ۱، ۲ و ۳ در DPI یکسان render کنید و هر سه را attach کنید. معمولاً دو تا از سه تا موافق‌اند، و آن اکثریت به شما می‌گوید آیا outlier، document است که متفاوت تفسیر می‌شود یا baseline expectation خودتان که off است. سه image ملموس یک اختلاف «render اشتباه» را بسیار سریع‌تر از یک پاراگراف صفت تسویه می‌کند

یک fallback chain که خودش را توضیح می‌دهد

یک‌بار probing و discipline state در جای خود باشند، خود fallback chain کوتاه است. detection یک failure به LastRenderError تکیه می‌کند، که متن message خود engine را برای آخرین render نگه می‌دارد و وقتی render موفق بود خالی است:

procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);
begin
  PDF.SelectRenderer(1);                            // اول built-in
  PDF.RenderPageToFile(200, Page, 5, OutFile);      // 5 = PNG
  if PDF.LastRenderError = '' then Exit;
  LogEngineFailure('built-in', Page, PDF.LastRenderError);
  if PDF.SelectRenderer(3) = 3 then                 // PDFium به‌عنوان fallback سنگین
  begin
    PDF.RenderPageToFile(200, Page, 5, OutFile);
    if PDF.LastRenderError = '' then Exit;
    LogEngineFailure('pdfium', Page, PDF.LastRenderError);
  end;
  raise Exception.CreateFmt('Page %d failed on all available engines', [Page]);
end;

دو design point اینجا وزن دارند. chain ضبط می‌کند چرا هر switch رخ داد، چون یک line log که می‌خواند «این page از release 3.7 به PDFium fallback شد» یک regression signal است که می‌خواهید در monitoring trending باشد به‌جای گم‌شده. خود ترتیب fallback یک policy است که ارزش انتخاب per workload دارد. engine built-in بدون DLL اضافی deploy می‌شود، که آن را به first try درست در بیشتر installationها تبدیل می‌کند، در حالی که documentهای سنگین با transparency groupها یا shading غیرعادی دلیل معمولی هستند که یک تیم اصلاً یک engine جایگزین را سیم‌کشی می‌کند. هیچ engineای به‌طور کلی سریع‌ترین نیست، که کل نکتهٔ انتخاب per call است: هر کدام را در برابر یک sample از documentهای واقعی‌تان در DPI واقعی‌تان benchmark کنید، و هر بار که DLLهای engine یا document mix تغییر کردند آن اندازه‌گیری را دوباره ببینید. corpus هر بار بحث را می‌برد

زنجیره بازگشتی رندر PDF: موتور داخلی اول تلاش می‌کند، شکست‌ها لاگ می‌شوند، PDFium دوباره تلاش می‌کند، و وقتی همه موتورهای موجود در یک صفحه شکست بخورند یک exception پرتاب‌شده گزارش می‌دهد
هر تلاش LastRenderError را بررسی و علت را پیش از تعویض موتور ثبت می‌کند. تنها وقتی هر موتور نصب‌شده شکست خورده باشد زنجیره exception می‌دهد، با علل گردآوری‌شده که از قبل در لاگ نشسته‌اند

فراتر از pageهای منفرد: batchهای TIFF و device contextهای زنده

دو همسایهٔ callهای per-page، toolkit را کامل می‌کنند. RenderAsMultipageTIFFToFile یک expression page-range را مستقیماً در یک multi-page TIFF render می‌کند، شکل طبیعی برای hand-offهای archival به document management systemهایی که پیش از PDF هستند. RenderPageToDC مستقیماً روی یک device context در Windows برای controlهای preview paint می‌کند، که توسط trio خود از تنظیمات چسبناک (SetRenderDCOffset، SetRenderDCErasePage، به‌علاوهٔ crop type) governance می‌شود که همان discipline reset را به‌عنوان scale factor نیاز دارند. screen preview و rendering در print-path به‌اندازهٔ کافی تلهٔ خودشان را حمل می‌کنند که یک مقالهٔ اختصاصی را-worthy باشند، که در زیر link شده

کجا بعد بروید

یک عادت ارزش carrying forward: چون SelectRenderer برای هر call بعدی روی instance اثر می‌گیرد، یک page سرسخت می‌تواند روی engine دیگری retry شود در حالی که بقیهٔ document روی پیش‌فرض می‌ماند. برای preview painting، selection printer و handling در DevMode، با مقالهٔ print preview و device context ادامه دهید. وقتی renderها یک pipeline با حجم بالا روی fileهای بسیار بزرگ را تغذیه می‌کنند، رویکرد handle-based در راهنمای direct-access به‌طور طبیعی با rendering per-page از طریق DARenderPageToFile جفت می‌شود

بسته‌بندی engine، formatهای پشتیبانی‌شده و buildهای trial در page محصول PDF Library for Delphi با جزئیات آمده‌اند