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

Създаване на интерактивен PDF визуализатор в Delphi с TPDFlibViewer

Вграждането на панел за четене на PDF в бизнес приложение на Delphi не изисква писане на цикъл за рендиране, машина за превъртане или модел за мащабиране. losLab PDF Library се доставя с TPDFlibViewer – VCL компонент, който отваря документ с едно извикване и предоставя селекция на текст, панорамиране, мащабиране с рамка (marquee zoom), екранни снимки (snapshots), подчертаване на региони, подчертаване на резултатите при пълнотекстово търсене, взаимодействие с анотации и печат като вградено поведение. Тази статия описва петте стъпки, които превръщат празна форма в работещ визуализатор: поставяне на компонента, свързване на готовите действия, добавяне на подчертаване при търсене, обработка на кликвания върху анотации и печат

Защо да използвате компонент за визуализация вместо да пишете цикъл за рендиране?

Аргументът за разделянето на слоевете е на първо място, тъй като той обяснява какво всъщност получавате. Рендирането на страница в растерно изображение (bitmap) е лесната част – 20 процента; останалите 80 процента са всичко около него: кеш за растерните изображения на страниците с бюджет за почистване, оформление на превъртането при страници с различни размери, режими на мащабиране, които се преизчисляват при промяна на размера, проверка на съвпадение (hit-testing), която съпоставя пиксел от мишката обратно към координатите на страницата при всякакво мащабиране и завъртане, и планиране на преначертаването без трептене. TPDFlibViewer управлява всичко това и делегира растерната работа на същия двигател, описан в статията за рендиране с множество двигатели, така че слоят на компонента и слоят за рендиране остават отделно заменяеми. Кодът на вашата форма комуникира с превъртаем Windows компонент, а не с модул за рендиране

Как да вградите PDF визуализатор в Delphi приложение?

TPDFlibViewer.LoadFromFile отваря документ чрез собствен вътрешен екземпляр на TPDFlib на компонента и връща 1 при успех, така че минималният визуализатор изисква извикване на конструктор, присвояване на Parent и едно зареждане. Ако вече притежавате екземпляр на TPDFlib другаде в приложението, AttachLibrary показва неговия избран документ вместо това, като собствеността остава на ваша страна

uses PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  if FViewer.LoadFromFile('quarterly-report.pdf', '') = 1 then
    FViewer.ViewerMode := vmHand;  // pan by dragging; a plain click still follows links
end;

Свойството ViewerMode определя какво прави мишката, измежду пет стойности: vmSelect е инструментът по подразбиране за избор на текст, vmHand панорамира чрез влачене с показалец за хващане, докато обикновеното щракване все още следва връзките, vmZoom е мащабиране с рамка (marquee zoom), което увеличава извлечената област, за да запълни екрана, vmSnapshot заснема маркираната област в клипборда (със събитие OnSnapshot и превключвател SnapshotToClipboard), а vmHighlight изчертава постоянни полупрозрачни подчертавания на региони. За инструмента за подчертаване си струва да се отбележат координатите: регионите се съхраняват в потребителското пространство на страницата, а не в пиксели на клиента, така че маркировките от AddHighlightRegion оцеляват при мащабиране, завъртане на изгледа и навигация по страниците без никакво допълнително управление от ваша страна. При завъртян изглед посоката на влачене може дори да върви срещу осите на страницата, което води до отрицателна ширина или височина след обратното преобразуване на завъртането – компонентът нормализира правоъгълника вместо вас

Как да свържете PDF лента с инструменти в Delphi без свързващ код?

Модулът PDFlibViewerActions напълно премахва необходимостта от свързващ код за лентата с инструменти. Той предоставя двадесет готови наследника на TAction, обхващащи навигацията по страниците (TPDFlibViewerFirstPage, TPDFlibViewerPriorPage, TPDFlibViewerNextPage, TPDFlibViewerLastPage), мащабирането (TPDFlibViewerZoomIn, TPDFlibViewerZoomOut, TPDFlibViewerFitWidth, TPDFlibViewerFitPage, TPDFlibViewerActualSize), завъртането на изгледа (TPDFlibViewerRotateClockwise, TPDFlibViewerRotateAntiClockwise), печат, диалози за отваряне и запис на файл, копиране на избран текст и по едно действие за превключване за всеки инструмент на мишката (TPDFlibViewerSelectMode, TPDFlibViewerHandMode, TPDFlibViewerMarqueeZoomMode, TPDFlibViewerSnapshotMode, TPDFlibViewerHighlightMode). Всяко действие сочи към даден компонент чрез публикуваното си свойство Viewer и поддържа собствено състояние: всяко действие се деактивира само, когато няма отворен документ, навигационните действия проследяват позицията на страницата, копирането проверява дали има избрана област, а действията за режим и напасване синхронизират състоянието си (checked) с активния инструмент и режим на мащабиране. По време на проектиране (design time) всички те се регистрират в категорията losLab PDF на редактора на Action List; в режим на изпълнение (runtime) същите класове изискват само по три реда всеки

uses System.Actions, Vcl.ActnList, PDFlibViewer, PDFlibViewerActions;

procedure TMainForm.BuildToolbar;
var
  NextPage: TPDFlibViewerNextPage;
  FitWidth: TPDFlibViewerFitWidth;
  HandMode: TPDFlibViewerHandMode;
begin
  NextPage := TPDFlibViewerNextPage.Create(Self);
  NextPage.ActionList := ActionList1;
  NextPage.Viewer := FViewer;
  btnNext.Action := NextPage;

  FitWidth := TPDFlibViewerFitWidth.Create(Self);
  FitWidth.ActionList := ActionList1;
  FitWidth.Viewer := FViewer;
  btnFitWidth.Action := FitWidth;

  HandMode := TPDFlibViewerHandMode.Create(Self);
  HandMode.ActionList := ActionList1;
  HandMode.Viewer := FViewer;
  btnHand.Action := HandMode;
end;

Добре е да знаете за един капан, свързан с обхвата на модулите, ако изграждате собствена регистрация на действия върху този шаблон. RegisterActions е деклариран в System.Actions, а не във Vcl.ActnList, и грешката при компилиране, която получавате само с Vcl.ActnList в клаузата uses, е подвеждаща: литералът на масива от класове [TAction1, TAction2, ...] се тълкува от компилатора като множество, което води до поредица от грешки E2010 Incompatible types: 'Integer' and 'class of ...', изглеждащи като несъответствие на типовете във вашите класове за действия. Решението е просто добавяне на модула в uses, но съобщението за грешка сочи навсякъде другаде, освен към него. За лентите с инструменти за мащабиране, TPDFlibViewer.EffectiveZoomPercent връща действително ефективното мащабиране – включително изчисления коефициент при режими за напасване по ширина и по страница – така че стъпката на увеличаване може да се базира на реалното текущо увеличение, вместо на последната зададена стойност

Подчертаване на резултати от търсенето и обръщането на координатите зад него

HighlightSearchHits претърсва целия документ за дадена заявка и изчертава полупрозрачна лента над всяко съвпадение, връщайки броя на съвпаденията; ClearSearchHighlights, SearchHighlightCount и свойството SearchHighlightColor допълват тази функционалност. Тъй като правоъгълниците на съвпаденията се съхраняват в пространството на страницата и се препроектират при всяко начертаване, лентите остават залепени за своя текст, докато потребителят превърта, мащабира и завърта изгледа. Ако използвате придружаващия компонент TPDFlibSearchPanel, неговият метод RunSearch автоматично изчертава същите ленти в прикачения визуализатор чрез свойството HighlightInView, което е включено по подразбиране

procedure TMainForm.btnSearchClick(Sender: TObject);
var
  Hits: Integer;
begin
  FViewer.SearchHighlightColor := clYellow;
  Hits := FViewer.HighlightSearchHits(edtQuery.Text, False);  // case-insensitive
  StatusBar1.SimpleText := Format('%d matches', [Hits]);
end;

Текстовият двигател съобщава правоъгълниците на съвпаденията в координати, които зависят от настройката Origin на библиотеката: при Origin 0 или 3 оста Y се движи отдолу нагоре, както е в родното потребителско пространство на PDF, а при Origin 2 или 3 оста X се измерва от десния ръб. Покритието (overlay) на визуализатора работи в ориентирано отгоре надолу и отляво надясно пространство на страницата, така че всеки правоъгълник на съвпадение трябва да бъде обърнат (Y = PageHeight - Bottom и, където е приложимо, X = PageWidth - Right), преди да бъде съхранен – приемането на съобщения горен ръб наготово при начална точка по подразбиране изчертава подчертаването на вертикално огледална позиция на страницата. TPDFlibViewer извършва това преобразуване според активната начална точка (origin), когато записва всяко съвпадение, и след това прилага директното преобразуване на завъртането при начертаване, поради което лентите попадат правилно под всеки ъгъл. Самата полупрозрачност използва Windows API функцията AlphaBlend с постоянна алфа на източника, тъй като VCL платното (canvas) няма вградено алфа смесване. Подлежащите API за търсене на текст и геометрия са разгледани подробно в статията за търсене на текст и изброяване на елементите на страницата

Кликвания върху анотации, подсказки и експортиране на прикачени файлове

TPDFlibViewer разглежда анотациите като интерактивни обекти от първи клас. Щракването върху анотация, която не е връзка, задейства събитието OnAnnotClick с номера на страницата, 1-базирания индекс на анотацията и низа за подтипа; AnnotAtPagePoint излага същата проверка за съвпадение програмно и пропуска придружаващите изскачащи (Popup) анотации, така че винаги да получавате анотацията, която потребителят действително вижда. Посочването на анотация с мишката показва нейния текст Contents като подсказка (hint) на компонента чрез превключвателя AnnotHints, който е включен по подразбиране и работи съвместно със стандартното VCL свойство ShowHint. За анотации от тип FileAttachment методът SaveAnnotAttachmentToFile записва вградения файл на диска и връща 1 при успех, което реализира работен процес за записване при щракване само с няколко реда код

procedure TMainForm.ViewerAnnotClick(Sender: TObject; APage, AnnotIndex: Integer;
  const Subtype: WideString);
begin
  if Subtype = 'FileAttachment' then
    if FViewer.SaveAnnotAttachmentToFile(APage, AnnotIndex,
        'C:\Temp\attached-invoice.xml') = 1 then
      StatusBar1.SimpleText := 'Attachment saved';
end;

Печат и границите, които трябва да предвидите

Печатът не се нуждае от отделна интеграция. PrintDoc показва стандартния диалогов прозорец за печат на Windows – избор на принтер, диапазон на страниците, брой копия, сортиране – и предава задачата на печатащия двигател на библиотеката, докато PrintDoc(False) изпраща целия документ към принтера по подразбиране без никакъв потребителски интерфейс. PrintDocRange работи изцяло програмно с конкретно име на принтер, начална и крайна страница, брой копия и флаг за сортиране, което е правилната входна точка за групови или задействани от сървъра задачи за печат, управлявани от съния екземпляр на визуализатора

Две ограничения заслужават честно отбелязване. Първо, TPDFlibViewer е Windows VCL компонент; слоят за визуализация е изключен от компилации без VCL (NOVCL), така че конзолните услуги и платформи извън VCL трябва да използват директно базовото API за рендиране на TPDFlib. Второ, използването без графичен интерфейс (headless) се поддържа съзнателно, но е ограничено: CaptureRegion изобразява всяка клиентска област – страници плюс подчертавания – в растерно изображение, собственост на извикващия код, и работи върху компонент без зададен родител (Parent) и без дескриптор на прозорец (window handle), тъй като пътят на изчертаване избягва VCL извиквания, които биха наложили създаване на дескриптор. Това прави практическа проверката на ниво пиксели в автоматизирани тестове, но всичко, което наистина изисква цикъл за съобщения (message loop), като интерактивните инструменти за мишката, все още принадлежи на реална сесия с потребителски интерфейс. По отношение на производителността, компонентът поддържа кеш на страниците в паметта с конфигурируем лимит чрез SetCacheLimit и може да съхранява рендираните страници между сесиите; статията за дисков кеш на страниците на визуализатора разглежда подробно този слой и неговото поведение при различна разделителна способност на монитора (per-monitor DPI)

И така, стъпките са пет: поставяне на компонента и извикване на LoadFromFile, прикачване на готовите действия от PDFlibViewerActions, извикване на HighlightSearchHits за търсене в страницата, обработка на събитието OnAnnotClick за работни процеси с анотации и излагане на PrintDoc. Всеки елемент, показан тук, се доставя в стандартната библиотека losLab PDF Library за Delphi, където компонентът за визуализация, класовете за действия и двигателят за рендиране се инсталират заедно