Τεχνικό Άρθρο

Σύγκριση PDF Δίπλα-Δίπλα στο Delphi με το PDFium Component

Δύο έγγραφα ανοιχτά ταυτόχρονα, ίδιος αριθμός σελίδας, το καθένα στο δικό του κυλιόμενο (scrollable) πάνελ: αυτός είναι ο πυρήνας ενός προγράμματος προβολής σύγκρισης. Το PDFium Component το προσφέρει αυτό μέσω ενός απλού μοντέλου αντικειμένου όπου το TPdf κατέχει το αρχείο και το TPdfView κατέχει την οθόνη. Ένα έγγραφο, ένα TPdf, ένα TPdfView. Αν θέλετε τρία πάνελ, έχετε τρία ζεύγη. Τα δύσκολα σημεία δεν είναι οι κλήσεις API. Είναι η αριθμητική διάταξης όταν το παράθυρο αλλάζει μέγεθος και η λογική συγχρονισμού σελίδας όταν αποφασίζετε ποια προβολή πρέπει να ακολουθεί ποια

Διάταξη Φόρμας

Η φόρμα VCL περιέχει τρία κοντέινερ TScrollBox δίπλα-δίπλα, το καθένα με ένα TPdfView μέσα, ευθυγραμμισμένο (aligned) στο alClient ώστε να γεμίζει το πλαίσιο. Δύο στοιχεία ελέγχου TSplitter βρίσκονται ανάμεσα στα πλαίσια έτσι ώστε ο χρήστης να μπορεί να προσαρμόσει τα πλάτη των στηλών κατά τον χρόνο εκτέλεσης. Μια γραμμή εργαλείων (toolbar) πάνω από τα πάνελ περιέχει τα κουμπιά ανοίγματος, τα στοιχεία ελέγχου ζουμ και την εναλλαγή προβολής δύο/τριών πάνελ

Η λειτουργία τριών προβολών (three-view mode) είναι μια boolean τιμή που η φόρμα παρακολουθεί εσωτερικά. Όταν αλλάζει, υπολογίζετε ξανά τα πλάτη και εμφανίζετε ή κρύβετε την τρίτη στήλη. Η απλούστερη προσέγγιση είναι να διαγράψετε όλες τις ιδιότητες 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;
    // 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;

Η ρύθμιση Align := alNone και στα τρία πλαίσια πριν από την ακέραια αριθμητική αποτρέπει τη μηχανή περιορισμών του VCL από το να καταπολεμήσει τις αναθέσεις σας. Επαναφέρετε την ορατότητα των διαχωριστών μετά την τοποθέτηση εάν θέλετε αλλαγή μεγέθους με μεταφορά (drag-to-resize) στη λειτουργία δύο προβολών

Το ύψος κάθε πλαισίου κύλισης (scroll box) είναι η περιοχή πελάτη (client area) μείον το ύψος του πίνακα της γραμμής εργαλείων. Επειδή η γραμμή εργαλείων είναι αγκυρωμένη στο επάνω μέρος με alTop, το ClientHeight - PanelButtons.Height σας δίνει τον χρησιμοποιήσιμο κατακόρυφο χώρο. Αναθέστε το και στα τρία πλαίσια μέσα στην ίδια κλήση UpdateLayout ώστε να μην υπάρχει ποτέ καρέ όπου το ένα πλαίσιο είναι ψηλότερο από τα άλλα, προκαλώντας τρεμόπαιγμα διάταξης (layout flicker)

Άνοιγμα ενός Εγγράφου

Κάθε ζεύγος πάνελ χρειάζεται τη δική του διαδικασία ανοίγματος. Το μοτίβο είναι σύντομο: απενεργοποιήστε το στοιχείο (component), ορίστε το όνομα αρχείου, προσπαθήστε να ενεργοποιήσετε, πιάστε (catch) το EPdfError εάν το αρχείο απαιτεί κωδικό πρόσβασης. Σημειώστε ότι το 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 := '';

  try
    PdfComponent.Active := True;
  except
    on E: EPdfError do
    begin
      if InputQuery('Password', 'Enter document password:', Password) then
      begin
        PdfComponent.Password := Password;
        PdfComponent.Active   := True;
      end
      else
        raise;
    end;
  end;

  if PdfComponent.Active then
  begin
    PdfViewComponent.PageNumber := 1;
    SetActivePdfView(PdfViewComponent);
  end;
end;

Ελέγχετε πάντα το PdfComponent.Active μετά την ανάθεση. Ένα κατεστραμμένο αρχείο ή λανθασμένος κωδικός πρόσβασης προκαλεί σιωπηλή αποτυχία φόρτωσης χωρίς να προκαλέσει εξαίρεση στην προεπιλεγμένη διαδρομή (default path). Η ρητή ρύθμιση PdfViewComponent.PageNumber := 1 μετά από ένα επιτυχημένο άνοιγμα αποφεύγει έναν παλιό (stale) αριθμό σελίδας από το προηγούμενο έγγραφο

Ο κώδικας χειρισμού κωδικών πρόσβασης παραπάνω εγείρει (raises) εξαίρεση σε οποιοδήποτε σφάλμα εκτός από το γνωστό μήνυμα κωδικού πρόσβασης. Αυτό είναι σκόπιμο: θέλετε τα κατεστραμμένα ή μη υποστηριζόμενα αρχεία να εμφανίζονται αμέσως αντί να καταπίνονται ως ένα ήσυχο κενό πάνελ. Ένας χρήστης που δεν βλέπει τίποτα δεν έχει ιδέα αν το αρχείο φορτώθηκε και είναι απλώς κενό ή αν το στοιχείο το απέρριψε. Η έγερση της εξαίρεσης διατηρεί το σφάλμα ορατό

Παρακολούθηση Ενεργού Πάνελ (Active Panel Tracking)

Όταν ο χρήστης κάνει κλικ μέσα σε ένα πάνελ, αυτό το πάνελ γίνεται ενεργό. Η φόρμα παρακολουθεί ένα ιδιωτικό πεδίο (private field) FActivePdfView: TPdfView. Η οπτική ανάδραση (visual feedback) είναι μια αλλαγή χρώματος περιγράμματος στο περιέχον TScrollBox: ορίστε το σε clHighlight για το ενεργό και clWindow για τα άλλα. Συνδέστε το σε κάθε TPdfView.OnClick και στη διαδικασία ανοίγματος ώστε η εστίαση να ακολουθεί το έγγραφο που μόλις ανοίξατε

Ορισμένες λειτουργίες ισχύουν για όλα τα ορατά πάνελ και όχι μόνο για το ενεργό. Μια boolean μεταβλητή FAllViewsMode στη φόρμα οδηγεί αυτόν τον κλάδο. Όταν είναι αληθές (true), οι αλλαγές ζουμ και η πλοήγηση σελίδας εξαπλώνονται σε κάθε πάνελ που έχει ενεργό έγγραφο:

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;

Συγχρονισμένη Πλοήγηση Σελίδας

Η συγχρονισμένη πλοήγηση είναι προαιρετική, αλλά χρήσιμη για ροές εργασιών αναθεώρησης εγγράφων όπου και τα δύο αρχεία καλύπτουν το ίδιο εύρος σελίδων. Η λογική ανήκει σε έναν χειριστή συμβάντων (event handler) που ενεργοποιείται αφού ο χρήστης πλοηγηθεί σε μία προβολή. Όταν μια προβολή προέλευσης αλλάζει το PageNumber, ο χειριστής μεταδίδει αυτόν τον αριθμό στις άλλες προβολές, με την εξής προϋπόθεση: η προβολή στόχος πρέπει να έχει τουλάχιστον τόσες σελίδες, αλλιώς γίνεται παράβλεψη (skip)

Το PageNumber στο TPdfView και στο TPdf είναι ανεξάρτητα. Το TPdf.PageNumber παρακολουθεί ποια σελίδα το στοιχείο εγγράφου θεωρεί τρέχουσα. Το TPdfView.PageNumber παρακολουθεί τι εμφανίζεται στην οθόνη. Για σκοπούς πλοήγησης, θέλετε την ιδιότητα της προβολής, όχι την ιδιότητα του εγγράφου

Ένα πλαίσιο ελέγχου (checkbox) με ετικέτα κάτι σαν "Συγχρονισμός σελίδων" δίνει τον έλεγχο στον χρήστη. Όταν δεν είναι επιλεγμένο, κάθε πάνελ πλοηγείται ανεξάρτητα και ο χειριστής εξέρχεται αμέσως. Αυτή η ανεξαρτησία είναι σημαντική για περιπτώσεις χρήσης όπου τα δύο έγγραφα έχουν διαφορετικό αριθμό σελίδων, ή όπου ο χρήστης θέλει να βρει το αντίστοιχο απόσπασμα σε μια μετάφραση που ξεκινά σε διαφορετική σελίδα. Η επιβολή συγχρονισμού πάντα, θα καθιστούσε το εργαλείο πιο δύσκολο στη χρήση από μια απλή διάταξη επιφάνειας εργασίας με δύο παράθυρα

Ένα πράγμα που πρέπει να προσέξετε: η ρύθμιση PdfView.PageNumber προγραμματιστικά μέσα στον χειριστή συγχρονισμού (sync handler) θα ενεργοποιήσει και η ίδια το συμβάν αλλαγής σε αυτήν την προβολή. Προστατευθείτε από την άπειρη αναδρομή (infinite recursion) με μια σημαία boolean που ορίζετε πριν από την ανάθεση και διαγράφετε αμέσως μετά. Η σημαία είναι ανά φόρμα (per-form), όχι ανά προβολή (per-view), επειδή και οι τρεις προβολές μοιράζονται τον ίδιο χειριστή

Ζουμ Ανά Πάνελ

Κάθε TPdfView φέρει τη δική του ιδιότητα Zoom, έναν τύπο Double σε ποσοστό (percent) όπου το Zoom := 100 σημαίνει πραγματικό μέγεθος (100%). Ο ορισμός του παρακάμπτει οποιαδήποτε ενεργή λειτουργία FitMode. Για ένα κουμπί προσαρμογής στο πλάτος (fit-to-width) στο ενεργό πάνελ, διαβάστε το ζουμ προσαρμογής από το PdfView.PageWidthZoom[PdfView.PageNumber] και αναθέστε το. Για προσαρμογή στη σελίδα (fit-to-page), χρησιμοποιήστε το PageZoom[PageNumber]. Και τα δύο είναι ιδιότητες πίνακα με δεικτοδότηση από αριθμό σελίδας με βάση το 1, επομένως προστατευτείτε από έναν αριθμό σελίδας μηδέν πριν αποκτήσετε πρόσβαση σε αυτά

Όταν εξάγετε την τρέχουσα σελίδα σε εικόνα, διαβάστε την περιστροφή (rotation) από την προβολή αλλά καλέστε τη RenderPage στο στοιχείο TPdf, όχι στην προβολή. Η μορφή bitmap της TPdf.RenderPage λαμβάνει ρητές διαστάσεις pixel συν μια τιμή TRotation και ένα σύνολο TRenderOptions. Η παραλλαγή της συνάρτησης επιστρέφει ένα TBitmap που ανήκει στον καλούντα (caller-owned), το οποίο ελευθερώνετε (free) μόνοι σας μετά την αποθήκευση:

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 στο πλάτος και το ύψος δίνει πιο ευκρινή έξοδο για έγγραφα με ψιλό κείμενο. Το try/finally γύρω από την απελευθέρωση (free) του bitmap δεν είναι προαιρετικό. Μια ακύρωση στο TSaveDialog εξακολουθεί να χτυπά το μπλοκ finally, και θέλετε το bitmap να απελευθερωθεί ανεξάρτητα από το τι έκανε ο χρήστης

Απαιτήσεις DLL

Το PDFium Component τυλίγει (wraps) την εγγενή βιβλιοθήκη pdfium. Μια διαδικασία φιλοξενίας 32-bit (32-bit host process) χρειάζεται το pdfium32.dll. Ένας κεντρικός υπολογιστής 64-bit χρειάζεται το pdfium64.dll. Οι παραλλαγές με τη μηχανή JavaScript V8 προσθέτουν την κατάληξη v8 και ζυγίζουν περίπου 23-27 MB έναντι των 5-6 MB που είναι οι τυπικές εκδόσεις. Για ένα πρόγραμμα προβολής σύγκρισης που απενεργοποιεί τη συμπλήρωση φόρμας (Pdf.FormFill := False), η τυπική έκδοση χωρίς V8 είναι επαρκής και διατηρεί τη διανομή μικρότερη

Τοποθετήστε το DLL στον ίδιο κατάλογο με το εκτελέσιμο (executable), ή σε οποιονδήποτε κατάλογο στο PATH του συστήματος. Το στοιχείο το φορτώνει κατά παραγγελία (on demand) όταν ενεργοποιείται το πρώτο TPdf, επομένως ένα DLL που λείπει εμφανίζεται σε αυτό το σημείο παρά κατά την εκκίνηση της εφαρμογής. Αν διαθέτετε ένα πρόγραμμα εγκατάστασης (installer), η πιο αξιόπιστη προσέγγιση είναι να αντιγράψετε το DLL στον φάκελο της εφαρμογής κατά την εγκατάσταση αντί να βασίζεστε σε έναν κατάλογο συστήματος που ένας διαχειριστής μπορεί να καθαρίσει αργότερα

Οι εκδόσεις V8 είναι κυρίως χρήσιμες όταν χρειάζεται να αλληλεπιδράσετε με ενέργειες JavaScript του PDF, για παράδειγμα για να ενεργοποιήσετε πεδία υπολογισμού ή να υποβάλετε (submit) χειριστές. Ένα παθητικό πρόγραμμα προβολής σύγκρισης δεν έχει λόγο να εκτελέσει JavaScript. Η ρύθμιση Pdf.FormFill := False πριν το Active := True παρακάμπτει εντελώς το περιβάλλον συμπλήρωσης φόρμας, το οποίο σημαίνει επίσης ότι καμία μηχανή JS δεν αρχικοποιείται ακόμη και αν χρησιμοποιείται η τυπική έκδοση (standard build). Αυτή είναι η σωστή προεπιλογή για ένα πρόγραμμα προβολής μόνο για ανάγνωση (read-only), ανεξάρτητα από την παραλλαγή του DLL που αποστέλλετε

Για περισσότερες λεπτομέρειες σχετικά με το στοιχείο PDFium Component και το πλήρες API του, επισκεφθείτε τη σελίδα προϊόντος Delphi PDFium Component