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

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

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

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

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

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

Διάγραμμα διάταξης φόρμας ενός προγράμματος σύγκρισης PDF δίπλα-δίπλα στη Delphi φτιαγμένου με το PDFium Component, που δείχνει μια γραμμή εργαλείων, τρία scroll box με πάνελ TPdfView και διαχωριστές σε λειτουργία δύο και τριών προβολών
Κάθε πάνελ είναι ένα scroll box με ένα TPdfView μέσα, και η εναλλαγή ανάμεσα σε δύο και τρεις προβολές είναι απλώς ένα διαφορετικό σύνολο αναθέσεων πλάτους
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;
    // Εφαρμόστε το ίδιο (ClientHeight - ύψος γραμμής εργαλείων) και στις τρεις τιμές Height
  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 να παλεύει με τις αναθέσεις σας. Επαναφέρετε την ορατότητα των διαχωριστών μετά την τοποθέτηση, αν θέλετε αλλαγή μεγέθους με σύρσιμο στη λειτουργία δύο προβολών

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

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

Κάθε ζεύγος πάνελ χρειάζεται τη δική του διαδικασία ανοίγματος. Το μοτίβο είναι σύντομο: απενεργοποιήστε το στοιχείο, ορίστε το όνομα αρχείου, ενεργοποιήστε το και έπειτα ελέγξτε το Active· αν έμεινε False, ζητήστε κωδικό και ξαναδοκιμάστε. Σημειώστε ότι το TPdfView.Active είναι αυτό που ελέγχει την απόδοση, ενώ το TPdf.Active είναι αυτό που πραγματικά ανοίγει το αρχείο· είναι ανεξάρτητα. Το να ορίσετε PdfView.Active := True όταν το συνδεδεμένο του TPdf δεν είναι ακόμη ενεργό είναι ακίνδυνο αλλά δεν εμφανίζει τίποτα

Διάγραμμα ροής για το άνοιγμα ενός εγγράφου PDF με το PDFium Component στη Delphi, που δείχνει τον σιωπηλό έλεγχο του Active, μία επανάληψη με κωδικό και ένα παράθυρο σφάλματος για κατεστραμμένα ή προστατευμένα με κωδικό αρχεία
Μια αποτυχημένη φόρτωση αφήνει το Active σε False χωρίς να εγείρει εξαίρεση, οπότε η ροή το ελέγχει, ξαναδοκιμάζει μία φορά με κωδικό και τελικά αναφέρει το πρόβλημα αντί να δείχνει ένα κενό πάνελ
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;

  // Οι αποτυχίες φόρτωσης είναι σιωπηλές: το Active μένει False αντί να εγερθεί εξαίρεση.
  if not PdfComponent.Active then
  begin
    // Πιθανότατα αρχείο προστατευμένο με κωδικό, δώστε στον χρήστη μία επανάληψη.
    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;

Ελέγχετε πάντα το PdfComponent.Active μετά την ανάθεση· ένα κατεστραμμένο αρχείο ή λάθος κωδικός κάνει τη φόρτωση να αποτύχει σιωπηλά, χωρίς να εγείρει εξαίρεση στην προεπιλεγμένη διαδρομή. Το να ορίσετε ρητά PdfViewComponent.PageNumber := 1 μετά από επιτυχές άνοιγμα αποτρέπει έναν ξεπερασμένο αριθμό σελίδας από το προηγούμενο έγγραφο

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

Παρακολούθηση του ενεργού πάνελ

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

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

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;

Συγχρονισμένη πλοήγηση σελίδων

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

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

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

Ένα πράγμα που πρέπει να προσέξετε: η ρύθμιση του PdfView.PageNumber μέσα από τον κώδικα, μέσα στον χειριστή συγχρονισμού, θα πυροδοτήσει και η ίδια το συμβάν αλλαγής σε εκείνη την προβολή. Προφυλαχθείτε από την άπειρη αναδρομή με μια δυαδική σημαία που θέτετε πριν από την ανάθεση και καθαρίζετε αμέσως μετά. Η σημαία είναι ανά φόρμα και όχι ανά προβολή, επειδή και οι τρεις προβολές μοιράζονται τον ίδιο χειριστή

Διάγραμμα συγχρονισμένης πλοήγησης σελίδων σε πρόγραμμα σύγκρισης PDF στη Delphi με το PDFium Component, με το πλαίσιο ελέγχου συγχρονισμού, έναν έλεγχο πλήθους σελίδων ανά προβολή-στόχο και μια σημαία προφύλαξης από αναδρομή
Ο αριθμός σελίδας ταξιδεύει από την προβολή προέλευσης σε κάθε άλλη προβολή μόνο όταν ο συγχρονισμός είναι ενεργός και κάθε προβολή-στόχος περιέχει πραγματικά εκείνη τη σελίδα

Ζουμ ανά πάνελ

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

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

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 γύρω από την απελευθέρωση του bitmap δεν είναι προαιρετικό· μια ακύρωση του TSaveDialog περνά και αυτή από το μπλοκ finally, και θέλετε το bitmap να απελευθερώνεται ανεξάρτητα από το τι έκανε ο χρήστης

Απαιτήσεις DLL

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

Τοποθετήστε το DLL στον ίδιο κατάλογο με το εκτελέσιμο ή σε οποιονδήποτε κατάλογο βρίσκεται στο PATH του συστήματος. Το στοιχείο το φορτώνει κατ' απαίτηση όταν ενεργοποιείται το πρώτο TPdf, οπότε ένα DLL που λείπει αναδεικνύεται εκείνη τη στιγμή και όχι στην εκκίνηση της εφαρμογής. Αν διανέμετε πρόγραμμα εγκατάστασης, ο πιο αξιόπιστος τρόπος είναι να αντιγράφετε το DLL στον φάκελο της εφαρμογής κατά την εγκατάσταση αντί να βασίζεστε σε κατάλογο συστήματος που κάποιος διαχειριστής μπορεί αργότερα να καθαρίσει

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

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