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

Αντικατάσταση Σελίδων PDF σε Delphi Χωρίς Bookmarks

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

Γιατί σπάνε οι σελιδοδείκτες μετά την αντικατάσταση μιας σελίδας PDF;

Οι σελιδοδείκτες σπάνε επειδή ένας προορισμός PDF ονομάζει μια σελίδα με έμμεση αναφορά αντικειμένου, όχι με αριθμό σελίδας. Το ISO 32000-1 §12.3.2.2 ορίζει έναν ρητό προορισμό ως πίνακα του οποίου το πρώτο στοιχείο είναι έμμεση αναφορά στο αντικείμενο σελίδας. Αν διαγραφεί εκείνο το αντικείμενο και προσαρτηθεί μια αντικατάσταση, η αναφορά μένει εκκρεμής: οι περισσότεροι viewers αντιδρούν ρίχνοντας τον αναγνώστη στη σελίδα 1, που είναι ακριβώς το σύμπτωμα που αναφέρουν οι χρήστες μετά από μια αντικατάσταση τύπου διαγραφή-και-μετά-εισαγωγή. Το page tree φαίνεται τέλειο, ο αριθμός σελίδων είναι σωστός, το rendering είναι σωστό, και ολόκληρο το επίπεδο πλοήγησης είναι σιωπηλά λάθος

Οι επώνυμοι προορισμοί (named destinations) δεν σώζουν την κατάσταση ούτε αυτοί. Η §12.3.2.3 δρομολογεί ένα όνομα μέσω του name tree /Dests στον document catalogue, αλλά το φύλλο στο οποίο επιλύεται εκείνο το όνομα εξακολουθεί να είναι ένας ρητός πίνακας προορισμού που κρατά την ίδια αναφορά σελίδας. Η ονομασία προσθέτει ένα επίπεδο έμμεσης παραπομπής πάνω από την αναφορά σελίδας, όχι γύρω από αυτήν. Η ίδια λογική καλύπτει το υπόλοιπο του διαδραστικού επιπέδου που περιγράφεται στην §12.5: μια annotation συνδέσμου φέρει ένα /Dest ή μια ενέργεια GoTo /A της οποίας το /D είναι εκείνος ο πίνακας, κάθε annotation μπορεί να φέρει μια εγγραφή /P που είναι έμμεση αναφορά στη σελίδα της, και ένα widget πεδίου φόρμας είναι annotation στην ίδια ακριβώς βάση. Μια αφελής εναλλαγή σελίδας αποσυνδέει τέσσερα υποσυστήματα ταυτόχρονα, και για να τα δει κανείς απαριθμημένα σε ένα πραγματικό αρχείο, το ίδιο γράφημα αντικειμένων είναι αυτό που διατρέχει η εσωτερική εξέταση outline και annotation

Διάγραμμα σύγκρισης του PDF Library for Delphi για προορισμό σελιδοδείκτη ονομασμένο με έμμεση αναφορά που επιβιώνει από αντικατάσταση σελίδας επί τόπου αλλά μένει κρεμάμενος μετά από ανταλλαγή διαγραφής και προσάρτησης
Οι προορισμοί δένουν σελιδοδείκτες, συνδέσμους και widgets με αριθμό αντικειμένου σελίδας, οπότε η μεταμόρφωση εκείνου του αντικειμένου επιτόπου κρατά την πλοήγηση ζωντανή, ενώ η διαγραφή-και-εισαγωγή ρίχνει τους αναγνώστες στη σελίδα 1

Ποιες εγγραφές σελίδας μεταφέρουν ταυτότητα και ποιες εμφάνιση

Ένα dictionary σελίδας αναμειγνύει δύο είδη εγγραφών, και μια αντικατάσταση επιτόπου πετυχαίνει ακριβώς όταν αυτά διαχωριστούν. Η πλευρά της εμφάνισης είναι πεπερασμένη και απαριθμήσιμη: /Contents, /Resources, τα πέντε page boxes /MediaBox, /CropBox, /BleedBox, /TrimBox και /ArtBox, συν /Rotate, /Group, /UserUnit και /BoxColorInfo. Αυτές οι έντεκα εγγραφές αποφασίζουν όλα όσα παράγει ένας rasteriser για τη σελίδα, και τίποτα άλλο στο αρχείο δεν τις δείχνει με όνομα

Η πλευρά της ταυτότητας είναι αυτό στο οποίο έχει δεσμευτεί το υπόλοιπο του εγγράφου: ο αριθμός αντικειμένου σελίδας και η generation, ο σύνδεσμος /Parent προς τα πίσω στο page tree, και το /Annots. Η PDF Library for Delphi κρατά κάθε ένα από αυτά ανέγγιχτο. Η ReplacePageRanges εξαλείφει τις έντεκα οπτικές εγγραφές από το dictionary της σελίδας-στόχου και τις ξαναπροσθέτει από την εισαγόμενη σελίδα-πηγή, οπότε το αντικείμενο σελίδας-στόχου τροποποιείται επιτόπου αντί να αντικατασταθεί. Η δομή page tree που απαιτεί η §7.7.3 παραμένει επίσης byte-προς-byte ταυτόσημη σε σχήμα: η σειρά /Kids, το /Count, και κάθε επιζών /Parent είναι τα ίδια πριν και μετά, επειδή κανένας κόμβος δεν αποσυνδέθηκε ποτέ

Πώς αντικαθιστά η PDF Library for Delphi μια σελίδα χωρίς επαναρίθμηση αντικειμένων;

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

var
  Lib: TPDFlib;
  TargetDoc, SourceDoc: Integer;
begin
  Lib := TPDFlib.Create;
  try
    // Το έγγραφο του οποίου οι σελιδοδείκτες και οι σύνδεσμοι πρέπει να επιβιώσουν
    if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
      Exit;
    TargetDoc := Lib.SelectedDocument;

    // Η αναθεωρημένη σελίδα ρήτρας, όπως αποδόθηκε από ό,τι την παρήγαγε
    if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
      Exit;
    SourceDoc := Lib.SelectedDocument;

    Lib.SelectDocument(TargetDoc);
    // Η σελίδα πηγής 1 αντικαθιστά τα οπτικά της σελίδας στόχου 3.
    // Ο αριθμός σελίδων, ο αριθμός αντικειμένου της σελίδας 3, οι σελιδοδείκτες και οι σημειώσεις διατηρούνται.
    if Lib.ReplacePageRanges(SourceDoc, 3, '1', 0) = 1 then
      Lib.SaveToFile('contract-final.pdf');
  finally
    Lib.Free;
  end;
end;

Εσωτερικά οι σελίδες-πηγή δεν μπορούν απλώς να διαβαστούν πέρα από τα όρια εγγράφου, επειδή κάθε έμμεση αναφορά μέσα τους ανήκει στην αρίθμηση αντικειμένων της πηγής. Έτσι το εύρος πηγής εισάγεται πρώτα με τον συνηθισμένο τρόπο, ως προσωρινές σελίδες προσαρτημένες μετά την τελευταία πραγματική σελίδα, κάτι που εκτελεί την πλήρη επαναχαρτογράφηση του γραφήματος αντικειμένων: content streams, γραμματοσειρές, XObjects, shadings και χρωματικοί χώροι επαναριθμούνται όλα στο έγγραφο-στόχο. Μόνο τότε αντιγράφονται οι έντεκα οπτικές εγγραφές από κάθε προσωρινή σελίδα στη σελίδα-στόχο της, και μόνο τότε οι προσωρινές σελίδες αποσυνδέονται από το page tree. Η δουλειά επαναχαρτογράφησης συμβαίνει εκεί όπου είναι φθηνή και ασφαλής, και η καταστροφική επεξεργασία περιορίζεται σε μια εναλλαγή σε επίπεδο dictionary πάνω σε σελίδες που ήδη υπάρχουν

Η διαδρομή διαγραφής που θα κατέστρεφε αυτό που μόλις μεταφέρθηκε

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

Η διόρθωση είναι μια λειτουργία preserve-referenced-objects στην εσωτερική διαδρομή διαγραφής. Όταν είναι ενεργή, η διαγραφή παρακάμπτει τόσο τη σάρωση μη-κοινόχρηστων πόρων όσο και τον καθαρισμό content stream, και δεν κάνει τίποτα άλλο εκτός από την αποσύνδεση των σελίδων από το page tree και τη διόρθωση της λογιστικής του δέντρου. Τα μεταφερμένα αντικείμενα επιζούν με νέο ιδιοκτήτη, και η ιδιοκτησία αντικειμένων μετά την πράξη είναι αυτό που θα ζωγράφιζε κανείς σε έναν πίνακα: ένα content stream, μία σελίδα-ιδιοκτήτης, ένας αριθμός αντικειμένου που ποτέ δεν μετακινήθηκε. Οι σχετικοί κανόνες κύκλου ζωής για τη δημιουργία, διαγραφή και αναδιάταξη σελίδων καλύπτονται ξεχωριστά στις σημειώσεις για τις πράξεις κύκλου ζωής εγγράφου και σελίδας

PDF Library for Delphi: Ανατομία λεξικού σελίδας που διαχωρίζει τις καταχωρήσεις ταυτότητας στις οποίες βασίζεται το αρχείο από τις έντεκα οπτικές καταχωρήσεις που ανταλλάσσει το ReplacePageRanges από μια εισαγόμενη σελίδα πηγής
Η ReplacePageRanges εξαφανίζει τα έντεκα οπτικά κλειδιά και τα ξαναπροσθέτει από την εισαγωγή ενώ ο αριθμός αντικειμένου, το generation, το /Parent και το /Annots παραμένουν ακριβώς όπως ήταν

Σειρά, διπλότυπα, και αποτυχία όλα-ή-τίποτα

Η σημαία επιλογών επιλέγει πώς ερμηνεύεται το εύρος πηγής. Το 0 ταξινομεί τους αναλυμένους αριθμούς σελίδων και αφαιρεί τα διπλότυπα, κάτι που είναι η λογική προεπιλογή όταν ο καλών περνά κάτι όπως '4-6,2' και απλώς σημαίνει εκείνες τις τέσσερις σελίδες. Το 1 διατηρεί τη σειρά που γράφτηκε και επιτρέπει μια σελίδα να επαναλαμβάνεται, οπότε το '2,1,2' σημαίνει γνησίως τρεις αντικαταστάσεις που προέρχονται από δύο σελίδες-πηγή. Η επικύρωση τρέχει πρώτη και τρέχει πλήρως: η σύνταξη εύρους, κάθε αριθμός σελίδας έναντι του αριθμού σελίδων πηγής, η ίδια η τιμή επιλογής, και η χωρητικότητα του στόχου ελέγχονται όλα πριν δημιουργηθεί ένα μόνο αντικείμενο. Μια απορριφθείσα κλήση θέτει το LastErrorCode σε 412, επαναφέρει την προηγουμένως επιλεγμένη σελίδα, και αφήνει το έγγραφο ακριβώς όπως ήταν

PDF Library for Delphi: Ροή τριών σταδίων του ReplacePageRanges που δείχνει προσωρινή εισαγωγή με επαναχαρτογράφηση αντικειμένων, αντιγραφή οπτικών καταχωρήσεων, και αποσύνδεση με διατήρηση αναφορών που προστατεύει τους μεταφερόμενους πόρους
Η εισαγωγή της πηγής ως προσωρινές σελίδες αφήνει τη συνηθισμένη επαναχαρτογράφηση να τρέξει πρώτη, οπότε η καταστροφική επέμβαση συρρικνώνεται σε αντιγραφή οπτικών κλειδιών και αποσύνδεση κόμβων χωρίς ανάκτηση ζωντανών πόρων
var
  Replaced: Integer;
begin
  Lib.SelectDocument(TargetDoc);
  // Options = 1: η σειρά πηγής διατηρείται και επιτρέπονται επαναλήψεις, οπότε
  // οι σελίδες στόχοι 5, 6 και 7 λαμβάνουν τις σελίδες πηγής 2, 1 και 2 αντίστοιχα
  Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
  if Replaced = 0 then
    raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
      [Lib.LastErrorCode]);
  // Σε επιτυχία, η επιλογή είναι η πρώτη αντικατεστημένη σελίδα
  Assert(Lib.SelectedPage = 5);
end;

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

// Μετα-συνθήκες που αξίζει να επαληθευτούν σε ένα regression test
Lib.SelectPage(3);
// Η γεωμετρία προέρχεται τώρα από τη σελίδα πηγή
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Οι σημειώσεις που ήταν ήδη στη σελίδα στόχο 3 παραμένουν συνδεδεμένες
WriteLn(Lib.AnnotationCount);
// Ο σελιδοδείκτης που δημιουργήθηκε πριν την αντικατάσταση εξακολουθεί να επιλύεται στη σελίδα 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// Και το έγγραφο έχει ακόμα το ίδιο μήκος
WriteLn(Lib.PageCount);

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

Οι annotations πηγής, τα πεδία φόρμας πηγής και τα outlines πηγής σκόπιμα δεν εισάγονται. Η μεταφορά ενός widget χωρίς την εγγραφή πεδίου /AcroForm του, ή μιας annotation που φέρει marked content χωρίς την ιδιοκτησία της στο structure tree, παράγει ένα μισο-εισαγμένο διαδραστικό αντικείμενο που κανένας viewer δεν μπορεί να συλλογιστεί, οπότε η πράξη μεταφέρει μόνο την εμφάνιση. Η πρακτική συνέπεια είναι ότι αν η σελίδα αντικατάστασης πρέπει να φέρει νέα πεδία φόρμας ή νέους συνδέσμους, αυτά προστίθενται στη σελίδα-στόχο εκ των υστέρων, πάνω στο αντικείμενο σελίδας-στόχου που εξακολουθεί να βρίσκεται εκεί περιμένοντάς τα

Δύο ακόμα όρια αξίζει να ελέγχονται στα δικά σας αρχεία. Πρώτον, το /Annots διατηρείται αλλά η γεωμετρία σελίδας όχι, οπότε η αντικατάσταση μιας σελίδας 220 mm με μια σελίδα 320 mm κρατά τα ορθογώνια annotation στις παλιές τους συντεταγμένες μέσα σε ένα /MediaBox διαφορετικού μεγέθους· αν η γεωμετρία αλλάζει, οι annotations που κρατήθηκαν πρέπει να επανατοποθετηθούν. Δεύτερον, οι εγγραφές εκτός των έντεκα οπτικών κλειδιών παραμένουν με τη σελίδα-στόχο εκ σχεδιασμού, κάτι που είναι σωστό για το /Trans ή το /AA και μπαγιάτικο για το /Thumb, οπότε τα thumbnails πρέπει να αναγεννηθούν μετά από μια αντικατάσταση. Τα tagged έγγραφα χρειάζονται μία επιπλέον σκέψη: τα στοιχεία δομής εξακολουθούν να δείχνουν στο σωστό αντικείμενο σελίδας μέσω του /Pg, αλλά οι αναγνωριστικοί τους δείκτες marked-content περιγράφουν περιεχόμενο που δεν βρίσκεται πια εκεί, οπότε μια εναλλαγή σελίδας μέσα σε μια ροή εργασίας PDF/UA είναι επεξεργασία structure tree εξίσου με επεξεργασία περιεχομένου. Αν η δουλειά είναι πράγματι σύνθεση αντί για εναλλαγή, στρώσεις εικαστικού πάνω σε σελίδες που κρατιούνται, η προσέγγιση page stitching και template είναι το φθηνότερο εργαλείο

Όλα όσα περιγράφονται εδώ, συμπεριλαμβανομένης της σύνταξης έκφρασης εύρους, των τιμών επιλογών και του γύρω API χειρισμού σελίδων, διατίθενται στην τυπική PDF Library for Delphi Delphi PDF Library για Delphi και C++Builder, της οποίας η τεκμηρίωση αναφοράς φέρει την πλήρη καταχώριση για την κλήση αντικατάστασης σελίδας και τους κωδικούς σφάλματός της