Tehnički članak

Usporedna (Side-by-Side) PDF usporedba u Delphiju uz PDFium Component

Dva dokumenta otvorena odjednom, isti broj stranice, svaki u svojoj pomičnoj (scrollable) ploči: to je srž preglednika za usporedbu. PDFium Component to pruža kroz izravan objektni model gdje TPdf posjeduje datoteku, a TPdfView posjeduje prikaz (display). Jedan dokument, jedan TPdf, jedan TPdfView. Ako želite tri ploče, imate tri para. Teški dijelovi nisu pozivi API-ja; to su aritmetika rasporeda kada prozor mijenja veličinu i logika sinkronizacije stranica (page-sync) kada odlučujete koji bi prikaz trebao pratiti koji

Raspored forme (Form Layout)

VCL forma sadrži tri spremnika TScrollBox jedan pored drugog, svaki s TPdfView unutra i poravnatim s alClient tako da ispunjava okvir. Dvije komponente TSplitter nalaze se između okvira tako da korisnik može prilagoditi širinu stupaca tijekom izvođenja (runtime). Alatna traka (toolbar) iznad ploča nosi gumbe za otvaranje, kontrole zumiranja i prekidač za dva prikaza / tri prikaza (two-view / three-view toggle)

Način rada s tri prikaza (Three-view mode) logička (boolean) je vrijednost koju forma interno prati. Kada se prebaci, ponovno izračunavate širine te prikazujete ili skrivate treći stupac. Najjednostavniji pristup je izbrisati sva svojstva Align, sakriti splittere (razdjelnike), a zatim postaviti apsolutne pozicije:

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;

Postavljanje Align := alNone na sva tri okvira prije aritmetike cijelih brojeva izbjegava da se VCL mehanizam ograničenja (constraint engine) bori protiv vaših dodjela. Vratite vidljivost splittera nakon pozicioniranja ako želite povlačenje za promjenu veličine (drag-to-resize) u načinu rada s dva prikaza

Visina svakog okvira za pomicanje (scroll box) je klijentsko područje minus visina ploče s alatnom trakom. Budući da je alatna traka usidrena na vrhu s alTop, ClientHeight - PanelButtons.Height daje vam iskoristivi okomiti prostor. Dodijelite to za sva tri okvira unutar istog poziva UpdateLayout kako nikada ne bi postojao okvir u kojem je jedan viši od ostalih i uzrokuje treperenje (flicker) rasporeda

Otvaranje dokumenta

Svaki par ploča treba vlastiti postupak otvaranja (open procedure). Obrazac je kratak: deaktivirajte komponentu, postavite naziv datoteke, pokušajte aktivirati, uhvatite EPdfError ako datoteka zahtijeva lozinku. Imajte na umu da je TPdfView.Active ono što kontrolira iscrtavanje, ali TPdf.Active je ono što zapravo otvara datoteku; oni su neovisni. Postavljanje PdfView.Active := True kada njegov povezani TPdf još nije aktivan je bezopasno, ali ne prikazuje ništa

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;

Uvijek provjerite PdfComponent.Active nakon dodjele; oštećena datoteka ili pogrešna lozinka uzrokuju tihi neuspjeh učitavanja bez pokretanja iznimke u zadanoj putanji (default path). Eksplicitno postavljanje PdfViewComponent.PageNumber := 1 nakon uspješnog otvaranja izbjegava zastarjeli broj stranice iz prethodnog dokumenta

Kod za rukovanje lozinkom iznad pokreće (raises) bilo koju pogrešku osim poznate poruke o lozinki. To je namjerno: želite da se oštećene ili nepodržane datoteke pojave na površini (surface immediately) umjesto da budu progutane kao tiha prazna ploča. Korisnik koji ne vidi ništa nema pojma je li se datoteka učitala i jednostavno je prazna, ili ju je komponenta odbila. Pokretanje (Raising) zadržava pogrešku vidljivom

Praćenje aktivne ploče (Active Panel Tracking)

Kada korisnik klikne unutar ploče, ta ploča postaje aktivna. Forma prati privatno polje FActivePdfView: TPdfView. Vizualna povratna informacija (Visual feedback) je promjena boje obruba na sadržanom TScrollBox: postavite na clHighlight za aktivnu i clWindow za ostale. Povežite (Wire) ovo sa svakim TPdfView.OnClick i s postupkom otvaranja tako da fokus prati dokument koji ste upravo otvorili

Neke se operacije primjenjuju na sve vidljive ploče, a ne samo na aktivnu. Logička (boolean) vrijednost FAllViewsMode na formi pokreće tu granu. Kada je postavljeno na true, promjene zumiranja i navigacija stranicama šire se (fan out) na svaku ploču koja ima aktivan 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;

Sinkronizirana navigacija po stranicama

Sinkronizirana navigacija nije obavezna, ali je korisna za tijekove rada (workflows) revizije dokumenata gdje obje datoteke pokrivaju isti raspon stranica. Logika pripada rukovatelju događajima (event handler) koji se pokreće nakon što korisnik pretražuje jedan prikaz. Kada izvorni prikaz promijeni svoj PageNumber, rukovatelj propagira taj broj na druge prikaze, podložno jednom ograničenju (guard): ciljani prikaz mora imati barem toliko stranica, inače preskočite

PageNumber na TPdfView i na TPdf su neovisni. TPdf.PageNumber prati koju stranicu komponenta dokumenta smatra trenutnom; TPdfView.PageNumber prati ono što je prikazano na zaslonu. Za potrebe navigacije želite svojstvo prikaza (view property), a ne svojstvo dokumenta

Potvrdni okvir (checkbox) s oznakom poput "Sinkroniziraj stranice" (Sync pages) daje korisniku kontrolu. Kada nije označen, svaka se ploča kreće (navigates) neovisno i rukovatelj (handler) odmah izlazi. Ta je neovisnost važna za slučajeve upotrebe u kojima dva dokumenta imaju različit broj stranica ili gdje korisnik želi pronaći ekvivalentni odlomak u prijevodu koji počinje na drugoj stranici. Stalno forsiranje sinkronizacije učinilo bi alat težim za upotrebu od jednostavnog rasporeda na radnoj površini s dva prozora

Jedna stvar na koju treba paziti: programsko postavljanje PdfView.PageNumber unutar rukovatelja sinkronizacijom (sync handler) samo će po sebi pokrenuti događaj promjene (change event) na tom prikazu. Čuvajte se beskonačne rekurzije pomoću logičke (boolean) zastavice koju postavite prije dodjele i obrišete odmah nakon toga. Zastavica je po formi (per-form), a ne po prikazu (per-view), jer sva tri prikaza dijele isti rukovatelj (handler)

Zumiranje po ploči (Zoom Per Panel)

Svaki TPdfView nosi vlastito svojstvo Zoom, Double u postocima gdje Zoom := 100 znači stvarnu veličinu (100%). Njegovo postavljanje nadjačava (overrides) svaki aktivni FitMode. Za gumb prilagodi širini (fit-to-width) na aktivnoj ploči, očitajte zumiranje prilagodbe s PdfView.PageWidthZoom[PdfView.PageNumber] i dodijelite ga. Za prilagodbu stranici (fit-to-page) koristite PageZoom[PageNumber]. Obje su svojstva polja (array properties) indeksirana brojem stranice temeljenom na 1 (1-based), stoga se zaštitite (guard) od nultog broja stranice prije nego im pristupite

Kada izvozite trenutnu stranicu u sliku, pročitajte rotaciju (rotation) iz prikaza, ali pozovite RenderPage na komponenti TPdf, a ne na prikazu. Bitmap oblik TPdf.RenderPage preuzima eksplicitne dimenzije piksela plus vrijednost TRotation i skup TRenderOptions. Varijanta funkcije vraća TBitmap u vlasništvu pozivatelja koji sami oslobađate (free) nakon spremanja:

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;

Množitelj 2x na širinu i visinu daje oštriji izlaz za dokumente sa sitnim tekstom (fine text). try/finally oko oslobađanja bitmape nije neobavezno; otkazivanje (cancel) TSaveDialog i dalje pogađa finally blok, a vi želite da bitmapa bude otpuštena (released) bez obzira na to što je korisnik učinio

Zahtjevi za DLL

PDFium Component omata (wraps) izvornu pdfium biblioteku. 32-bitnom host procesu (host process) treba pdfium32.dll; 64-bitnom hostu treba pdfium64.dll. Varijante s V8 JavaScript mehanizmom dodaju sufiks v8 i teže otprilike 23-27 MB u usporedbi sa 5-6 MB u standardnim verzijama (builds). Za preglednik usporedbi koji onemogućuje ispunjavanje obrazaca (Pdf.FormFill := False), standardna ne-V8 verzija je dovoljna i održava distribuciju manjom

Smjestite DLL u isti direktorij s izvršnom datotekom (executable) ili u bilo koji direktorij na sistemskom PATH-u. Komponenta ga učitava na zahtjev kada se aktivira prvi TPdf, tako da se DLL koji nedostaje pojavljuje u tom trenutku, a ne pri pokretanju aplikacije. Ako isporučujete (ship) instalacijski program (installer), najpouzdaniji je pristup kopirati DLL u mapu aplikacije tijekom instalacije umjesto oslanjanja na direktorij sustava koji administrator može kasnije izbrisati

V8 verzije primarno su korisne kada trebate komunicirati (interact) s PDF JavaScript radnjama, na primjer za pokretanje polja izračuna ili podnošenje rukovatelja (submit handlers). Pasivni preglednik usporedbe nema razloga za pokretanje JavaScripta; postavljanje Pdf.FormFill := False prije Active := True u potpunosti preskače okruženje za popunjavanje obrasca (form-fill environment), što također znači da se JS mehanizam ne inicijalizira čak ni ako se koristi standardna verzija. To je ispravna zadana postavka za preglednik samo za čitanje (read-only) bez obzira koju varijantu DLL-a isporučujete

Za više detalja o komponenti PDFium Component i njezinom punom API-ju posjetite stranicu proizvoda Delphi PDFium Component