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

Συρραφή Duplex Scan σε Delphi: PDF Interleave

Η CollateDocumentsEx στη βιβλιοθήκη PDF PDF Library for Delphi για Delphi συγχωνεύει πολλά ανοιχτά έγγραφα σε ένα ενιαίο έγγραφο με εναλλαγή σελίδων. Προσαρτά GroupSize σελίδες από κάθε πηγή ανά γύρο, δέχεται μια λίστα εύρους σελίδων ανά πηγή, και αντιμετωπίζει ένα φθίνον εύρος όπως 3-1 ως αντιστροφή αυτής της πηγής· μια κλήση μετατρέπει μια μπροστινή στοίβα και μια ανεστραμμένη πίσω στοίβα σε σειρά ανάγνωσης

Το σενάριο πίσω από αυτό το API είναι απλό και εξαιρετικά συνηθισμένο. Ένας scanner τροφοδοσίας φύλλων με μονόπλευρη διαδρομή περνά όλη τη στοίβα ανάποδα, και μετά ο χειριστής αναποδογυρίζει τη στοίβα και την περνά ξανά· καταλήγετε με δύο PDF: μπροστινές σελίδες με σειρά, πίσω σελίδες με αντίστροφη σειρά. Το αρχείο που θέλει ο χρήστης είναι ένα μόνο, σελίδα 1 μπρος, σελίδα 1 πίσω, σελίδα 2 μπρος, και ούτω καθεξής. Αυτό το άρθρο αφορά το πρόβλημα της σειράς και την παγίδα διπλασιασμού πόρων που κρύβεται από κάτω· αν σας ενδιαφέρει η καθαρή απόδοση συνένωσης, δείτε γρήγορη συγχώνευση PDF με μετατόπιση αναφορών σε επίπεδο byte· αν οι είσοδοι είναι υπερβολικά μεγάλες για να χωρέσουν στη μνήμη, δείτε συγχώνευση και διαχωρισμός PDF πολλών gigabyte με άμεση πρόσβαση

Ο scanner παράγει δύο στοίβες, μία από αυτές ανάποδα

Η συρραφή δεν είναι συγχώνευση. Μια συγχώνευση συνενώνει εύρη σελίδων· μια συρραφή τα εναλλάσσει, και το μοτίβο εναλλαγής είναι ιδιότητα της φυσικής συσκευής που παρήγαγε την είσοδο. Αν το μοτίβο είναι λάθος, το αρχείο δεν είναι ελαφρώς λάθος, είναι μη αναγνώσιμο: κάθε δεύτερη σελίδα ανήκει σε διαφορετικό φύλλο. Τρεις μεταβλητές περιγράφουν σχεδόν κάθε πραγματική περίπτωση: πόσες πηγές συμμετέχουν στην εναλλαγή, πόσες σελίδες προέρχονται από κάθε πηγή ανά γύρο, και αν κάποια πηγή χρειάζεται να διαβαστεί ανάποδα. Η CollateDocuments καλύπτει τις δύο πρώτες με έναν απλό πίνακα χειριστών εγγράφων και έναν ακέραιο GroupSize. Η CollateDocumentsEx προσθέτει την τρίτη δεχόμενη μια λίστα ευρών σελίδων χωρισμένη με ερωτηματικό, ένα τμήμα ανά πηγή, όπου ένα κενό τμήμα σημαίνει όλες τις σελίδες αυτής της πηγής και ένα φθίνον εύρος την αντιστρέφει. Και οι δύο συναρτήσεις προσαρτούν στο τέλος του τρέχοντος επιλεγμένου εγγράφου και επιστρέφουν 1 σε επιτυχία, 0 σε οποιαδήποτε απόρριψη

Γιατί η αφελής συρραφή πολλαπλασιάζει το μέγεθος του αρχείου;

Επειδή ο χάρτης εισαγωγής που αντιστοιχίζει αριθμούς αντικειμένων πηγής σε αριθμούς αντικειμένων προορισμού ανακατασκευάζεται σε κάθε κλήση αντιγραφής, και οτιδήποτε είναι προσβάσιμο από περισσότερα από ένα τμήματα εισάγεται μία φορά ανά τμήμα. Μέσα στο PDF Library for Delphi, η TPDFDocument.CopyPagesFromDoc επαναφέρει τη NewIndObjList στην αρχή κάθε κλήσης. Αυτή η λίστα είναι η μόνη μνήμη που έχει ο αντιγραφέας για ό,τι έχει ήδη μεταφέρει. Καλέστε τη μία φορά με εύρος δέκα σελίδων και μια γραμματοσειρά κοινή σε όλες τις δέκα σελίδες ενσωματώνεται μία φορά. Καλέστε τη δέκα φορές με μία σελίδα κάθε φορά και η ίδια γραμματοσειρά ενσωματώνεται δέκα φορές. Αυτό έχει πολύ μεγαλύτερη σημασία για scans παρά για έγγραφα κειμένου, επειδή μια σαρωμένη σελίδα είναι ένα μόνο μεγάλο image XObject και τα κοινά αντικείμενα είναι αυτά με πραγματικό βάρος: ένα ενσωματωμένο προφίλ ICC, μια κοινή αλυσίδα /DecodeParms, μια σφραγίδα ή υδατογράφημα form XObject που εφαρμόζεται σε κάθε φύλλο, η γραμματοσειρά του στρώματος κειμένου OCR. Ο προφανής τρόπος να γράψετε μια περιτροπική συρραφή είναι ένας βρόχος πάνω από τους γύρους, και αυτός ο βρόχος είναι ακριβώς η παθολογική περίπτωση

// Μην το κάνετε αυτό. Κάθε κλήση CopyPageRanges ανακατασκευάζει τον χάρτη εισαγωγής,
// οπότε οτιδήποτε μοιράζονται εσωτερικά οι δύο πηγές εισάγεται μία φορά ανά
// γύρο αντί για μία φορά ανά πηγή.
var
  RoundIndex: Integer;
begin
  for RoundIndex := 1 to 12 do
  begin
    PDF.CopyPageRanges(Fronts, IntToStr(RoundIndex));
    PDF.CopyPageRanges(Backs, IntToStr(13 - RoundIndex));
  end;
end;

Δώδεκα γύροι, δύο πηγές, είκοσι τέσσερις χάρτες εισαγωγής. Τίποτα δεν προειδοποιεί. Η σειρά σελίδων είναι σωστή, κάθε σελίδα αποδίδεται κανονικά, και το μόνο σύμπτωμα είναι ένα αρχείο πολλές φορές μεγαλύτερο από το άθροισμα των εισόδων του. Σε μια παρτίδα 300 σελίδων ο πολλαπλασιαστής δεν είναι σφάλμα στρογγυλοποίησης, είναι η διαφορά ανάμεσα σε ένα αρχείο που χωρά στον προϋπολογισμό διατήρησης και σε ένα που δεν χωρά

Χάρτης συγχώνευσης σάρωσης διπλής όψης για το PDF Library for Delphi: μια στοίβα μπροστινών όψεων αποθηκεύει τις σελίδες 1 έως 12 και συναντά μια στοίβα πίσω όψεων που αποτυπώθηκε αντεστραμμένα, και το CollateDocumentsEx με GroupSize 1 τις εναλλάσσει σε σειρά ανάγνωσης F1 B12 F2 B11 έως τη σελίδα 12, ενώ η αφελής αντιγραφή ανά γύρο επανεισάγει τους κοινόχρηστους πόρους σε κάθε γύρο
Οι μπροστινές σελίδες αποθηκευμένες από 1 έως 12 συναντούν τις πίσω σελίδες που καταγράφηκαν από 12 έως 1, και μία κλήση CollateDocumentsEx τις παρεμβάλει στην πραγματική σειρά ανάγνωσης — η υλοποίησή του ως βρόχου αντιγραφής ανά γύρο θα ενσωμάτωνε ξανά κοινές γραμματοσειρές και εικόνες σε κάθε γύρο

Εισαγωγή μία φορά, μετά αναδιάταξη του δέντρου σελίδων

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

Οι μετακινήσεις σελίδων είναι δωρεάν με την έννοια που έχει σημασία εδώ. Το ISO 32000-1 §7.7.3 ορίζει το δέντρο σελίδων ως μια ισορροπημένη δομή λεξικών κόμβων των οποίων οι πίνακες /Kids περιέχουν έμμεσες αναφορές, με το /Count να μεταφέρει το σύνολο φύλλων σε κάθε κόμβο. Η μετατόπιση μιας σελίδας σημαίνει αφαίρεση μιας έμμεσης αναφοράς από έναν πίνακα /Kids, εισαγωγή της σε άλλον, προσαρμογή και των δύο τιμών /Count, και επαναπροσανατολισμό του /Parent της σελίδας. Κανένα ρεύμα περιεχομένου δεν αγγίζεται, κανένας πόρος δεν διπλασιάζεται, κανένα αντικείμενο δεν δημιουργείται. Το αντικείμενο σελίδας διατηρεί τον αριθμό αντικειμένου του, κάτι που εξηγεί και γιατί οι αριθμοί αντικειμένων παραμένουν σταθεροί με τον τρόπο που παραμένουν στην αντικατάσταση σελίδων με διατήρηση αριθμών αντικειμένων. Υπάρχει μια ακόμη λεπτομέρεια που μια αφελής μετακίνηση σελίδας κάνει λάθος και η MovePage όχι. Το ISO 32000-1 §7.7.3.4 επιτρέπει τα /Resources, /MediaBox, /CropBox και /Rotate να κληρονομούνται από έναν πρόγονο κόμβο αντί να δηλώνονται στη σελίδα. Μια σελίδα που κληρονομεί τους πόρους της από τον κόμβο A και μετά μετακινείται κάτω από τον κόμβο B κληρονομεί σιωπηλά κάτι διαφορετικό, ή τίποτα. Η MovePage επομένως επιλύει την κληρονομημένη τιμή και τη γράφει στο λεξικό της σελίδας πριν από τη μετατόπιση, ώστε η σελίδα να μεταφέρει τα δικά της χαρακτηριστικά μέσα από τη μετακίνηση

Τι κάνει στην πραγματικότητα το πέρασμα αναδιάταξης;

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

Ροή του CollateDocumentsEx του PDF Library for Delphi: τέσσερις πύλες ορισμάτων επικυρώνονται πριν αλλάξει οτιδήποτε, ένα πέρασμα εισαγωγής ανά πηγή που κρατά τις κοινές γραμματοσειρές σε ένα αντίγραφο, προσθήκη με σειρά περιστροφής μέσω GroupSize, αναδιάταξη δέντρου σελίδων μέσω MovePage, δημόσια ανάκτηση DeletePages για αποτυχία μέσα στην αντιγραφή, και επιστροφή 1 με σταθερούς αριθμούς αντικειμένων σελίδων
Η CollateDocumentsEx διαχωρίζει την αντιγραφή από την ταξινόμηση: κάθε πηγή εισάγεται ακριβώς μία φορά ώστε οι κοινοί πόροι να παραμένουν ενιαία αντίγραφα, και στη συνέχεια η MovePage αναδιατάσσει το προσαρτημένο τμήμα, ενώ οι απορρίψεις ορισμάτων επιστρέφουν νωρίς και οι αποτυχίες μέσο αντιγραφής επαναφέρονται μέσω της δημόσιας DeletePages

Η υλοποίηση διατηρεί έναν πίνακα Current που μοντελοποιεί πού βρίσκεται αυτή τη στιγμή κάθε προσαρτημένη σελίδα, σαρώνει προς τα εμπρός από τη θέση K για τη σελίδα που ανήκει στο K, εκδίδει τη μετακίνηση, και μετά ολισθαίνει τις καταχωρίσεις του πίνακα ώστε να καθρεφτίζουν αυτό που έκανε η μετακίνηση στο δέντρο. Είναι O(n τετράγωνο) σε πράξεις πίνακα και μηδέν σε αντίγραφα αντικειμένων, που είναι η σωστή ανταλλαγή για αυτό το φορτίο εργασίας: μια συρραφή 500 σελίδων είναι ένα τέταρτο εκατομμυρίου ανακατατάξεων ακεραίων και ούτε ένα byte δεδομένων εικόνας διπλασιασμένο. Τα φθίνοντα εύρη και οι επαναλαμβανόμενες σελίδες δεν χρειάζονται ειδικό χειρισμό σε αυτό το πέρασμα επειδή η PLParsePageRangeList καλείται με απενεργοποιημένη την ταξινόμηση και επιτρεπόμενα τα διπλότυπα, οπότε η ζητούμενη σειρά επιβιώνει άθικτη από την ανάλυση

Ανεστραμμένα εύρη και η συγχώνευση duplex με μία κλήση

Με την αναστροφή εκφρασμένη ως εύρος, η περίπτωση διπλού περάσματος του flatbed συμπτύσσεται σε μία μόνο κλήση. Οι μπροστινές σελίδες θέλουν τη φυσική τους σειρά και οι πίσω σελίδες θέλουν 12-1, και το κενό πρώτο τμήμα πριν το ερωτηματικό λέει ότι η πρώτη πηγή συνεισφέρει όλες τις σελίδες της

var
  PDF: TPDFlib;
  Target, Fronts, Backs: Integer;
begin
  PDF := TPDFlib.Create;
  try
    Target := PDF.NewDocument;
    if PDF.LoadFromFile('fronts.pdf', '') <> 1 then
      Exit;
    Fronts := PDF.SelectedDocument;
    if PDF.LoadFromFile('backs.pdf', '') <> 1 then
      Exit;
    Backs := PDF.SelectedDocument;
    PDF.SelectDocument(Target);
    // μπροστινές 1..12 με τη σειρά, πίσω σελίδες σε αντίστροφη σάρωση: F1 B12 F2 B11 ...
    if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 1 then
      PDF.SaveToFile('duplex.pdf');
  finally
    PDF.Free;
  end;
end;

Δύο συμπεριφορές σε αυτό το απόσπασμα αξίζει να δηλωθούν ρητά. Οι συρραμμένες σελίδες προσαρτώνται στο επιλεγμένο έγγραφο, οπότε ένα έγγραφο που δημιουργήθηκε με NewDocument συνεισφέρει την αρχική κενή σελίδα του πριν από αυτές και πρέπει να τη διαγράψετε αν δεν τη θέλετε. Και οι πηγές μπορεί να είναι άνισες: με GroupSize 2 πάνω σε μια πηγή τριών σελίδων και μια πηγή πέντε σελίδων, οι γύροι βγαίνουν A1 A2 B1 B2, μετά A3 B3 B4 όταν η A έχει σχεδόν εξαντληθεί, μετά B5 μόνη της, επειδή μια εξαντλημένη πηγή απλώς παραλείπεται αντί να συμπληρωθεί

Αναίρεση, πεδία φόρμας, και τι δεν ακολουθεί

Κάθε όρισμα επικυρώνεται πριν αγγιχτεί ο προορισμός. Ένας χειριστής εγγράφου που λείπει, το επιλεγμένο έγγραφο αναφερόμενο ως δική του πηγή, ένα GroupSize κάτω από ένα, ένας αριθμός τμημάτων που δεν ταιριάζει με τον αριθμό πηγών, ένα εύρος που ονομάζει σελίδα που η πηγή δεν έχει: όλα αυτά επιστρέφουν 0 με τον προορισμό αμετάβλητο. Η αποτυχία κατά την αντιγραφή είναι η πιο δύσκολη περίπτωση, και αντιμετωπίζεται μέσω της δημόσιας DeletePages αντί της ακατέργαστης PageTree.DeletePages. Ο λόγος είναι συγκεκριμένος. Η αντιγραφή εκτελείται με ενεργοποιημένο το MergeFormData, οπότε τα πεδία φόρμας της πηγής έχουν ήδη προσαρτηθεί στον πίνακα /AcroForm /Fields του προορισμού μέχρι να αποτύχει μια μεταγενέστερη πηγή. Η διαγραφή των σελίδων στο επίπεδο δέντρου σελίδων θα αφαιρούσε τις σελίδες widget και θα άφηνε αυτές τις αναφορές πεδίων κρεμασμένες· η δημόσια διαδρομή αποσυνδέει το πεδίο, το περίγραμμα και τις αναφορές νημάτων άρθρων μαζί με τις σελίδες

if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 0 then
  // Δεν προσαρτήθηκε τίποτα και ο προορισμός είναι byte-προς-byte ίδιος με πριν.
  // Το 412 είναι αποτυχία αντιγραφής· το 0 σημαίνει ότι τα ορίσματα απορρίφθηκαν
  // κατά την επικύρωση, πριν αγγιχτεί καμία σελίδα.
  Log(Format('collate rejected, LastErrorCode=%d', [PDF.LastErrorCode]));

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

Το PDF Library for Delphi διαθέτει τις συναρτήσεις συρραφής μαζί με το υπόλοιπο επιφάνειας συναρμολόγησης σελίδων, ώστε η ροή εργασίας του scanner, η εξαγωγή βάσει εύρους και οι διαδρομές μεγάλων αρχείων να βρίσκονται όλες πίσω από ένα component σε Delphi και C++Builder. Η πλήρης αναφορά API και μια δοκιμαστική έκδοση βρίσκονται στη σελίδα προϊόντος losLab Delphi PDF library