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

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

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

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

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

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

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

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

// Do not do this. Each CopyPageRanges call rebuilds the import map,
// so anything the two sources share internally is imported once per
// round instead of once per source.
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 σελίδων ο πολλαπλασιαστής δεν είναι σφάλμα στρογγυλοποίησης, είναι η διαφορά ανάμεσα σε ένα αρχείο που χωρά στον προϋπολογισμό διατήρησης και σε ένα που δεν χωρά

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

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

Η υλοποίηση διατηρεί έναν πίνακα 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);
    // fronts 1..12 in order, backs scanned in reverse: 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
  // Nothing was appended and the target is byte-identical to before.
  // 412 is the copy failure; 0 means the arguments were rejected
  // during validation, before any page was touched.
  Log(Format('collate rejected, LastErrorCode=%d', [PDF.LastErrorCode]));

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

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