Διαγράψτε επτά σελίδες από ένα εγχειρίδιο 200 σελίδων και κάθε σελιδοδείκτης καταλήγει κάπου λάθος. Η λύση δεν είναι η επαναδόμηση του outline από μια επίπεδη λίστα τίτλων. Το PDFiumPas εκθέτει το TPdfOutlineEditor, που φορτώνει το πραγματικό δέντρο outline, σας αφήνει να μετακινήσετε και να επαναπροσδιορίσετε στόχους στοιχείων και μετά εκτελεί το ApplyPageMap για να μεταφέρει κάθε explicit destination μέσα από το σχέδιο σελίδων σας
Γιατί η διαγραφή σελίδων χαλάει κάθε σελιδοδείκτη;
Επειδή ένα στοιχείο outline δεν αποθηκεύει αριθμό σελίδας. Αποθηκεύει μια αναφορά σε αντικείμενο σελίδας, και όταν τα αντικείμενα σελίδων αλλάζουν η αναφορά είτε δείχνει σε σελίδα που μετακινήθηκε είτε στο τίποτα. Το ISO 32000-1 §12.3.2.2 ορίζει ένα explicit destination ως πίνακα του οποίου το πρώτο στοιχείο είναι έμμεση αναφορά σε λεξικό σελίδας, ακολουθούμενη από όνομα προσαρμογής όπως /Fit ή /XYZ. Διαγράψτε τη σελίδα και μένετε με μια κρεμάμενη αναφορά· αναδιατάξτε τις σελίδες και η αναφορά εξακολουθεί να είναι έγκυρη αλλά πλέον περιγράφει διαφορετικό κεφάλαιο. Το PDFiumPas επιλύει αυτόν τον πίνακα πίσω σε αριθμό σελίδας κατά τη φόρτωση, οπότε το TPdfOutlineItem.PageNumber σας δίνει δείκτη σελίδας που ξεκινά από το 1 και ταιριάζει με το δημόσιο API του TPdf αντί για αριθμό αντικειμένου. Αυτό είναι όλο το νόημα της αφαίρεσης: η λογική επαναχαρτογράφησής σας δουλεύει στο ίδιο σύστημα συντεταγμένων με το σχέδιο σελίδων που έχετε ήδη χτίσει όταν κόψετε, αναδιατάξετε ή στοιχειοθετήσετε το έγγραφο. Αν χτίζετε αυτό το σχέδιο, η ίδια σύμβαση αρίθμησης από το 1 διατρέχει τον διαχωρισμό PDF εγγράφων σε πολλαπλά αρχεία και την στοιχειοθεσία n-up και αναδιάταξη σελίδων
Το outline είναι διπλά συνδεδεμένο δέντρο, όχι λίστα
Ο λόγος που δεν μπορείτε απλώς να σειριοποιήσετε μια επίπεδη σειρά τίτλων είναι ότι το ISO 32000-1 §12.3.3 συνδέει κάθε στοιχείο outline με πέντε ξεχωριστούς συνδέσμους: /Parent, /Prev, /Next, /First και /Last. Η μετακίνηση ενός μόνο subtree ξαναγράφει επομένως τον παλιό γονέα, τον νέο γονέα, και τα δύο γειτονικά αδέλφια εκατέρωθεν του σημείου κοπής και του σημείου εισαγωγής, και τον δείκτη γονέα του ίδιου του μετακινούμενου κόμβου. Κάνετε ένα από αυτά λάθος και οι συμβατοί αναγνώστες δείχνουν ένα κομμένο δέντρο ή μπαίνουν σε βρόχο. Το PDFiumPas κρατά την κατάσταση επεξεργασίας ως πίνακα depth-first από εγγραφές TPdfOutlineItem με σταθερό ακέραιο Id, οπότε ένα subtree είναι ένα συνεχές τμήμα και η αλυσίδα αδελφών παράγεται, δεν συντηρείται χειροκίνητα. Το TPdfOutlineEditor.Move ανυψώνει αυτό το τμήμα, το επανεισάγει υπό τον νέο γονέα στο ζητούμενο ευρετήριο αδέλφου και επανααναθέτει μόνο τη ρίζα του μπλοκ. Αρνείται επίσης τις δύο μετακινήσεις που θα διέφθειραν το γράφημα: τη μετακίνηση στοιχείου στο δικό του subtree και τον ορισμό γονέα που δεν υπάρχει
Γιατί το /Count φέρει πρόσημο;
Επειδή το πρόσημο μεταφέρει την κατάσταση ανάπτυξης, όχι το μέγεθος. Ένα θετικό /Count σημαίνει ότι το στοιχείο είναι ανοιχτό και ο αριθμός λέει πόσοι απόγονοι είναι ορατοί τώρα· ένα αρνητικό /Count σημαίνει ότι το στοιχείο είναι κλειστό. Το PDFiumPas γράφει το πλήθος απογόνων για κάθε στοιχείο με παιδιά και το αντιστρέφει σε πρόσημο όταν το IsOpen είναι False, και στη φόρτωση διαβάζει την κατάσταση πίσω ως IsOpen := HasCount and (CountValue > 0). Αυτό είναι το πιο συνηθισμένο χειροποίητο bug σε writers outline: η εκπομπή count χωρίς πρόσημο και το σιωπηλό άνοιγμα όλου του δέντρου
var
Source, Dest: TMemoryStream;
Editor: TPdfOutlineEditor;
Options: TPdfOutlineEditOptions;
Report: TPdfOutlineValidationReport;
RootId, ChapterId: Integer;
begin
Source := TMemoryStream.Create;
Dest := TMemoryStream.Create;
Editor := nil;
try
Source.LoadFromFile('handbook.pdf');
Options := TPdfOutlineEditOptions.Default; // MaxItems 100000, MaxDepth 64
if not TPdfOutlineEditor.TryLoad(Source, Options, Editor, Report) then
raise Exception.Create(Report.ErrorMessage);
RootId := Editor[0].Id;
ChapterId := Editor[2].Id;
Editor.Move(ChapterId, RootId, 1); // γίνεται δεύτερο παιδί της ρίζας
Editor.SetTitle(ChapterId, 'Appendix B');
Editor.SetStyle(ChapterId, [posBold, posItalic]);
Editor.SetColor(ChapterId, 0.25, 0.5, 0.75);
Editor.SetExpanded(RootId, False); // γράφει αρνητικό /Count
Editor.Retarget(ChapterId, 12, '/XYZ 10 20 1');
if not Editor.SaveIncremental(Source, Dest, Report) then
raise Exception.Create(Report.ErrorMessage);
Dest.SaveToFile('handbook-edited.pdf');
finally
Editor.Free;
Dest.Free;
Source.Free;
end;
end;
Το Retarget χειρίζεται και τα δύο σχήματα που επιτρέπει η προδιαγραφή. Περάστε DestinationInAction ως False και το PDFiumPas γράφει έναν άμεσο πίνακα /Dest· περάστε True και γράφει μια ενέργεια Go-To, /A << /S /GoTo /D [ page ref suffix ] >>, κατά το ISO 32000-1 §12.6.4.2. Και στις δύο περιπτώσεις αφαιρεί πρώτα τυχόν υπάρχον /Dest και /A από το στοιχείο ώστε τα δύο να μην συνυπάρχουν και να διαφωνούν. Το suffix από προεπιλογή είναι /Fit και πρέπει να ξεκινά με όνομα PDF, γι' αυτό ένα κενό ή κακοσχηματισμένο suffix προκαλεί άμεσα εξαίρεση αντί να παράγει πίνακα προορισμού που κανένας αναγνώστης δεν μπορεί να αναλύσει
Πώς καταναλώνει το ApplyPageMap ένα σχέδιο σελίδων;
Το ApplyPageMap παίρνει ακριβώς τον πίνακα που το σχέδιο σελίδων σας έχει ήδη επικυρώσει: NewPageNumbers, δεικτοδοτημένο με παλιά σελίδα μείον ένα, που κρατά τον νέο αριθμό σελίδας με βάση τη μονάδα ή μηδέν όταν η σελίδα δεν επέζησε. Διασχίζει τον πίνακα στοιχείων προς τα πίσω ώστε η διαγραφή subtree να μην ακυρώνει ποτέ έναν δείκτη που δεν έχει επισκεφθεί ακόμη, και αναφέρει τι έκανε μέσω RemappedDestinationCount και RemovedDanglingItemCount
var
NewPageNumbers: array of Integer;
Report: TPdfOutlineValidationReport;
I: Integer;
begin
// Μία εγγραφή ανά σελίδα του ΑΡΧΙΚΟΥ εγγράφου
SetLength(NewPageNumbers, OriginalPageCount);
for I := 0 to OriginalPageCount - 1 do
NewPageNumbers[I] := 0; // 0 == αυτή η σελίδα απορρίφθηκε
NewPageNumbers[0] := 1; // παλιά σελίδα 1 -> νέα σελίδα 1
NewPageNumbers[1] := 2;
NewPageNumbers[9] := 3; // παλιά σελίδα 10 -> νέα σελίδα 3
// True: διαγραφή όλου του κρεμάμενου subtree. False: κράτηση του στοιχείου, αφαίρεση του στόχου του
if not Editor.ApplyPageMap(NewPageNumbers, True, Report) then
raise Exception.Create(Report.ErrorMessage);
WriteLn(Format('%d remapped, %d dangling items removed',
[Report.RemappedDestinationCount, Report.RemovedDanglingItemCount]));
end;
Η σημαία DeleteDangling αποφασίζει την πολιτική για προορισμό που χαρτογραφήθηκε σε μηδέν, και οι δύο κλάδοι είναι σκόπιμοι. Με True, το PDFiumPas διαγράφει το στοιχείο και όλο το subtree του, επειδή ένας κόμβος outline του οποίου ο στόχος εξαφανίστηκε συνήθως προΐσταται κεφαλαίου που εξαφανίστηκε μαζί του. Με False, το στοιχείο επιζεί με τον τίτλο και την ιεραρχία του ανέπαφα αλλά με αφαιρεμένα /Dest και /A, που είναι αυτό που θέλετε όταν κάποιος θα επαναπροσδιορίσει τον στόχο του σε αναθεώρηση. Οντως κακοσχηματισμένη είσοδος εξακολουθεί να αποτυγχάνει δυνατά αντί να επιδιορθώνεται: μια αρνητική εγγραφή ή ένας προορισμός που δείχνει πέρα από το τέλος του δοσμένου χάρτη επιστρέφει False με IssueKind σε poviInvalidPageMap
Αδιαφανείς εγγραφές και ο ειλικρινής συμβιβασμός
Δεν έχει κάθε στοιχείο outline αριθμό σελίδας που το PDFiumPas μπορεί να επεξεργαστεί λογικά. Τρία είδη μεταφέρονται ανέγγιχτα: τα named destinations, οι ενέργειες που δεν είναι /S /GoTo και τα άγνωστα κλειδιά λεξικού που πρόσθεσε όποιος παρήγαγε το αρχείο. Αυτά φορτώνουν με PageNumber ίσο με μηδέν, κρατούν τα αρχικά τους bytes στο στοιχείο και ξαναγράφονται κατά λέξη εκτός αν καλέσετε ρητά Retarget πάνω τους
- Ένα named destination είναι κλειδί στο δέντρο ονομάτων του εγγράφου, οπότε η σωστή επαναχαρτογράφησή του σημαίνει επίλυση του δέντρου και ξαναγράψιμο της εγγραφής στόχου, όχι μαντεψιά στο επίπεδο του outline
- Μια ενέργεια
/URI,/Launchή JavaScript δεν έχει καθόλου σημασιολογία σελίδας και δεν πρέπει να μετατρέπεται σιωπηλά σε Go-To - Κλειδιά ειδικά για προμηθευτή και structure destinations διατηρούνται επειδή η απόρριψη ό,τι δεν καταλαβαίνετε είναι ο τρόπος με τον οποίο οι round-trips χάνουν δεδομένα
Το κόστος είναι υπαρκτό και αξίζει να δηλώνεται ευθέως: το ApplyPageMap παρακάμπτει εντελώς αυτά τα στοιχεία, οπότε ένα έγγραφο του οποίου οι σελιδοδείκτες χρησιμοποιούν όλοι named destinations θα περάσει μια διαγραφή σελίδων με outline δομικά έγκυρο και σημασιολογικά παρωχημένο. Αυτή είναι η σκόπιμη επιλογή — ένας παρωχημένος σύνδεσμος που ένας αναθεωρητής μπορεί να πιάσει νικά έναν σίγουρα λάθος που κανείς δεν προσέχει. Αν ταξινομείτε εισερχόμενα αρχεία πριν τα επεξεργαστείτε, ένα πέρασμα απογραφής σε ένα PDF intake review workbench θα σας πει ποια έγγραφα πέφτουν σε αυτή την κατηγορία
Αποθήκευση: επαυξημένη αναθεώρηση και μετά ανεξάρτητη επαναφόρτωση
Το TPdfOutlineEditor.SaveIncremental προσθέτει μια αραιή επαυξημένη αναθεώρηση αντί να ξαναγράψει το αρχείο. Τα στοιχεία που είχαν φορτωθεί κρατούν την αρχική έμμεση αναφορά αντικειμένου τους συμπεριλαμβανομένης της ακριβούς γενιάς, οπότε οι υπάρχουσες σταυροαναφορές μένουν έγκυρες· μόνο τα στοιχεία που προσθέσατε παίρνουν φρέσκο αριθμό, δεσμευμένο από ένα μετά τον μέγιστο αριθμό αντικειμένου της αναθεώρησης. Ο κατάλογος ενημερώνεται στην ίδια αναθεώρηση, και μια λείπουσα εγγραφή /Outlines προστίθεται σε αυτόν όταν η πηγή δεν είχε καθόλου outline
Αυτό που συμβαίνει μετά την εγγραφή είναι το μέρος που αξίζει αντιγραφής. Το PDFiumPas ξανανοίγει τη ροή προορισμού με ένα εντελώς ανεξάρτητο editor και συγκρίνει το επαναφορτωμένο δέντρο με αυτό στη μνήμη — πλήθος στοιχείων, τίτλους, αριθμούς σελίδων, suffix προορισμών, μορφή ενέργειας έναντι άμεσου προορισμού, στυλ, κατάσταση ανάπτυξης και σχέσεις γονέα. Οποιαδήποτε αναντιστοιχία ή αποτυχία φόρτωσης καθαρίζει τη ροή προορισμού και επιστρέφει poviVerificationFailure αντί να σας παραδώσει ένα αρχείο που μοιάζει πειστικό. Κρυπτογραφημένες πηγές απορρίπτονται εξαρχής με poviEncryptedInput, αφού νέοι τίτλοι και προορισμοί δημιουργούν περιεχόμενο συμβολοσειρών που δεν μπορεί να παραχθεί αντιγράφοντας το trailer /Encrypt μπροστά
if not Editor.SaveIncremental(Source, Dest, Report) then
case Report.IssueKind of
poviEncryptedInput:
Log('Source is encrypted; outline editing needs an unprotected copy');
poviInvalidDestination:
Log(Format('Item %d %d targets a missing page',
[Report.ObjectNumber, Report.Generation]));
poviVerificationFailure:
Log('Reload check rejected the written revision: ' + Report.ErrorMessage);
else
Log(Report.ErrorMessage);
end;
Αντιμετωπίστε το outline ως αυτό που είναι — ένα συνδεδεμένο γράφημα αντικειμένων με τους δικούς του αμετάβλητους κανόνες — και η διαγραφή σελίδων παύει να είναι καταστροφή σελιδοδεικτών και γίνεται ένας χάρτης σελίδων που δίνετε σε μία κλήση μεθόδου. Το TPdfOutlineEditor, το ApplyPageMap και ο επαληθευμένος επαυξημένος writer έρχονται στο PDFiumPas από τη v3.98.0 για Delphi, C++Builder και Lazarus· μπορείτε να δείτε ολόκληρο το API και να κατεβάσετε δοκιμαστική έκδοση στη σελίδα προϊόντος PDFium Delphi Component