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

Διαχωρισμός εγγράφων PDF με το PDFium Component στη Delphi

Το PDFium Component σάς δίνει μία μέθοδο για τον διαχωρισμό PDF: την ImportPages. Όλα τα υπόλοιπα, είτε απομονώνετε μία μόνο σελίδα, είτε κόβετε σε αυθαίρετα όρια, είτε ακολουθείτε τη δομή σελιδοδεικτών του ίδιου του εγγράφου, είναι απλώς διαφορετικοί τρόποι να αποφασίσετε ποιοι αριθμοί σελίδων μπαίνουν σε κάθε αρχείο εξόδου. Ο μηχανισμός μένει ο ίδιος. Το να το καταλάβετε νωρίς γλιτώνει πολλές λάθος στροφές

Πώς λειτουργεί ο βρόχος διαχωρισμού

Το μοτίβο είναι το ίδιο ανεξάρτητα από το πώς χωρίζετε το έγγραφο προέλευσης. Δημιουργήστε μια φρέσκια παρουσία TPdf, καλέστε πάνω της τη CreateDocument για να αρχικοποιήσετε ένα κενό PDF στη μνήμη, εισαγάγετε τις σελίδες που θέλετε με την ImportPages, αποθηκεύστε το αποτέλεσμα και έπειτα επαναφέρετε το Active σε False πριν από την επόμενη επανάληψη. Αυτό το τελευταίο βήμα είναι εκείνο που ξεχνούν οι περισσότεροι: η CreateDocument δεν κλείνει σιωπηρά το έγγραφο που βρίσκεται ακόμη στη μνήμη, οπότε πρέπει να αποθηκεύσετε την έξοδό σας και να επαναφέρετε ρητά το Active := False πριν την καλέσετε ξανά· η επαναφορά πρώτα κρατά την κατάσταση καθαρή και σαφώς ορισμένη. Η εξωτερική παρουσία TPdf επαναχρησιμοποιείται σε όλες τις επαναλήψεις, κάτι που κρατά χαμηλά την πίεση δέσμευσης μνήμης σε μεγάλες εργασίες

Διάγραμμα του βρόχου διαχωρισμού του PDFium Component στη Delphi: CreateDocument, ImportPages από την προέλευση μόνο για ανάγνωση, ελεγμένη SaveAs και η επαναφορά του Active πριν από κάθε νέα επανάληψη
Ό,τι κι αν καθορίζει τις ομάδες, ο βρόχος μένει πανομοιότυπος: εισαγάγετε τις σελίδες, αποθηκεύστε με έλεγχο του αποτελέσματος και έπειτα επαναφέρετε το Active ώστε η επόμενη CreateDocument να ξεκινά από καθαρή κατάσταση

Να πώς μοιάζει ο διαχωρισμός σελίδα προς σελίδα, γυμνωμένος ως τα βασικά του:

procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
  I: Integer;
  PdfOut: TPdf;
  OutFile: string;
begin
  PdfOut := TPdf.Create(nil);
  try
    for I := 1 to Source.PageCount do
    begin
      PdfOut.CreateDocument;

      // Το Range είναι συμβολοσειρά αριθμών σελίδων με βάση το 1, θέση εισαγωγής 1 = πρώτη θέση
      if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
        raise Exception.CreateFmt('Failed to import page %d', [I]);

      OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);

      PdfOut.Active := False;   // επαναφορά πριν από την επόμενη CreateDocument
    end;
  finally
    PdfOut.Free;
  end;
end;

Η παράμετρος Range της ImportPages έχει την ίδια μορφή συμβολοσειράς που χρησιμοποιεί εσωτερικά το PDFium: μια λίστα αριθμών σελίδων χωρισμένων με κόμματα ή εύρη οριοθετημένα με παύλα, όλα με βάση το 1. Το '3' εισάγει τη σελίδα 3. Το '1-5' εισάγει τις σελίδες 1 έως 5 με τη σειρά. Το '2,5,8' εισάγει εκείνες τις τρεις σελίδες. Η τρίτη παράμετρος είναι η θέση εισαγωγής με βάση το 1 μέσα στο έγγραφο προορισμού· περνώντας 1 τοποθετείτε πάντα τις εισαγόμενες σελίδες στην αρχή ενός κατά τα άλλα κενού αρχείου, που είναι ακριβώς αυτό που θέλετε εδώ

Διαχωρισμός ανά εύρη σελίδων

Όταν ο καλών δίνει μια λίστα όπως 1-12,13-24,25-36, την αναλύετε σε ζεύγη αρχής και τέλους και τρέχετε τον ίδιο βρόχο, χτίζοντας τη συμβολοσειρά εύρους από κάθε ζεύγος:

procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
  const OutputDir: string);
var
  I: Integer;
  PdfOut: TPdf;
  OutFile: string;
begin
  PdfOut := TPdf.Create(nil);
  try
    for I := 0 to High(RangeList) do
    begin
      PdfOut.CreateDocument;
      if not PdfOut.ImportPages(Source, RangeList[I], 1) then
        raise Exception.Create('Invalid page range: ' + RangeList[I]);
      OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);
      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

Η επικύρωση πριν φτάσετε στην ImportPages έχει σημασία εδώ. Η ImportPages επιστρέφει False όταν ένας αριθμός σελίδας μέσα στη συμβολοσειρά εύρους ξεπερνά το Source.PageCount, αλλά δεν εγείρει εξαίρεση και δεν παράγει μερικό αρχείο εξόδου που να το εντοπίζετε από το όνομα και μόνο. Ελέγξτε την τιμή επιστροφής της SaveAs και καταγράψτε ξεχωριστά τις αποτυχίες· ένα εύρος που παράγει κενό αρχείο εξόδου δεν φαίνεται προφανώς λάθος μέχρι να το ανοίξει κάποιος

Διαχωρισμός στα όρια των σελιδοδεικτών

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

Διάγραμμα που αντιστοιχίζει σελιδοδείκτες ανώτατου επιπέδου PDF σε υπολογισμένα εύρη σελίδων και αρχεία εξόδου κατά τον διαχωρισμό με το PDFium Component στη Delphi, μαζί με έναν σελιδοδείκτη εκτός ορίων που παραλείπεται
Μια ενότητα εκτείνεται από τη σελίδα κάθε σελιδοδείκτη ανώτατου επιπέδου ως μία σελίδα πριν από τον επόμενο σελιδοδείκτη, ενώ οι καταχωρίσεις που δείχνουν πέρα από το τέλος παραλείπονται αντί να παράγουν κενά αρχεία
procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
  Bm: TBookmarks;
  I, StartPage, EndPage: Integer;
  PdfOut: TPdf;
  RangeStr, OutFile, SafeTitle: string;
begin
  Bm := Source.Bookmarks;
  if Length(Bm) = 0 then
    Exit;

  PdfOut := TPdf.Create(nil);
  try
    for I := 0 to High(Bm) do
    begin
      StartPage := Bm[I].PageNumber;
      if I < High(Bm) then
        EndPage := Bm[I + 1].PageNumber - 1
      else
        EndPage := Source.PageCount;

      if (StartPage < 1) or (EndPage < StartPage) then
        Continue;

      RangeStr := Format('%d-%d', [StartPage, EndPage]);

      PdfOut.CreateDocument;
      if not PdfOut.ImportPages(Source, RangeStr, 1) then
      begin
        PdfOut.Active := False;
        Continue;   // παράλειψη μιας κακοσχηματισμένης ενότητας αντί για εγγραφή κενού αρχείου
      end;

      SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
      SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
      OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);

      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

Ένα έγγραφο χωρίς σελιδοδείκτες δεν είναι κατάσταση σφάλματος που αξίζει να παρουσιαστεί ως τέτοια στον χρήστη· σημαίνει απλώς ότι αυτή η λειτουργία διαχωρισμού δεν έχει από πού να ξεκινήσει. Ο έλεγχος Length(Bm) = 0 το χειρίζεται σιωπηλά. Αυτό που αξίζει να αναδειχθεί είναι όταν ο αριθμός σελίδας ενός σελιδοδείκτη βρίσκεται εκτός του εύρους του εγγράφου, κάτι που συμβαίνει σε κακοσχηματισμένα αρχεία όπου η διάρθρωση δεν ενημερώθηκε ποτέ μετά τη διαγραφή σελίδων. Ο έλεγχος ορίων στα StartPage και EndPage παραλείπει αυτές τις καταχωρίσεις αντί να περάσει σκουπίδια ως εύρος στην ImportPages

Ονοματοδοσία αρχείων εξόδου και η επαναφορά του Active

Η ασφάλεια των ονομάτων αρχείων που προκύπτουν από σελιδοδείκτες χρειάζεται ρητή προσοχή. Οι τίτλοι σελιδοδεικτών μπορεί να περιέχουν χαρακτήρες έγκυρους μέσα σε συμβολοσειρά PDF αλλά όχι σε διαδρομή αρχείου. Κατ' ελάχιστον, αντικαταστήστε την κάθετο, την ανάστροφη κάθετο και την άνω και κάτω τελεία πριν χτίσετε τη διαδρομή εξόδου. Στα Windows απαγορεύονται επίσης τα *, ?, ", <, > και |· ένας απλός βρόχος πάνω σε ένα σταθερό σύνολο τα καλύπτει χωρίς να φέρετε μέσα regex

Η γραμμή Active := False στο τέλος κάθε επανάληψης αξίζει έμφαση, επειδή είναι η μόνη μη προφανής απαίτηση του μοτίβου. Η CreateDocument δεν κλείνει σιωπηρά ό,τι είναι ανοιχτό. Αν το Active είναι ακόμη True όταν τρέξει ξανά η CreateDocument, το έγγραφο που βρίσκεται ακόμη στη μνήμη δεν έκλεισε ούτε αποθηκεύτηκε ποτέ σωστά, και δεν μπορείτε να βασιστείτε σε σαφώς ορισμένη συμπεριφορά σε αυτή την κατάσταση, οπότε αποθηκεύστε και επαναφέρετε ρητά πριν ξεκινήσετε το επόμενο έγγραφο. Σκεφτείτε το ως το ταίρι του try/finally: το μπλοκ finally απελευθερώνει το εξωτερικό αντικείμενο, ενώ το Active := False επαναφέρει την κατάσταση του εσωτερικού εγγράφου ανάμεσα στις επαναλήψεις του βρόχου

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

Ένα ακόμη σχετικά με τη SaveAs: επιστρέφει Boolean. Ένας κατάλογος εξόδου που δεν υπάρχει, μια διαδρομή με χαρακτήρες που απορρίπτει το λειτουργικό σύστημα ή ένας γεμάτος δίσκος θα κάνουν όλα τη SaveAs να επιστρέψει False χωρίς να εγείρει εξαίρεση. Σε μια μαζική εργασία που διαχωρίζει ένα έγγραφο 200 σελίδων σε 200 μονοσέλιδα αρχεία, μια σιωπηλή αποτυχία στη σελίδα 147 προσπερνιέται εύκολα. Ελέγχετε την τιμή επιστροφής σε κάθε κλήση και μετρήστε τις επιτυχίες έναντι του αναμενόμενου συνόλου όταν τελειώσει ο βρόχος

Οι μέθοδοι ImportPages και CreateDocument που παρουσιάζονται εδώ αποτελούν μέρος του PDFium Component για Delphi και C++Builder