مقاله فنی

نمایشگر PDF برای لازاروس و فری‌پاسکال با PDFium

دلفی و لازاروس از یک Object Pascal مشترک برای کامپایل استفاده می‌کنند، و دقیقاً همین شباهت ظاهری است که پورت کردن یک نمایشگر بین آن‌ها را فریبنده می‌سازد. این دو زنجیره ابزار (toolchain) در سه جا که برای کار با PDF مهم است از هم فاصله می‌گیرند: نوع string بومی در دلفی UTF-16 و در یک برنامه LCL از نوع UTF-8 است؛ VCL و LCL فریم‌ورک‌های بصری متفاوتی هستند که کنترل‌ها، دیالوگ‌ها و فرمت‌های استریم‌سازی فرم خاص خود را دارند؛ و یک باینری دلفی ویندوز را هدف قرار می‌دهد در حالی که یک باینری FPC ممکن است به سمت لینوکس یا macOS برود. هیچ‌یک از این تفاوت‌ها در زمان کامپایل خود را نشان نمی‌دهند. یک نمایشگر ساخته شده روی PDFium Component، که نسخه‌های VCL و LCL را از یک درخت منبع واحد ارائه می‌دهد، تحت لازاروس پس از تغییر نام چند یونیت و چند بلوک {$IFDEF FPC} به صورت تمیز کامپایل می‌شود. خرابی‌ها بعداً از راه می‌رسند، زمانی که داده‌های واقعی و یک استقرار واقعی (deployment) فرضیاتی را فاش می‌کنند که نسخه بیلد دلفی بی‌صدا می‌ساخت

چهار مورد از این فرضیات بیشترِ زمان از دست رفته را به خود اختصاص می‌دهند: انکودینگ متن در مرز UI، وسوسه حفظ دو کپی از فرم، روشی که باینری موتور بومی در زمان اجرا (runtime) حل‌وفصل می‌شود، و لحظه‌ای که تبدیل متن به گفتار (text-to-speech) پلتفرم را تمام می‌کند وقتی SAPI از بین می‌رود. اگر بدانید که این مشکلات در راه هستند، مدیریت هر یک ارزان است، و اگر ندانید، پیگیری آنها گران تمام می‌شود

همان پاسکال، اما محموله‌های رشته متفاوت

نوع بومی string در دلفی از سال ۲۰۰۹ به صورت UTF-16 بوده است. پیش‌فرض در لازاروس و فری‌پاسکال برای برنامه‌های LCL از نوع UTF-8 است. APIهای متنیِ کامپوننت از طریق نوع WString با UTF-16 صحبت می‌کنند، که بیلد FPC آن را به WideString پیوند می‌دهد (alias)، بنابراین هر مرزی که متن بین UI LCL شما و موتور PDF عبور می‌کند، یک نقطه تبدیل است

تبدیل‌ها در انتساب‌های (assignments) سرراست به طور خودکار اتفاق می‌افتند، و بیشتر کدها هرگز نیازی به فکر کردن در مورد آنها ندارند. دو عادت، باگ‌های انکودینگ را دور نگه می‌دارند. متن را مستقیماً و بدون دستکاری در سطح بایت عبور دهید: کدی که یک عبارت جستجو را با آفست بایت برش می‌دهد در دلفی کار می‌کند، جایی که یک Char معادل یک واحد UTF-16 است، اما UTF-8 چند بایتی را در LCL خراب می‌کند. و از همان اجرای اول با داده‌های غیر-ASCII تست کنید. یک نام فایل آلمانی، یک عبارت جستجوی سیریلیک، نام نویسنده‌ای با حروف دارای آکسان در متادیتا سند: داده‌های آزمایشی کاملاً ASCII هر نقص انکودینگی را پنهان می‌کنند، زیرا ASCII تنها محدوده‌ای است که UTF-8 و UTF-16 به صورت بایت به کاراکتر با هم توافق دارند. باگ تمام مدت واقعی است؛ ASCII فقط آن را نامرئی نگه می‌دارد تا زمانی که یک مشتری در مونیخ فایلی را باز کند که شما هرگز آن را تست نکرده‌اید

یک بلوک شرطی، نه یک انشعاب به ازای هر IDE

پس از ده دوازده IFDEF اول، پایگاه کد (codebase) شبیه دو پروژه‌ای می‌شود که یک ریپازیتوری را پوشیده‌اند، و انشعاب (forking) آن به ازای هر IDE وسوسه‌انگیز به نظر می‌رسد. این یک حرکت اشتباه است. تفاوت‌های واقعی در یک بلوک اعلانِ مشترک خلاصه می‌شوند، و یک انشعاب هزینه هر رفع باگ را از آن زمان به بعد دو برابر می‌کند. لایه شرطی را به همین کوچکی نگه دارید:

{$IFDEF FPC}
uses
  LCLType, Forms, Graphics, Controls;

type
  WString = WideString;   // component text APIs are UTF-16
  TBytes  = array of Byte;
{$ELSE}
uses
  Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}

هر چیزی که در زیر آن بلوک قرار دارد در هر دو IDE به طور یکسان کامپایل می‌شود. مدیریت سند، پیمایش صفحه، فراخوانی‌های رندرینگ: TPdf و TPdfView سطح یکسانی را در نسخه‌های VCL و LCL در معرض نمایش قرار می‌دهند، بنابراین بخش عمده‌ای از نمایشگر هرگز یک شرط کامپایلر را نمی‌بیند. نگه داشتن آن به این شکل، یک نظم ساختاری است و نه یک ترفند هوشمندانه. منطق مشترک PDF در یونیت‌هایی قرار می‌گیرد که هیچ دیالوگ یا پنل خاص فریم‌ورک را فراخوانی نمی‌کنند. تعداد انگشت‌شماری از مواردی که واقعاً متفاوت هستند، مانند دیالوگ‌های چاپ و انتخابگرهای فایل با قراردادهای پلتفرم خود، در پشت یک رابط (interface) نازک که یک بار در هر فریم‌ورک پیاده‌سازی شده، پنهان می‌شوند. بلوک IFDEF به تنها مکانی تبدیل می‌شود که واگرایی پلتفرم در آینده اجازه فرود در آن را دارد، به جای نشت دادن بخشنامه‌های کامپایلر (compiler directives) در سراسر چهل یونیت

فرم را در کد بسازید، نه در دو طراح (designer)

استریم‌سازی فرم جایی است که پروژه‌های دو-IDE به آرامی پوسیده می‌شوند. یک .dfm و یک .lfm که ادعا می‌کنند همان فرم را توصیف می‌کنند، ویژگی به ویژگی از هم دور می‌شوند تا زمانی که دو بیلد به دلایلی که هیچ کس نمی‌تواند مقایسه (diff) کند، متفاوت رفتار می‌کنند، زیرا این دو فایل حتی در فرمت یکسانی نیستند. ساختن نمایشگر در زمان اجرا، کل مشکل را دور می‌زند. یک توالی سازنده (constructor) وجود دارد که به عنوان کد معمولی کنترل نسخه (version-controlled) شده است، و در هر دو پلتفرم یکسان خوانده می‌شود:

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  Pdf := TPdf.Create(Self);

  PdfView := TPdfView.Create(Self);
  PdfView.Parent := Self;
  PdfView.Align := alClient;
  PdfView.Pdf := Pdf;
  PdfView.FitMode := pfmFitWidth;

  if ParamCount > 0 then
  begin
    Pdf.FileName := ParamStr(1);
    Pdf.Active := True;   // opens the document; PageCount valid after this
  end;
end;

ترتیب دقیق آن انتساب‌ها کمتر از آن یک خطی که کار واقعی را انجام می‌دهد اهمیت دارد. PdfView.Pdf := Pdf کنترل بصری را به کامپوننت سند متصل می‌کند، و از آن نقطه به بعد پیمایش صفحه از طریق PageNumber و رفتار تناسب (fit) از طریق FitMode به طور یکسان تحت VCL و LCL پاسخ می‌دهند. یک خصلت بین-فریم‌ورکی ارزش دانستن دارد قبل از اینکه کاربری آن را به عنوان باگ گزارش دهد: انتساب دستیِ Zoom باعث بازگشت (snap back) ویژگی FitMode به حالت pfmNone در هر دو فریم‌ورک می‌شود. بنابراین اگر نوار ابزار شما "fit width" را به عنوان یک ترجیح چسبنده (sticky preference) در نظر می‌گیرد، شما باید بعد از هر گونه زومِ برنامه‌ریزی‌شده حالت تناسب را مجدداً اختصاص دهید، در غیر این صورت این ترجیح بی‌صدا در اولین باری که کد سطح زوم را لمس می‌کند از چسبندگی دست می‌کشد

باینری‌ای که IDE هرگز در مورد آن به شما هشدار نداد

این کامپوننت موتور PDFium را که به عنوان یک باینری پلتفرم بومی ارائه می‌شود دربرمی‌گیرد، و آن باینری منبع تقریباً همه گزارش‌های "در IDE کار می‌کند، در شورت‌کات نصب شده شکست می‌خورد" است. سه قانون بیشتر آنها را توضیح می‌دهند. معماری بیتی (Bitness) باید دقیقاً مطابقت داشته باشد. یک فایل اجرایی ۳۲ بیتی نمی‌تواند یک کتابخانه pdfium ۶۴ بیتی را بارگیری کند، و پیامی که سیستم عامل برمی‌گرداند ("module not found" در برخی از نسخه‌های ویندوز) به طور فعال گمراه‌کننده است، زیرا فایل دقیقاً در کنار فایل اجرایی قرار دارد. مسیر کتابخانه را نسبت به فایل اجرایی حل‌وفصل کنید، هرگز نسبت به پوشه کاری (working directory)؛ راه‌اندازی از طریق IDE و راه‌اندازی از طریق shell دقیقاً در همین نقطه با هم تفاوت دارند، و به همین دلیل است که باگ در طول توسعه پنهان می‌ماند. و خرابی بارگیری را قبل از باز شدن اولین سند بگیرید، سپس آن را با مشخص کردن مسیر مورد انتظار و معماری گزارش دهید. یک تیکت پشتیبانی که می‌گوید "باینری 64-bit PDFium در <مسیر> مفقود است" در عرض چند دقیقه بسته می‌شود. تیکتی که می‌گوید "نمایشگر هنگام راه‌اندازی کرش می‌کند" به یک هفته رفت‌وآمد تبدیل می‌شود

در حالی که مشغول آن هستید، نسخه باینری موتور را همگام با فایل اجرایی مشخص کنید. PDFium سریع حرکت می‌کند، و نصابی که برنامه را به‌روز می‌کند اما یک کتابخانه قدیمی را روی دیسک جا می‌گذارد، باعث ایجاد کرش‌هایی می‌شود که هیچ‌کس در دفتر شما نمی‌تواند آنها را بازتولید کند، به این دلیل ساده که هر ماشینی در دفتر شما به طور اتفاقی جفتِ مطابق را در خود دارد. با کتابخانه به عنوان بخشی از آرتیفکت بیلد رفتار کنید، با همان نصاب، همان مهر نسخه، و همان مسیر بازگشت (rollback) مانند فایل اجرایی‌ای که آن را بارگیری می‌کند

ثبت کامپوننت‌ها در Lazarus IDE

ساخت‌وساز زمان-اجرا نیازی به ثبت زمان-طراحی ندارد، که این تمیزترین راه‌اندازی برای نمایشگری است که UI خود را در کد می‌سازد. زمانی که می‌خواهید کامپوننت‌ها را روی پالت لازاروس برای کارهای زمان-طراحی داشته باشید، پکیج را نصب کنید و اجازه دهید یونیت ثبت اختصاصی آن، PDFiumLazReg در Lib/FPC/PDFiumLaz.lpk، آن را اداره کند. آن یونیت عمداً با عنوان زمان-طراحی علامت‌گذاری شده است: این یونیت به رابط‌های ویرایشگر ویژگی (property-editor) IDE ارجاع می‌دهد که هرگز نباید به فایل اجرایی در حال توزیعِ شما پیوند داده شوند

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

گفتار و صفحه‌خوان‌ها خارج از ویندوز

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

هدف قرار دادن لینوکس یا macOS موضوع دیگری است. هیچ SAPIای برای فراخوانی وجود ندارد، بنابراین خروجی صوتی باید به یک سرویس گفتار بومی مجدداً سیم‌کشی شود در حالی که APIهای خواندن بالای آن سر جای خود می‌مانند. آن جداسازی (split) استدلالی برای قرار دادن گفتار در پشت یک رابط (interface) از همان کامیت (commit) اول است: تحلیل ترتیب-خواندن و مکان‌نمای ردیابی کلمه (word-tracking) از نظر پلتفرم خنثی هستند و بدون تغییر منتقل می‌شوند، و فقط لایه نازکی که در واقع صدا تولید می‌کند باید در هر پلتفرم تغییر کند. مقاله خواننده دسترسی‌پذیر (accessible reader) به عمق آن ماشین‌آلات خواندن می‌پردازد

یک چک‌لیست برابری قبل از اینکه پورت را تمام شده بنامید

گذر زیر رگرسیون‌های واقعی را گرفته است، که تقریباً به ترتیب خرابی‌هایی که معمولاً ظاهر می‌شوند فهرست شده‌اند. سندی را باز کنید که مسیر آن حاوی کاراکترهای غیر-ASCII باشد. عبارتی را با کاراکترهای غیر-ASCII جستجو کنید و تأیید کنید که موارد یافت شده در جایی که باید، برجسته (highlight) می‌شوند. اسکرول چرخ ماوس، انتخاب با کشیدن (drag selection) و پیمایش صفحه با کیبورد را در هر مجموعه ویجت (widget set) که توزیع می‌کنید بررسی کنید، زیرا مدیریت فوکوس و رفتار چرخ وابسته‌ترین گوشه‌ها به مجموعه-ویجت در LCL هستند. رندرینگ را در مقیاس‌های نمایش (display scaling) 100٪، 150٪ و 200٪ بررسی کنید. در نهایت، بیلد نصب شده را اجرا کنید، نه بیلد IDE را، آن هم روی ماشینی که هرگز IDE روی آن نبوده است، زیرا این تنها آزمایشی است که حل‌وفصل باینری را صادقانه اجرا می‌کند. هر چیز دیگری می‌تواند قبول شود در حالی که این یکی به آرامی شکست می‌خورد

توان عملیاتی رندرینگ (Rendering throughput) بدون تغییر بین دو نسخه منتقل می‌شود، بنابراین رویکرد کش کردن از مقاله کش رندر و عملکرد زومدقیقاً همانطور که برای نسخه VCL نوشته شده است، برای نمایشگر LCL اعمال می‌شود

هیچ یک از این موارد نسخه LCL را به نسخه ضعیف‌تری تبدیل نمی‌کند. سطح هسته در هر دو طرف یکسان است: TPdf، TPdfView، رندرینگ، فرم‌ها، استخراج متن و APIهای دسترسی‌پذیری بدون در نظر گرفتن اینکه کدام IDE آنها را کامپایل کرده است یکسان رفتار می‌کنند. هر تفاوتی که ارزش پیگیری دارد به جای محدود بودن به نسخه، محدود به پلتفرم است. گفتار SAPI فقط مخصوص ویندوز است، دیالوگ‌ها از قراردادهای هر فریم‌ورک پیروی می‌کنند، و باینری باید با معماری که در آن بارگذاری می‌شود مطابقت داشته باشد. مرزهای انکودینگ، فرم زمان اجرا، و حل‌وفصل باینری را درست انجام دهید، و بقیه پورت همان کار مکانیکی است که کامپایلر از قبل برای شما مدیریت کرده است

نسخه‌های VCL و LCL که در اینجا توضیح داده شده‌اند با هم به عنوان PDFium Component عرضه می‌شوند، به همراه سورس کد و APIهای عمومی یکسان برای دلفی، C++Builder، و Lazarus/FPC