Technisch artikel

Bouw een PDF-viewer in Delphi met de PDFium-component

Een PDF-viewer in Delphi komt neer op twee componenten en de bedrading (wiring) ertussen. TPdf is eigenaar van het document: het opent het bestand, decodeert (decrypt) het en beantwoordt vragen over het aantal pagina's en de metadata. TPdfView is het visuele besturingselement (visual control) dat pagina's op het scherm schildert en het scrollen, de zoom en de pagina bekijkt waar de gebruiker momenteel naar kijkt. De PDFium-component omhult dezelfde rendering-engine die wordt meegeleverd in Chrome, dus de tekens, anti-aliasing en kleur die u op het canvas krijgt, komen overeen met wat uw gebruikers al in hun browser zien. Het werk zit niet in het renderen. Het zit in het verbinden van het documentobject met de weergave (view), het laden zonder te crashen op een beschadigd of met een wachtwoord beveiligd bestand, en het geven van de handvol besturingselementen aan de gebruiker die een viewer 'af' (finished) laten voelen: de pagina omslaan, de zoom wijzigen en de pagina passend maken voor het venster

Dit artikel doorloopt die opbouw in de volgorde waarin u deze daadwerkelijk bouwt. Alles hierin rendert één enkele pagina tegelijk, wat de meeste documentworkflows ook willen. Als u pagina's in één continu scrollende kolom gestapeld wilt hebben, is dat een andere lay-outbeslissing en niet het pad dat hier wordt bewandeld

TPdf koppelen aan TPdfView

Plaats (drop) een TPdf en een TPdfView op het formulier en vertel de weergave vervolgens welk document moet worden weergegeven. Die enkele toewijzing (assignment) is de volledige link tussen het niet-visuele document en het besturingselement dat het tekent

procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf and PdfView were dropped at design time.
  PdfView.Pdf := Pdf;                 // the view paints whatever this document holds
  PdfView.FitMode := pfmFitWidth;     // start the user at a sensible zoom
end;

Voordat dit alles wordt uitgevoerd, moet de native PDFium-bibliotheek zich op de machine bevinden. De PDFium-component roept pdfium32.dll of pdfium64.dll aan, afhankelijk van uw doelplatform (target platform), en het document weigert simpelweg te openen als de DLL niet kan worden gevonden. Lever de bijbehorende DLL mee naast uw executable, of plaats deze op een plek waar de systeemlader (system loader) deze zal vinden. De versies (builds) met V8-ondersteuning bestaan alleen voor PDF's die JavaScript bevatten dat u wilt uitvoeren, wat een eenvoudige viewer niet doet. Grijp dus naar de standaard-DLL tenzij u een concrete reden hebt om dat niet te doen

Een document laden zonder de invoer te vertrouwen

De eerste ingeving is om het laden in een try/except-blok te verpakken en een gegenereerde uitzondering (exception) te behandelen als een mislukking. Dat instinct is hier verkeerd, en het fout doen levert een viewer op die er prima uitziet totdat iemand er een kapot bestand aan overhandigt. Het instellen van Active := True genereert geen foutmelding (raise) bij een mislukte laadpoging. De PDFium-component vangt de interne fout op en laat Active op False staan. De enige eerlijke manier om te weten of het document is geopend, is dus om de eigenschap (property) terug te lezen nadat u deze hebt ingesteld

procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // never raises; failure leaves Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // the view tracks its own current page
  UpdatePageLabel;
end;

Twee dingen verdienen de aandacht. Het eerste is dat PageNumber op beide objecten bestaat en dat die twee onafhankelijk zijn. Pdf.PageNumber is de notie van het document van een huidige pagina; PdfView.PageNumber is de pagina die het besturingselement (control) daadwerkelijk weergeeft, en dit is degene die u instelt om de gebruiker door het bestand te verplaatsen. Het instellen van het ene object verplaatst het andere niet, dus een viewer stuurt altijd de eigenschap van de weergave (view) aan. Het tweede is de 1-gebaseerde indexering: pagina's lopen van 1 tot Pdf.PageCount, niet vanaf 0. Dit zet iedereen op het verkeerde been die gewend is aan zero-based arrays

Een gecodeerd (encrypted) bestand afhandelen

Gecodeerde documenten integreren in hetzelfde laadpad. Als het open-wachtwoord is ingesteld vóór activering, decodeert (decrypts) het document naarmate het opent. Als het onjuist is of ontbreekt, blijft Active op False staan, precies zoals het geval is voor een corrupt bestand. Het herstel (recovery) is dus om om een wachtwoord te vragen en de activering opnieuw te proberen

procedure TFormMain.OpenWithPassword(const FileName: string);
var
  Password: string;
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    if InputQuery('Password required', 'Password:', Password) then
    begin
      Pdf.Password := Password;       // must be set before Active := True
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Unable to open the document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

Omdat de fout stilzwijgend is voor zowel een onjuist wachtwoord als een beschadigd bestand, kunt u de twee niet onderscheiden op basis van Active alleen. In de praktijk is dat acceptabel voor een viewer: de gebruiker geeft óf het juiste wachtwoord op, óf merkt dat het bestand niet wil openen, en het bericht leest in beide gevallen hetzelfde

Door het document bladeren (Paging)

Wanneer het document open is, is navigatie een kwestie van rekenen met PdfView.PageNumber, begrensd door Pdf.PageCount. Het enige echte werk is klemmen (clamping), zodat de knoppen de pagina nooit buiten het bereik duwen en de eerste en laatste knoppen uitgeschakeld blijven aan de uiteinden van het bestand

procedure TFormMain.GoToPage(NewPage: Integer);
begin
  if not Pdf.Active then
    Exit;
  if NewPage < 1 then
    NewPage := 1
  else if NewPage > Pdf.PageCount then
    NewPage := Pdf.PageCount;
  PdfView.PageNumber := NewPage;
  UpdatePageLabel;
end;

// the four navigation buttons reduce to one call each
procedure TFormMain.FirstClick(Sender: TObject);  begin GoToPage(1); end;
procedure TFormMain.PrevClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber - 1); end;
procedure TFormMain.NextClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber + 1); end;
procedure TFormMain.LastClick(Sender: TObject);   begin GoToPage(Pdf.PageCount); end;

Een "ga naar pagina N"-tekstvak is dezelfde GoToPage-aanroep die wordt gevoed door een geparseerd geheel getal (integer). De 'clamp' dekt het geval waarin de gebruiker 9999 typt in een bestand van tien pagina's. Behoud UpdatePageLabel als de enige plek die "Pagina 3 van 12" schrijft, zodat de weergave (readout) nooit uit de pas (out of sync) gaat lopen met wat de view toont

Zoom: expliciete percentages en weergavemodi (fit modes)

Zoomen met TPdfView komt in twee smaken (flavors) die op elkaar inwerken. Het begrijpen van de interactie maakt het verschil tussen een zoomregelaar die zich gedraagt en een regelaar die de gebruiker tegenwerkt. De directe route is de Zoom-eigenschap, een percentage waarbij 100 staat voor werkelijke grootte. De andere route is FitMode, wat de weergave (view) vertelt om de zoom voor u te bereken en deze te blijven herberekenen zodra de grootte van het venster verandert

// fixed magnifications
PdfView.Zoom := 100;     // actual size
PdfView.Zoom := 50;      // half
PdfView.Zoom := 200;     // double

// let the view size the page to the window, and keep it sized on resize
PdfView.FitMode := pfmFitWidth;   // page width fills the control
PdfView.FitMode := pfmFitPage;    // whole page visible
PdfView.FitMode := pfmActualSize; // 1:1 with the document's points

Hier is het onderdeel dat mensen op het verkeerde been zet. Het rechtstreeks toewijzen van Zoom reset FitMode naar pfmNone. Dat is correct gedrag, geen bug (fout): op het moment dat de gebruiker exact 150% kiest, kan de weergave niet langer ook "aan breedte aanpassen" (fit to width) in acht nemen, omdat de twee verzoeken in strijd zijn. De consequentie voor uw UI (gebruikersinterface) is dat een zoom-in-knop en een 'aan pagina aanpassen'-knop staten zijn die elkaar uitsluiten. De werkbalk (toolbar) moet de actieve modus zichtbaar maken. Wanneer de gebruiker op 'aan pagina aanpassen' klikt, stelt u FitMode in. Wanneer er op een numerieke zoom wordt geklikt, stelt u Zoom in en laat u deze uit zichzelf de weergavemodus wissen

Als u de 'fit'-waarde (fit value) liever zelf wilt berekenen, bijvoorbeeld om een zoomschuifregelaar te voeden met het huidige aanpassingspercentage, dan geven de per-page helpers (helpers per pagina) u de cijfers zonder de modus te wijzigen. PageWidthZoom[N], PageZoom[N] en ActualSizeZoom[N] retourneren het percentage dat pagina N aan de breedte zou aanpassen, in zijn geheel passend zou maken of op ware grootte zou weergeven

// seed a zoom readout from the fit-to-width value of the current page
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

Wat een afgewerkte (finished) viewer daadwerkelijk nodig heeft

De bovenstaande viewer is een paar dozijn regels lang en doet al de taak die een documentworkflow nodig heeft: een bestand openen, een beschadigd (bad) exemplaar overleven, een pagina tonen, navigeren (move) tussen pagina's en de vergroting (magnification) handmatig of passend veranderen. PDFium doet the harde werk in stilte. Ingesloten lettertypen (embedded fonts) lossen zich op, annotaties en formuliervelden worden geverfd (painted) op de plek waar het document ze plaatst. De pagina die u ziet, is bovendien identiek aan de pagina die een Chrome-gebruiker zou zien, omdat het dezelfde engine is die beide tekent

Vanuit deze basis zijn de toevoegingen eerder incrementeel dan structureel. Tekstselectie en zoeken worden afgelezen van dezelfde tekstlaag die PDFium reeds opbouwt. Metadata zoals Pdf.Title en Pdf.Author is slechts één ingelezen eigenschap verwijderd. Rotatie en grijstinten (grayscale) zijn renderopties die u doorgeeft (passes) wanneer u een pagina naar een bitmap tekent. Geen van deze zaken verandert de ruggengraat (spine) die u hier hebt, namelijk het documentobject, de view (weergave) en de laad-en-navigeer flow (load-then-navigate flow) die ze met elkaar verbindt. Krijg die ruggengraat goed op orde en de rest is decoratie

De doorlopend gebruikte componenten TPdf en TPdfView maken deel uit van de PDFium-component voor Delphi en C++Builder, die de volledige viewer-referentie op zijn productpagina (product page) draagt