Teknisk artikkel

Side-ved-side PDF-sammenligning i Delphi med PDFium-komponent

To dokumenter åpne på en gang, samme sidetall, hvert i sitt eget rullbare (scrollable) panel: det er kjernen i en sammenligningsviser (comparison viewer). PDFium-komponenten leverer dette gjennom en rett frem objektmodell der TPdf eier filen og TPdfView eier visningen. Ett dokument, én TPdf, én TPdfView. Vil du ha tre paneler, har du tre par. De vanskelige delene er ikke API-kallene; det er layoutaritmetikken når vinduet endrer størrelse og side-synk-logikken når du bestemmer hvilken visning som skal følge hvilken

Skjemaoppsett (Form Layout)

VCL-skjemaet (VCL form) holder tre TScrollBox-containere side ved side, hver med en TPdfView inni og justert (aligned) til alClient slik at den fyller boksen. To TSplitter-komponenter sitter mellom boksene slik at brukeren kan justere kolonnebredder ved kjøretid. En verktøylinje (toolbar) over panelene bærer åpneknappene, zoom-kontrollene, og vekslebryteren (toggle) for to-visning / tre-visning

Tre-visningsmodus er en boolsk verdi (boolean) skjemaet sporer internt. Når den vipper (flips), omberegner du bredder og viser eller skjuler den tredje kolonnen. Den enkleste tilnærmingen er å tømme alle Align-egenskaper, skjule splitterne, og deretter angi absolutte posisjoner:

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;

Å sette Align := alNone på alle tre boksene før heltallsaritmetikken unngår at VCL-begrensningsmotoren kjemper mot tilordningene dine. Gjenopprett splitterens synlighet etter posisjonering hvis du vil ha dra-for-å-endre-størrelse (drag-to-resize) i to-visningsmodus

Høyden på hver rulleboks (scroll box) er klientområdet minus verktøylinjens panelhøyde. Fordi verktøylinjen er dokket på toppen med alTop, gir ClientHeight - PanelButtons.Height deg det brukbare vertikale rommet. Tilordne dette til alle tre boksene inni det samme UpdateLayout-kallet slik at det aldri er en ramme der én boks er høyere enn de andre og forårsaker et layout-flimmer (layout flicker)

Åpne et dokument

Hvert panelpar trenger sin egen åpningsprosedyre. Mønsteret er kort: deaktiver komponenten, sett filnavnet, aktiver, sjekk deretter Active; hvis det forble False, be om et passord og prøv igjen. Legg merke til at TPdfView.Active er det som styrer gjengivelsen (rendering), men TPdf.Active er det som faktisk åpner filen; de er uavhengige. Å sette PdfView.Active := True når dens tilknyttede TPdf ennå ikke er aktiv er ufarlig, men viser ingenting

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;

Sjekk alltid PdfComponent.Active etter tilordningen; en skadet fil eller feil passord fører til at lastingen mislykkes i det stille uten å heve et unntak (raise an exception) i standardbanen. Å sette PdfViewComponent.PageNumber := 1 eksplisitt etter en vellykket åpning unngår et foreldet (stale) sidetall fra det forrige dokumentet

Meldingsdialogen (message dialog) på slutten er bevisst: du vil at korrupte eller ikke-støttede filer skal dukke opp umiddelbart snarere enn å bli svelget som et stille blankt panel. En bruker som ikke ser noen ting, har ingen anelse om filen ble lastet og bare er tom, eller om komponenten avviste den. Å rapportere feilen holder feilen synlig

Sporing av aktivt panel

Når brukeren klikker inni et panel, blir det panelet aktivt. Skjemaet (form) sporer et privat FActivePdfView: TPdfView felt. Visuell tilbakemelding er en endring i rammefarge på den inneholdende TScrollBox: sett den til clHighlight for den aktive og clWindow for de andre. Knytt dette (wire this) til hver TPdfView.OnClick og til åpningsprosedyren slik at fokus følger dokumentet du akkurat åpnet

Noen operasjoner gjelder for alle synlige paneler i stedet for bare det aktive. En boolsk variabel (boolean) FAllViewsMode på skjemaet driver den grenen. Når den er sann, sprer zoom-endringer og sidenavigasjon seg (fan out) til hvert panel som har et aktivt dokument:

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;

Synkronisert sidenavigasjon

Synkronisert navigasjon er valgfritt, men nyttig for dokumentrevisjonsarbeidsflyter (document revision workflows) der begge filer dekker det samme sideintervallet (page range). Logikken hører hjemme i en hendelseshåndterer (event handler) som fyrer av etter at brukeren navigerer i én visning. Når en kildevisning endrer sin PageNumber, propagerer håndtereren det tallet til de andre visningene, underlagt én vakt (guard): målvisningen må ha minst så mange sider, ellers hopp over

PageNumberTPdfView og på TPdf er uavhengige. TPdf.PageNumber sporer hvilken side dokumentkomponenten anser som gjeldende (current); TPdfView.PageNumber sporer hva som vises på skjermen. For navigasjonsformål vil du ha visningsegenskapen, ikke dokumentegenskapen

En avkrysningsboks (checkbox) merket med noe sånt som "Synkroniser sider" gir brukeren kontroll. Når den ikke er krysset av, navigerer hvert panel uavhengig og håndtereren avslutter umiddelbart. Den uavhengigheten er viktig for brukstilfeller der de to dokumentene har forskjellig antall sider, eller der brukeren vil finne den tilsvarende passasjen i en oversettelse som starter på en annen side. Å tvinge frem synk alltid ville gjøre verktøyet vanskeligere å bruke enn en enkel to-vinduers skrivebordsordning

En ting å passe på: å sette PdfView.PageNumber programmatisk inni synk-håndtereren vil i seg selv utløse (trigger) endringshendelsen på den visningen. Beskytt mot uendelig rekursjon (infinite recursion) med et boolsk flagg som du setter før tilordningen og fjerner (clear) umiddelbart etter. Flagget er per-skjema, ikke per-visning, fordi alle tre visninger deler den samme håndtereren

Zoom per panel

Hver TPdfView bærer sin egen Zoom-egenskap, en Double i prosent der Zoom := 100 betyr faktisk størrelse (100%). Å angi den overstyrer enhver aktiv FitMode. For en tilpass-til-bredde-knapp (fit-to-width button) på det aktive panelet, leser du tilpass-zoomen (fit zoom) fra PdfView.PageWidthZoom[PdfView.PageNumber] og tilordner den. For tilpass-til-side (fit-to-page), bruk PageZoom[PageNumber]. Begge er array-egenskaper indeksert av et 1-basert sidetall, så vakt mot et null-sidetall før du aksesserer dem

Når du eksporterer den gjeldende siden til et bilde, leser du rotasjonen fra visningen, men kaller RenderPageTPdf-komponenten, ikke visningen. Bitkart-formen (bitmap form) av TPdf.RenderPage tar eksplisitte pikseldimensjoner pluss en TRotation-verdi og et TRenderOptions-sett. Funksjonsvarianten returnerer et oppringereid (caller-owned) TBitmap som du frigjør selv etter lagring:

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 multiplikatoren på bredde og høyde gir skarpere utdata for dokumenter med fin tekst. try/finally rundt bitkart-frigjøringen er ikke valgfri; en TSaveDialog-avbrytelse treffer fortsatt finally-blokken, og du vil at bitkartet skal frigis uavhengig av hva brukeren gjorde

DLL-krav

PDFium-komponenten (PDFium Component) pakker inn (wraps) det native pdfium-biblioteket. En 32-biters vertsprosess (host process) trenger pdfium32.dll; en 64-biters vert trenger pdfium64.dll. Varianter med V8 JavaScript-motoren (JavaScript engine) legger til v8-suffikset og veier omtrent 23-27 MB kontra de 5-6 MB standardbyggene (standard builds) veier. For en sammenligningsviser (comparison viewer) som deaktiverer skjemautfylling (Pdf.FormFill := False), er det standard ikke-V8 bygget tilstrekkelig og holder distribusjonen mindre

Plasser DLL-en i samme katalog som den kjørbare filen (executable), eller i hvilken som helst katalog på systemets PATH. Komponenten laster den på forespørsel (on demand) når den første TPdf-en aktiveres, så en manglende DLL dukker opp på det tidspunktet fremfor ved applikasjonsstart. Hvis du leverer (ship) et installasjonsprogram (installer), er den mest pålitelige tilnærmingen å kopiere DLL-en inn i applikasjonsmappen under installasjon i stedet for å stole på en systemkatalog som en administrator kan rydde opp i senere

V8-byggene er primært nyttige når du trenger å samhandle med PDF JavaScript-handlinger, for eksempel for å utløse kalkulasjonsfelt (calculation fields) eller sende inn håndterere (submit handlers). En passiv sammenligningsviser har ingen grunn til å kjøre JavaScript; å sette Pdf.FormFill := False før Active := True hopper over skjemautfyllingsmiljøet helt, noe som også betyr at ingen JS-motor initialiseres selv om standardbygget brukes. Det er riktig standardinnstilling for en skrivebeskyttet (read-only) viser uavhengig av hvilken DLL-variant du leverer

For ytterligere detaljer om PDFium-komponenten og dens fulle API, besøk Delphi PDFium-komponent produktsiden