مقاله فنی

ساخت یک نمایشگر PDF در دلفی با PDFium Component

یک نمایشگر PDF در دلفی به دو کامپوننت و سیم‌کشی بین آنها خلاصه می‌شود. TPdf مالک سند است: فایل را باز می‌کند، آن را رمزگشایی می‌کند و به سؤالاتی در مورد تعداد صفحات و فراداده (metadata) پاسخ می‌دهد. TPdfView کنترل بصری است که صفحات را روی صفحه نمایش نقاشی (paint) می‌کند و اسکرول، زوم و صفحه‌ای که کاربر در حال حاضر به آن نگاه می‌کند را کنترل می‌کند. PDFium Component همان موتور رندرینگی را که در داخل Chrome ارسال می‌شود، می‌پوشاند، بنابراین گلیف‌ها، anti-aliasing و رنگی که روی بوم (canvas) دریافت می‌کنید با آنچه کاربرانتان قبلاً در مرورگر خود می‌بینند مطابقت دارد. کار در رندر کردن نیست. بلکه در اتصال شیء سند به نما (view)، بارگذاری بدون خرابی (crash) در یک فایل آسیب‌دیده یا محافظت‌شده با رمز عبور، و دادن مشتی کنترل به کاربر است که باعث می‌شود یک نمایشگر احساس کامل بودن بدهد: ورق زدن صفحه، تغییر زوم، متناسب کردن صفحه با پنجره

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

سیم‌کشی TPdf به TPdfView

یک TPdf و یک TPdfView را روی فرم رها کنید، سپس به نما بگویید که کدام سند را نمایش دهد. آن تخصیص منفرد کل پیوند بین سند غیر بصری و کنترلی است که آن را نقاشی می‌کند

procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf and PdfView were dropped at design time.
  PdfView.Pdf := Pdf;                 // the view paints whatever this document holds
  PdfView.FitMode := pfmFitWidth;     // start the user at a sensible zoom
end;

قبل از اجرای هر یک از این موارد، کتابخانه بومی PDFium باید روی دستگاه باشد. PDFium Component بسته به پلتفرم هدف شما pdfium32.dll یا pdfium64.dll را فراخوانی می‌کند، و اگر DLL پیدا نشود، سند به سادگی از باز شدن امتناع می‌کند. DLL منطبق را در کنار فایل اجرایی خود ارسال کنید، یا آن را در جایی قرار دهید که سیستم لودر آن را پیدا کند. ساخت‌های (builds) دارای V8 فقط برای PDFهایی وجود دارند که حاوی جاوااسکریپتی هستند که می‌خواهید اجرا کنید، که یک نمایشگر ساده این کار را انجام نمی‌دهد، بنابراین به سراغ DLL استاندارد بروید مگر اینکه دلیل مشخصی برای انجام ندادن آن داشته باشید

بارگیری یک سند بدون اعتماد به ورودی

غریزه این است که بارگذاری را در یک try/except بپیچید و یک استثنای پرتاب شده را به عنوان شکست در نظر بگیرید. آن غریزه در اینجا اشتباه است، و اشتباه گرفتن آن باعث ایجاد نمایشگری می‌شود که خوب به نظر می‌رسد تا زمانی که کسی فایلی خراب را به آن تحویل دهد. تنظیم Active := True در صورت خرابی بارگذاری خطایی ایجاد نمی‌کند. PDFium Component خطای داخلی را می‌گیرد و Active را در False قرار می‌دهد، بنابراین تنها راه صادقانه برای دانستن اینکه آیا سند باز شده است یا خیر، خواندن ویژگی (property) پس از تنظیم آن است

procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // never raises; failure leaves Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // the view tracks its own current page
  UpdatePageLabel;
end;

دو چیز سزاوار توجه است. اول اینکه PageNumber در هر دو شی وجود دارد و این دو مستقل هستند. Pdf.PageNumber مفهوم سند از یک صفحه فعلی است؛ PdfView.PageNumber صفحه‌ای است که کنترل در واقع نمایش می‌دهد و این همان صفحه‌ای است که شما برای حرکت دادن کاربر در فایل تنظیم می‌کنید. تنظیم یکی دیگری را حرکت نمی‌دهد، بنابراین یک نمایشگر همیشه ویژگی نما را هدایت می‌کند. دوم نمایه‌سازی (indexing) مبتنی بر 1 است: صفحات از 1 تا Pdf.PageCount اجرا می‌شوند، نه از 0، که هر کسی را که به آرایه‌های مبتنی بر صفر عادت کرده است غافلگیر می‌کند

مدیریت یک فایل رمزگذاری شده

اسناد رمزگذاری شده در همان مسیر بارگذاری قرار می‌گیرند. اگر رمز عبور باز کردن قبل از فعال‌سازی تنظیم شده باشد، سند هنگام باز شدن رمزگشایی می‌شود؛ اگر اشتباه یا مفقود باشد، Active دقیقاً مانند یک فایل خراب در False باقی می‌ماند. بنابراین راه‌حل برای بازیابی این است که رمز عبور را بخواهید و فعال‌سازی را دوباره امتحان کنید

procedure TFormMain.OpenWithPassword(const FileName: string);
var
  Password: string;
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    if InputQuery('Password required', 'Password:', Password) then
    begin
      Pdf.Password := Password;       // must be set before Active := True
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Unable to open the document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

از آنجایی که شکست برای رمز عبور بد و فایل آسیب دیده بی‌صدا است، نمی‌توانید این دو را فقط از روی Active تشخیص دهید. در عمل، این برای یک نمایشگر قابل قبول است: کاربر یا رمز عبور درست را ارائه می‌دهد یا متوجه می‌شود که فایل باز نمی‌شود و پیام در هر دو حالت به یک شکل خوانده می‌شود

صفحه‌بندی از طریق سند

با باز بودن سند، ناوبری عملیات حسابی روی PdfView.PageNumber است که توسط Pdf.PageCount محدود شده است. تنها کار واقعی محدود کردن (clamping) است، بنابراین دکمه‌ها هرگز صفحه را به خارج از محدوده نمی‌برند و دکمه‌های اول و آخر در انتهای فایل غیرفعال می‌مانند

procedure TFormMain.GoToPage(NewPage: Integer);
begin
  if not Pdf.Active then
    Exit;
  if NewPage < 1 then
    NewPage := 1
  else if NewPage > Pdf.PageCount then
    NewPage := Pdf.PageCount;
  PdfView.PageNumber := NewPage;
  UpdatePageLabel;
end;

// the four navigation buttons reduce to one call each
procedure TFormMain.FirstClick(Sender: TObject);  begin GoToPage(1); end;
procedure TFormMain.PrevClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber - 1); end;
procedure TFormMain.NextClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber + 1); end;
procedure TFormMain.LastClick(Sender: TObject);   begin GoToPage(Pdf.PageCount); end;

یک کادر متنی "رفتن به صفحه N" همان فراخوانی GoToPage است که از یک عدد صحیح تجزیه شده تغذیه می‌شود، و محدودکننده، حالتی را پوشش می‌دهد که کاربر 9999 را در یک فایل ده‌صفحه‌ای تایپ کند. UpdatePageLabel را به عنوان تنها مکانی نگه دارید که "Page 3 of 12" را می‌نویسد تا بازخوانی (readout) هرگز با آنچه که نما نشان می‌دهد ناهماهنگ نشود

زوم: درصدهای صریح و حالت‌های تناسب

زوم روی TPdfView در دو طعم ظاهر می‌شود که با یکدیگر تعامل دارند، و درک این تعامل تفاوت بین کنترل زومی است که رفتار درستی دارد و کنترلی که با کاربر مبارزه می‌کند. مسیر مستقیم ویژگی Zoom است، درصدی که در آن 100 به معنای اندازه واقعی است. مسیر دیگر FitMode است، که به نما می‌گوید زوم را برای شما محاسبه کند و با تغییر اندازه پنجره به محاسبه مجدد آن ادامه دهد

// fixed magnifications
PdfView.Zoom := 100;     // actual size
PdfView.Zoom := 50;      // half
PdfView.Zoom := 200;     // double

// let the view size the page to the window, and keep it sized on resize
PdfView.FitMode := pfmFitWidth;   // page width fills the control
PdfView.FitMode := pfmFitPage;    // whole page visible
PdfView.FitMode := pfmActualSize; // 1:1 with the document's points

در اینجا قسمتی است که افراد را به اشتباه می‌اندازد. تخصیص مستقیم Zoom، FitMode را به pfmNone بازنشانی می‌کند. این رفتار صحیح است، نه یک اشکال (bug): لحظه‌ای که کاربر دقیقاً 150٪ را انتخاب می‌کند، نما دیگر نمی‌تواند "تناسب با عرض" را نیز رعایت کند، زیرا این دو درخواست با هم در تضاد هستند. نتیجه برای رابط کاربری شما این است که دکمه بزرگ‌نمایی و دکمه تناسب-با-صفحه (fit-to-page) حالت‌های متقابلاً انحصاری هستند و نوار ابزار باید حالت فعال را قابل مشاهده کند. هنگامی که کاربر روی تناسب-با-صفحه کلیک می‌کند، FitMode را تنظیم کنید؛ هنگامی که روی زوم عددی کلیک می‌کنند، Zoom را تنظیم کنید و اجازه دهید حالت تناسب را به تنهایی پاک کند

اگر ترجیح می‌دهید خودتان مقدار تناسب را محاسبه کنید، شاید برای بذرپاشی (seed) یک نوار لغزنده زوم با درصد تناسب فعلی، کمکی‌های هر-صفحه (per-page helpers) اعداد را بدون تغییر حالت به شما می‌دهند. PageWidthZoom[N]، PageZoom[N] و ActualSizeZoom[N] درصدی را برمی‌گردانند که صفحه N را با عرض متناسب می‌کند، آن را به طور کامل متناسب می‌کند، یا آن را در اندازه واقعی رندر می‌کند

// seed a zoom readout from the fit-to-width value of the current page
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

یک نمایشگر کامل در واقع به چه چیزی نیاز دارد

نمایشگر بالا چند ده خط است و در حال حاضر کاری را که جریان کاری سند به آن نیاز دارد انجام می‌دهد: باز کردن فایل، زنده ماندن در برابر یک فایل بد، نشان دادن صفحه، حرکت بین صفحات و تغییر بزرگ‌نمایی با دست یا با تناسب. PDFium قسمت‌های سخت را بی‌صدا انجام می‌دهد. فونت‌های جاسازی شده رفع می‌شوند، حاشیه‌نویسی‌ها و فیلدهای فرم در جایی که سند آن‌ها را قرار می‌دهد رنگ‌آمیزی می‌شوند و صفحه‌ای که می‌بینید با صفحه‌ای مطابقت دارد که یک کاربر Chrome می‌بیند، زیرا این همان موتور است که هر دو را رسم می‌کند

از این پایه، موارد اضافی بیشتر تدریجی هستند تا ساختاری. انتخاب متن و جستجو از همان لایه متنی خوانده می‌شوند که PDFium از قبل می‌سازد؛ فراداده‌هایی (metadata) مانند Pdf.Title و Pdf.Author به اندازه خواندن یک ویژگی فاصله دارند؛ چرخش و مقیاس خاکستری (grayscale) گزینه‌های رندرینگی هستند که هنگام رسم یک صفحه در یک بیت‌مپ (bitmap) ارسال می‌کنید. هیچ‌کدام از آن‌ها ستون فقراتی را که در اینجا دارید تغییر نمی‌دهند، که همان شیء سند، نما و جریان بارگیری-سپس-ناوبری است که آن‌ها را به هم متصل می‌کند. این ستون فقرات را به درستی انجام دهید و بقیه موارد تزئین است

کامپوننت‌های TPdf و TPdfView که در سراسر استفاده شده‌اند، بخشی از PDFium Component برای دلفی و سی‌پلاس‌پلاس‌بیلدر (C++Builder) هستند که مرجع کامل نمایشگر را در صفحه محصول خود به همراه دارد