Техническа статия

Сравнение на PDF файлове рамо до рамо в Delphi с PDFium Component

Два документа, отворени едновременно, един и същ номер на страница, всеки в собствен панел с възможност за превъртане: това е сърцевината на програма за преглед за сравнение (comparison viewer). PDFium Component доставя това чрез ясен обектен модел (object model), при който TPdf притежава файла, а TPdfView притежава дисплея. Един документ, един TPdf, един TPdfView. Искате три панела, имате три двойки. Трудните части не са извикванията на API; те са аритметиката на оформлението (layout), когато прозорецът променя размера си, и логиката за синхронизиране на страниците, когато решавате кой изглед кой трябва да следва

Оформление на формата (Form Layout)

VCL формата съдържа три TScrollBox контейнера един до друг, всеки с TPdfView вътре и подравнен към alClient, така че да запълва кутията. Два компонента TSplitter стоят между кутиите, така че потребителят да може да регулира ширините на колоните по време на изпълнение. Лента с инструменти (toolbar) над панелите носи бутоните за отваряне, контролите за мащабиране и превключвателя между два/три изгледа

Режимът с три изгледа е булева стойност, която формата проследява вътрешно. Когато се превключи, преизчислявате ширините и показвате или скривате третата колона. Най-простият подход е да изчистите всички свойства Align, да скриете сплитерите (splitters), след което да зададете абсолютни позиции:

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;
    // Приложете същата (ClientHeight - височина на лентата с инструменти) към всичките три стойности на Height
  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 (constraint engine) да се бори с вашите присвоявания. Възстановете видимостта на сплитера след позициониране, ако искате преоразмеряване с плъзгане (drag-to-resize) в режим на два изгледа

Височината на всяка кутия за превъртане е клиентската област (client area) минус височината на панела с инструменти. Тъй като лентата с инструменти е закачена отгоре с alTop, ClientHeight - PanelButtons.Height ви дава използваемото вертикално пространство. Присвоете това на всичките три кутии в рамките на едно и също извикване на UpdateLayout, така че никога да няма кадър, в който една кутия е по-висока от другите и причинява трептене на оформлението (layout flicker)

Отваряне на документ

Всяка двойка панели се нуждае от собствена процедура за отваряне. Моделът е кратък: деактивирайте компонента, задайте името на файла, активирайте, след това проверете Active; ако остане False, поискайте парола и опитайте отново. Имайте предвид, че TPdfView.Active е това, което контролира рендирането, но TPdf.Active е това, което действително отваря файла; те са независими. Задаването на PdfView.Active := True, когато свързаният с него TPdf все още не е активен, е безобидно, но не показва нищо

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;

  // Неуспехите при зареждане са тихи: Active остава False, вместо да хвърля грешка.
  if not PdfComponent.Active then
  begin
    // Най-вероятно защитен с парола файл; дайте на потребителя един повторен опит.
    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 +
      ' (повреден файл или грешна парола)');
    Exit;
  end;

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

Винаги проверявайте PdfComponent.Active след присвояването; повреден файл или грешна парола кара зареждането да се провали тихо, без да се хвърля изключение в пътя по подразбиране. Изричното задаване на PdfViewComponent.PageNumber := 1 след успешно отваряне избягва остарял (stale) номер на страница от предишния документ

Диалогът за съобщение в края е умишлен: вие искате повредените или неподдържани файлове да излязат на повърхността незабавно, вместо да бъдат погълнати като тих празен панел. Потребител, който не вижда нищо, няма представа дали файлът се е заредил и е просто празен, или компонентът го е отхвърлил. Съобщаването на неуспеха запазва грешката видима

Проследяване на активния панел

Когато потребителят щракне вътре в панел, този панел става активен. Формата проследява частно поле FActivePdfView: TPdfView. Визуалната обратна връзка е промяна на цвета на границата на съдържащия TScrollBox: задайте го на clHighlight за активния и clWindow за другите. Свържете това към всеки TPdfView.OnClick и към процедурата за отваряне, така че фокусът да следва документа, който току-що сте отворили

Някои операции се прилагат към всички видими панели, а не само към активния. Булева стойност FAllViewsMode във формата задвижва този клон. Когато е вярна, промените в мащабирането и навигацията по страниците се разпространяват (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;

Синхронизирана навигация по страниците

Синхронизираната навигация е по избор, но е полезна за работни процеси за ревизия (revision) на документи, където и двата файла покриват един и същ диапазон от страници. Логиката принадлежи към манипулатор на събития (event handler), който се задейства, след като потребителят навигира в един изглед. Когато изглед източник промени своя PageNumber, манипулаторът разпространява този номер към другите изгледи, при условие на един предпазител: целевият изглед трябва да има поне толкова страници, в противен случай пропуснете

PageNumber в TPdfView и в TPdf са независими. TPdf.PageNumber проследява коя страница документният компонент счита за текуща; TPdfView.PageNumber проследява какво се показва на екрана. За целите на навигацията искате свойството на изгледа, а не свойството на документа

Квадратче за отметка с етикет като "Синхронизиране на страници" дава контрол на потребителя. Когато не е отметнато, всеки панел се навигира независимо и манипулаторът излиза незабавно. Тази независимост е важна за случаи на употреба, при които двата документа имат различен брой страници или когато потребителят иска да намери еквивалентния пасаж в превод, който започва на различна страница. Принудителното синхронизиране винаги би направило инструмента по-трудeн за използване от простото подреждане на работния плот (desktop arrangement) с два прозореца

Едно нещо, за което да внимавате: програмното задаване на PdfView.PageNumber в манипулатора за синхронизация само по себе си ще задейства събитието за промяна в този изглед. Предпазете се от безкрайна рекурсия с булев флаг, който задавате преди присвояването и изчиствате веднага след това. Флагът е за форма (per-form), а не за изглед, защото всичките три изгледа споделят един и същ манипулатор

Мащабиране за всеки панел

Всеки TPdfView носи собствено свойство Zoom, Double в проценти, където Zoom := 100 означава действителен размер (100%). Задаването му отменя (overrides) всеки активен FitMode. За бутон за побиране по ширина (fit-to-width) на активния панел, прочетете подходящото мащабиране от PdfView.PageWidthZoom[PdfView.PageNumber] и го присвоете. За побиране на страница (fit-to-page) използвайте PageZoom[PageNumber]. И двете са свойства на масиви (array properties), индексирани по базиран на 1 номер на страница, така че се предпазете от нулев номер на страница, преди да получите достъп до тях

Когато експортирате текущата страница в изображение, прочетете ротацията от изгледа, но извикайте RenderPage в компонента TPdf, а не в изгледа. Формата на растерно изображение (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 върху ширината и височината дава по-рязък (sharper) изход за документи с фин текст. try/finally около освобождаването на растерното изображение не е по избор; отмяната (cancel) на TSaveDialog все още удря (hits) блока finally и вие искате растерното изображение да бъде освободено, независимо какво е направил потребителят

Изисквания за DLL

PDFium Component обвива (wraps) родната (native) библиотека pdfium. 32-битов процес хост се нуждае от pdfium32.dll; 64-битов хост се нуждае от pdfium64.dll. Вариантите с V8 JavaScript двигателя добавят суфикса v8 и тежат приблизително 23-27 MB срещу 5-6 MB за стандартните компилации. За програма за преглед за сравнение, която деактивира попълването на формуляри (Pdf.FormFill := False), стандартната non-V8 компилация е достатъчна и поддържа дистрибуцията по-малка

Поставете DLL файла в същата директория като изпълнимия файл (executable) или във всяка директория от системния PATH. Компонентът го зарежда при поискване, когато първият TPdf е активиран, така че липсващ DLL излиза на повърхността (surfaces) в този момент, а не при стартиране на приложението. Ако доставяте инсталатор, най-надеждният подход е да копирате DLL файла в папката на приложението по време на инсталацията, вместо да разчитате на системна директория, която администратор може по-късно да почисти

V8 компилациите са полезни предимно когато трябва да взаимодействате с PDF JavaScript действия, например за задействане на полета за изчисление или манипулатори за изпращане (submit handlers). Пасивната програма за преглед за сравнение няма причина да изпълнява JavaScript; задаването на Pdf.FormFill := False преди Active := True напълно пропуска средата за попълване на формуляри, което също означава, че не се инициализира JS двигател, дори ако се използва стандартната компилация. Това е правилното решение по подразбиране за програма за преглед само за четене, независимо кой вариант на DLL доставяте

За допълнителни подробности относно PDFium Component и пълното му API, посетете продуктовата страница на Delphi PDFium Component