مقاله فنی

مقایسه PDF به صورت کنارهم در Delphi با PDFium Component

دو سند که هم‌زمان باز شده‌اند، روی یک شماره صفحه مشترک قرار دارند و هر کدام در پنل اسکرول‌پذیر خودشان دیده می‌شوند: این هسته یک نمایشگر مقایسه است. PDFium Component این کار را با یک مدل شیئی سرراست ارائه می‌کند که در آن TPdf مالک فایل است و TPdfView مالک نمایش. یک سند، یک TPdf، یک TPdfView. اگر سه پنل بخواهید، سه جفت خواهید داشت. بخش‌های سخت، خود فراخوانی‌های API نیستند؛ سختی اصلی در محاسبات layout هنگام تغییر اندازه پنجره و منطق همگام‌سازی صفحه است، وقتی تصمیم می‌گیرید کدام view باید از کدام view پیروی کند

چیدمان فرم

فرم VCL سه محفظه TScrollBox را کنار هم نگه می‌دارد که هر کدام یک TPdfView درون خود دارند و با alClient تنظیم شده‌اند تا کل جعبه را پر کنند. دو مؤلفه TSplitter میان این جعبه‌ها قرار می‌گیرند تا کاربر بتواند پهنای ستون‌ها را در زمان اجرا تغییر دهد. یک toolbar بالای پنل‌ها دکمه‌های باز کردن، کنترل‌های بزرگ‌نمایی، و toggle مربوط به حالت دو نمایی یا سه نمایی را در خود می‌گذارد

حالت سه‌نمایی یک boolean است که فرم در داخل خود نگه می‌دارد. وقتی این مقدار عوض می‌شود، باید پهناها را دوباره محاسبه کنید و ستون سوم را نشان بدهید یا پنهان کنید. ساده‌ترین راه این است که همه ویژگی‌های Align را پاک کنید، splitterها را موقتاً مخفی کنید، و بعد موقعیت‌های مطلق را تنظیم کنید:

procedure TFormMain.UpdateLayout;
var
  TotalWidth: Integer;
begin
  TotalWidth := ClientWidth;

  if ThreeViewMode then
  begin
    ScrollBox3.Visible := True;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 3;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth div 3;
    ScrollBox3.Left   := ScrollBox2.Left + ScrollBox2.Width;
    ScrollBox3.Width  := TotalWidth - ScrollBox3.Left;
    // Apply the same (ClientHeight - toolbar height) to all three Height values
  end
  else
  begin
    ScrollBox3.Visible := False;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 2;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth - ScrollBox2.Left;
  end;
end;

اگر پیش از محاسبات صحیح، روی هر سه جعبه Align := alNone بگذارید، موتور محدودیت‌های VCL با انتساب‌های شما درگیر نمی‌شود. اگر بخواهید در حالت دو نمایی امکان drag-to-resize حفظ شود، بعد از جای‌گذاری می‌توانید splitterها را دوباره مرئی کنید

ارتفاع هر scroll box برابر است با فضای client منهای ارتفاع پنل toolbar. چون toolbar با alTop در بالا dock شده است، عبارت ClientHeight - PanelButtons.Height فضای عمودی قابل استفاده را به شما می‌دهد. این مقدار را در همان فراخوانی UpdateLayout به هر سه جعبه بدهید تا هیچ فریمی وجود نداشته باشد که یکی از جعبه‌ها از بقیه بلندتر شود و باعث flicker در layout گردد

باز کردن یک سند

هر جفت پنل به روال باز کردن مخصوص خودش نیاز دارد. الگو کوتاه است: مؤلفه را غیرفعال کنید، نام فایل را بگذارید، آن را فعال کنید، و بعد Active را بررسی کنید؛ اگر مقدارش همچنان False ماند، برای گذرواژه prompt بدهید و دوباره تلاش کنید. توجه کنید که این TPdfView.Active است که رندر را کنترل می‌کند، اما این TPdf.Active است که واقعاً فایل را باز می‌کند؛ این دو مستقل از هم هستند. اگر در حالی که TPdf پیوندخورده هنوز active نشده، روی PdfView.Active := True مقداردهی کنید، خطایی رخ نمی‌دهد اما چیزی هم نمایش داده نمی‌شود

procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
  PdfViewComponent: TPdfView);
var
  Password: string;
begin
  if not OpenDialog.Execute then
    Exit;

  PdfComponent.Active   := False;
  PdfComponent.FileName := OpenDialog.FileName;
  PdfComponent.Password := '';
  PdfComponent.Active   := True;

  // Load failures are silent: Active stays False instead of raising.
  if not PdfComponent.Active then
  begin
    // Most likely a password-protected file; give the user one retry.
    if InputQuery('Password', 'Enter document password:', Password) then
    begin
      PdfComponent.Password := Password;
      PdfComponent.Active   := True;
    end;
  end;

  if not PdfComponent.Active then
  begin
    ShowMessage('Could not open ' + OpenDialog.FileName +
      ' (damaged file or wrong password)');
    Exit;
  end;

  PdfViewComponent.PageNumber := 1;
  SetActivePdfView(PdfViewComponent);
end;

همیشه بعد از انتساب، PdfComponent.Active را بررسی کنید؛ یک فایل خراب یا گذرواژه نادرست باعث می‌شود بارگذاری در مسیر پیش‌فرض بی‌صدا شکست بخورد و استثنایی هم بالا نیاید. اگر پس از باز شدن موفق، PdfViewComponent.PageNumber := 1 را صریحاً تنظیم کنید، دیگر شماره صفحه کهنه از سند قبلی باقی نمی‌ماند

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

ردیابی پنل فعال

وقتی کاربر داخل یکی از پنل‌ها کلیک می‌کند، همان پنل active می‌شود. فرم یک فیلد خصوصی به نام FActivePdfView: TPdfView را نگه می‌دارد. بازخورد بصری با تغییر رنگ حاشیه TScrollBoxای که آن view را در بر دارد داده می‌شود: برای پنل فعال مقدار clHighlight و برای بقیه clWindow را بگذارید. این منطق را به TPdfView.OnClick هر پنل و همین‌طور به روال باز کردن متصل کنید تا focus به سندی که تازه باز شده منتقل شود

بعضی عملیات‌ها به جای یک پنل فعال، روی همه پنل‌های مرئی اعمال می‌شوند. یک boolean به نام FAllViewsMode روی فرم این شاخه را کنترل می‌کند. وقتی این مقدار true باشد، تغییرات zoom و ناوبری صفحه به همه پنل‌هایی که سند فعال دارند fan-out می‌شوند:

procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
  if PdfView1.Active then PdfView1.Zoom := NewZoom;
  if PdfView2.Active then PdfView2.Zoom := NewZoom;
  if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;

ناوبری همگام صفحه

ناوبری همگام اختیاری است، اما برای جریان‌های کاری بازبینی نسخه سند بسیار مفید است؛ جایی که هر دو فایل تقریباً همان بازه صفحات را پوشش می‌دهند. این منطق باید در یک event handler قرار بگیرد که بعد از ناوبری کاربر در یکی از viewها اجرا می‌شود. وقتی view مبدأ مقدار PageNumber خود را عوض می‌کند، handler همان عدد را به viewهای دیگر منتقل می‌کند، البته با یک محافظ: view مقصد باید دست‌کم آن تعداد صفحه را داشته باشد، وگرنه آن مقصد را نادیده بگیرید

PageNumber روی TPdfView و روی TPdf مستقل از هم هستند. TPdf.PageNumber دنبال می‌کند مؤلفه سند کدام صفحه را جاری می‌داند؛ TPdfView.PageNumber دنبال می‌کند روی صفحه‌نمایش چه چیزی دیده می‌شود. برای مقاصد ناوبری، شما به ویژگی view نیاز دارید نه ویژگی document

یک checkbox با برچسبی مثل "Sync pages" کنترل را به کاربر می‌دهد. وقتی تیک آن برداشته شده باشد، هر پنل مستقل حرکت می‌کند و handler باید فوراً خارج شود. این استقلال برای سناریوهایی مهم است که دو سند تعداد صفحه متفاوتی دارند، یا وقتی کاربر می‌خواهد معادل یک بند را در ترجمه‌ای پیدا کند که از صفحه دیگری شروع می‌شود. اگر همگام‌سازی همیشه اجباری باشد، ابزار از یک چیدمان ساده دو پنجره‌ای روی دسکتاپ هم سخت‌تر استفاده خواهد شد

یک نکته را باید مراقب باشید: اگر درون sync handler مقدار PdfView.PageNumber را به صورت برنامه‌ای تنظیم کنید، همان view خودش event تغییر را دوباره شلیک می‌کند. با یک پرچم boolean از بازگشت بی‌نهایت جلوگیری کنید؛ آن را پیش از انتساب set کنید و بلافاصله بعد از آن clear نمایید. این پرچم به ازای کل فرم است، نه به ازای هر view، چون هر سه view از همان handler مشترک استفاده می‌کنند

بزرگ‌نمایی در هر پنل

هر TPdfView ویژگی Zoom خودش را دارد؛ یک Double بر حسب درصد که در آن Zoom := 100 یعنی اندازه واقعی 100 درصد. تنظیم این مقدار هر FitMode فعال را override می‌کند. برای یک دکمه fit-to-width روی پنل فعال، مقدار بزرگ‌نمایی مناسب را از PdfView.PageWidthZoom[PdfView.PageNumber] بخوانید و همان را انتساب دهید. برای fit-to-page، از PageZoom[PageNumber] استفاده کنید. هر دو آرایه‌محور هستند و با شماره صفحه 1-based اندیس می‌خورند، پس پیش از دسترسی مراقب شماره صفحه صفر باشید

وقتی صفحه جاری را به تصویر export می‌کنید، rotation را از view بخوانید اما RenderPage را روی مؤلفه TPdf صدا بزنید، نه روی خود view. فرم bitmap از TPdf.RenderPage ابعاد پیکسلی صریح به اضافه یک مقدار TRotation و یک مجموعه TRenderOptions می‌گیرد. گونه تابعی آن یک TBitmap متعلق به فراخواننده برمی‌گرداند که بعد از ذخیره‌سازی باید خودتان آزادش کنید:

procedure TFormMain.SaveActiveViewAsImage;
var
  Pdf: TPdf;
  Bmp: TBitmap;
  Jpeg: TJpegImage;
begin
  if not Assigned(FActivePdfView) or not FActivePdfView.Active then
    Exit;

  Pdf := FActivePdfView.Pdf;
  Pdf.PageNumber := FActivePdfView.PageNumber;

  Bmp := Pdf.RenderPage(
    0, 0,
    Round(Pdf.PageWidth * 2),
    Round(Pdf.PageHeight * 2),
    FActivePdfView.Rotation, [], clWhite);
  try
    if SavePictureDialog.Execute then
    begin
      Jpeg := TJpegImage.Create;
      try
        Jpeg.Assign(Bmp);
        Jpeg.CompressionQuality := 90;
        Jpeg.SaveToFile(SavePictureDialog.FileName);
      finally
        Jpeg.Free;
      end;
    end;
  finally
    Bmp.Free;
  end;
end;

ضریب 2x روی عرض و ارتفاع خروجی شارپ‌تری برای اسناد دارای متن ریز می‌دهد. بلوک try/finally دور آزاد کردن bitmap اختیاری نیست؛ حتی اگر TSaveDialog لغو شود، باز هم مسیر finally اجرا می‌شود و شما می‌خواهید bitmap فارغ از کاری که کاربر کرده آزاد شود

الزامات DLL

PDFium Component کتابخانه بومی pdfium را wrap می‌کند. یک فرایند میزبان 32 بیتی به pdfium32.dll نیاز دارد؛ یک میزبان 64 بیتی به pdfium64.dll. گونه‌هایی که موتور JavaScript مبتنی بر V8 را دارند پسوند v8 می‌گیرند و تقریباً 23 تا 27 مگابایت حجم دارند، در حالی که buildهای استاندارد حدود 5 تا 6 مگابایت هستند. برای یک نمایشگر مقایسه که پر کردن فرم را غیرفعال می‌کند یعنی Pdf.FormFill := False، build استانداردِ بدون V8 کافی است و توزیع را هم کوچک‌تر نگه می‌دارد

DLL را در همان پوشه executable قرار دهید، یا در هر پوشه‌ای که روی PATH سیستم باشد. مؤلفه آن را در زمان نیاز، وقتی نخستین TPdf فعال می‌شود، بار می‌کند، بنابراین نبودن DLL در همان نقطه خود را نشان می‌دهد نه در شروع برنامه. اگر installer ارسال می‌کنید، مطمئن‌ترین راه این است که DLL را در زمان نصب داخل پوشه برنامه کپی کنید، نه این‌که به یک شاخه سیستمی تکیه کنید که بعداً شاید یک مدیر سیستم پاکش کند

buildهای V8 عمدتاً وقتی مفیدند که لازم باشد با کنش‌های JavaScript داخل PDF تعامل کنید، مثلاً برای تحریک فیلدهای محاسباتی یا submit handlerها. یک نمایشگر مقایسه صرفاً خواندنی هیچ دلیلی برای اجرای JavaScript ندارد؛ اگر پیش از Active := True مقدار Pdf.FormFill := False را تنظیم کنید، کل محیط form-fill نادیده گرفته می‌شود، یعنی حتی اگر build استاندارد هم استفاده شود موتور JS مقداردهی اولیه نخواهد شد. این پیش‌فرض درست برای یک نمایشگر فقط‌خواندنی است، فارغ از این‌که کدام گونه DLL را ارسال می‌کنید

برای جزئیات بیشتر درباره PDFium Component و API کامل آن، به صفحه محصول Delphi PDFium Component مراجعه کنید