Το PDF Library for Delphi γράφει περιοχές ετικετών σελίδων με το AddPageLabels, και από το v3.539.10 εκείνη η κλήση δουλεύει και σε φορτωμένα αρχεία των οποίων το δέντρο αριθμών /PageLabels είναι κομμένο σε nodes /Kids: η ρίζα ισοπεδώνεται σε ένα μοναδικό φύλλο /Nums πριν μπει η νέα περιοχή, οπότε η ετικέτα όντως εμφανίζεται στον viewer αντί να αγνοείται σιωπηλά. Το τυπικό θύμα είναι ένα book-style PDF από εργαλείο σελιδοποίησης, με ρωμαϊκούς αριθμούς στα προκαταρκτικά, αραβική αρίθμηση στο κυρίως σώμα και παράρτημα με ετικέτες A-1, A-2, όπου θέλατε απλώς να μετονομάσετε το παράρτημα και δεν άλλαξε τίποτα
Τι είναι οι ετικέτες σελίδων PDF και πώς αποθηκεύονται;
Οι ετικέτες σελίδων είναι τα strings που δείχνει ένας viewer στο page box του αντί για τον φυσικό δείκτη σελίδας, και το ISO 32000-1 §12.4.2 τα αποθηκεύει ως δέντρο αριθμών κάτω από το κλειδί καταλόγου /PageLabels. Κάθε κλειδί είναι 0-based δείκτης σελίδας που ξεκινά μια περιοχή ετικετών, και κάθε τιμή είναι dictionary ετικέτας σελίδας με έως τρεις εγγραφές: /S για το στυλ αρίθμησης (D, R, r, A ή a), /P για string προθέματος, και /St για την αριθμητική τιμή της πρώτης σελίδας της περιοχής, που defaults στο 1. Μια περιοχή τρέχει μέχρι το επόμενο κλειδί, και η προδιαγραφή απαιτεί το δέντρο να περιέχει τιμή για δείκτη σελίδας 0, οπότε κάθε σελίδα καλύπτεται από κάποια περιοχή
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
Exit;
// Σελίδες 1-4: i, ii, iii, iv (μικρά ρωμαϊκά)
Lib.AddPageLabels(1, 3, 1, '');
// Σελίδες 5-120: 1, 2, 3 ... (δεκαδικά)
Lib.AddPageLabels(5, 1, 1, '');
// Σελίδες 121 και μετά: A-1, A-2 ... (δεκαδικά με πρόθεμα)
Lib.AddPageLabels(121, 1, 1, 'A-');
WriteLn(Lib.GetPageLabel(5)); // 1
WriteLn(Lib.GetPageLabel(122)); // A-2
Lib.SaveToFile('handbook-labeled.pdf');
finally
Lib.Free;
end;
end;
Το TPDFlib.AddPageLabels(Start, Style, Offset, Prefix) αντιστοιχίζει τα ορίσματά του σε εκείνο το dictionary χωρίς εκπλήξεις μόλις ξέρετε τρεις κανόνες. Το Start είναι 1-based όπως κάθε άλλο όρισμα σελίδας στη βιβλιοθήκη και γράφεται στο δέντρο ως Start - 1. Το Style τρέχει από 0 έως 5, όπου 0 σημαίνει μόνο πρόθεμα και 1 έως 5 γίνονται τιμές /S D, R, r, A και a· οτιδήποτε έξω από εκείνη την περιοχή επιστρέφει 0 και δεν αγγίζει τίποτα. Το Offset γίνεται /St μόνο όταν είναι μεγαλύτερο από μηδέν, οπότε το 0 απλώς παραλείπει το κλειδί και ο viewer γυρνά στο default του 1. Επειδή οι ετικέτες σελίδων ήρθαν στο PDF 1.3, η κλήση τρέχει και EnsureMinVersion('1.3', '/PageLabels'), που ανεβάζει την έκδοση εξόδου ενός παλαιότερου αρχείου εκτός αν έχετε κλειδώσει ρητά την έκδοση αποθήκευσης
Γιατί εξαφανίζονται οι νέες ετικέτες σελίδων όταν το δέντρο έχει /Kids;
Οι νέες ετικέτες εξαφανίζονται επειδή το ISO 32000-1 §7.9.7 (Πίνακας 37) θέλει η ρίζα ενός δέντρου αριθμών να κουβαλά είτε /Kids είτε /Nums, ποτέ και τα δύο, και ο παλιότερος helper NumTreeSet ήξερε μόνο να ψάχνει για /Nums. Παραγωγοί που βγάζουν μεγάλα έγγραφα συχνά κόβουν το δέντρο σε ενδιάμεσα nodes, το καθένα με ζευγάρι /Limits, και τα κρεμάνε σε ρίζα που έχει μόνο /Kids. Ο παλιός κώδικας δεν βρήκε /Nums σε εκείνη τη ρίζα, έφτιαξε φρέσκο δίπλα στα υπάρχοντα /Kids, και έβαλε τη νέα περιοχή εκεί. Το αποτέλεσμα ήταν μια ρίζα με δύο αμοιβαία αποκλειόμενα σημεία εισόδου. Οι viewers κατεβαίνουν μέσα από /Kids και δεν κοιτούν ποτέ την παραπλανητική αράθμηση, το δικό EnumNumTree της βιβλιοθήκης επίσης ελέγχει πρώτα /Kids, και το NumTreeLookup αρνείται node όπου HasKids xor HasNums είναι false. Το AddPageLabels εξακολούθησε να επιστρέφει 1 και το αποθηκευμένο αρχείο άνοιγε καθαρά, που είναι το χειρότερο είδος αποτυχίας: τίποτα δεν παραπονιέται, οι ετικέτες απλώς μένουν ίδιες
Το fix στο NumTreeSet μετατρέπει τη ρίζα σε φύλλο πριν εισάγει οτιδήποτε. Όταν η ρίζα κουβαλά /Kids, το EnumNumTree περπατά κάθε φύλλο με τη σειρά και μαζεύει κάθε ζευγάρι κλειδιού και τιμής, ένας νέος flat πίνακας /Nums χτίζεται από εκείνη τη λίστα, και /Kids, /Limits και όποιο ξεπερασμένο /Nums καθαρίζονται από τη ρίζα πριν προσαρτηθεί ο flat πίνακας. Το κόψιμο του /Limits δεν είναι καλλωπιστικό, αφού ο Πίνακας 37 επιτρέπει εκείνη την εγγραφή μόνο σε ενδιάμεσα και φύλλα nodes, ποτέ στη ρίζα. Από εκεί και πέρα η εισαγωγή είναι συνηθισμένο ταξινομημένο insert σε έναν πίνακα, και οι υπάρχουσες περιοχές επιζητούν με τα πρωτότυπα dictionaries ετικετών τους. Ο συμβιβασμός είναι σκόπιμος: το δέντρο δεν ξαναχτίζεται σε ισορροπημένα nodes /Kids μετά. Για ετικέτες σελίδων αυτό δεν κοστίζει τίποτα, αφού ακόμα και ένα μεγάλο εγχειρίδιο αναφοράς σπάνια έχει πάνω από μερικές δεκάδες περιοχές, και ένα μοναδικό φύλλο είναι ό,τι γράφουν οι περισσότεροι παραγωγοί ούτως ή άλλως
// Μετονομασία του παραρτήματος σε αρχείο με ρίζα /PageLabels που χρησιμοποιεί /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
WriteLn('Before: ', Lib.GetPageLabel(121)); // π.χ. A-1
// Αντικατάσταση της περιοχής που ξεκινά στη σελίδα 121: App-a, App-b ...
if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
Lib.SaveToFile('vendor-manual-relabeled.pdf');
// Οι υπάρχουσες ρωμαϊκές και δεκαδικές περιοχές είναι ακόμα στο ισοπεδωμένο φύλλο
WriteLn('After: ', Lib.GetPageLabel(121)); // App-a
WriteLn('Front: ', Lib.GetPageLabel(2)); // ii, αμετάβλητο
end;
Πώς μπορεί ένας πίνακας /Nums να παρερμηνευτεί ως κλειδιά;
Ένας πίνακας /Nums παρερμηνεύεται όταν ο κώδικας τον περπατά ένα στοιχείο τη φορά, επειδή ο πίνακας είναι flat διάταξη εναλλασσόμενων ζευγαριών, [key0 value0 key1 value1 ...], και μόνο οι άρτιες θέσεις είναι κλειδιά. Η παλιά λούπα του NumTreeSet τεστάριζε κάθε στοιχείο για αριθμητικό τύπο, οπότε μια τιμή που τύχαινε να είναι αριθμός συγκρινόταν σαν να ήταν κλειδί· ένα less-than χτύπημα μπορούσε να βάλει το σημείο εισαγωγής σε μονό δείκτη και να ρίξει το νέο ζευγάρι στη μέση υπάρχοντος, μετατοπίζοντας κάθε μεταγενέστερο ζευγάρι εκτός φάσης. Το EnumNumTree είχε το ίδιο μονοβηματικό πέρασμα. Και τα δύο πλέον επαναλαμβάνουν ζευγάρια με βήμα δύο, διαβάζοντας το κλειδί στο X * 2 και την τιμή στο X * 2 + 1, και ακριβές ταίριασμα κλειδιού αντικαθιστά την τιμή και εξέρχεται με Break. Για να είμαστε δίκαιοι, οι τιμές ετικετών σελίδων είναι dictionaries, οπότε αυτό το δεύτερο bug σπάνια πυροδοτήθηκε στο ίδιο το /PageLabels, αλλά ένας helper δέντρου αριθμών που διαβάζει λάθος βήμα είναι διεφθαρμένος τη στιγμή που οποιαδήποτε τιμή είναι αριθμός, και διορθώθηκε στο ίδιο πέρασμα
Ανάγνωση ετικετών πίσω και round trip τους
Το TPDFlib.GetPageLabel(Page) επιστρέφει την ετικέτα για σελίδα 1-based και έχει δύο fallbacks που αξίζει να ξέρετε. Χωρίς καθόλου εγγραφή /PageLabels επιστρέφει τον δεκαδικό αριθμό σελίδας, οπότε ο καλών μπορεί να τον χρησιμοποιεί άνευ όρων. Με δέντρο παρόν αλλά καμία περιοχή που καλύπτει τη σελίδα επιστρέφει κενό string, που είναι ακριβώς ό,τι συμβαίνει όταν ένα αρχείο παραλείπει την υποχρεωτική εγγραφή δείκτη 0· η documentation αναφοράς λέει ότι μια περιοχή που ξεκινά στη σελίδα 1 πρέπει να υπάρχει για να εμφανίζονται σωστά οι ετικέτες, και ο κώδικας κάνει εκείνη την απαίτηση ορατή. Τα στυλ γραμμάτων ακολουθούν την προδιαγραφή αντί για στήλες spreadsheet: μετά το Z έρχεται AA, μετά BB, επαναλαμβάνοντας το γράμμα αντί να κάνει κράτημα
var
P: Integer;
Data: WideString;
begin
// Γρήγορος έλεγχος του τι θα δείξει ένας viewer στο page box του
for P := 1 to Lib.PageCount do
WriteLn(P, ' -> ', Lib.GetPageLabel(P));
// Η τιμή option 4 εξάγει μόνο περιοχές ετικετών ως εγγραφές PageLabelBegin
Data := Lib.ExportDocumentData(4);
// Η εισαγωγή τις ξαναπαίζει μέσω ClearPageLabels + AddPageLabels
Lib.ImportDocumentData(Data, 0);
end;
Για μαζικές αλλαγές, το ExportDocumentData με τιμή option 4 γράφει κάθε περιοχή ως block PageLabelBegin με γραμμές PageLabelNewIndex, PageLabelStart, PageLabelPrefix και PageLabelNumStyle, και το ImportDocumentData μεταχειρίζεται την πρώτη εγγραφή ετικέτας που βλέπει ως πλήρη αντικατάσταση: καλεί το ClearPageLabels μία φορά και μετά τροφοδοτεί κάθε εγγραφή στο AddPageLabels. Αυτό κάνει ένα round trip κειμένου ντετερμινιστικό ακόμα και όταν το πρωτότυπο αρχείο χρησιμοποιούσε δέντρο /Kids, επειδή το καθάρισμα αφαιρεί ολόκληρη την εγγραφή καταλόγου και το ξαναχτισμένο δέντρο είναι ένα μοναδικό φύλλο από την αρχή
Τι δεν εγγυάται ακόμα το fix;
Η ισοπέδωση είναι μονής κατεύθυνσης και εμπιστεύεται τη σειρά που βρίσκει. Το EnumNumTree μαζεύει ζευγάρια σε σειρά αρχείου, και το GetPageLabel εφαρμόζει την τελευταία περιοχή της οποίας το κλειδί είναι μικρότερο ή ίσο του δείκτη σελίδας, οπότε ένα ξένο αρχείο με φύλλα εκτός σειράς, που το §7.9.7 απαγορεύει αλλά κυκλοφορεί, μπορεί ακόμα να δώσει λάθος ετικέτες μέχρι να ξαναχτίσετε τις περιοχές με ClearPageLabels και φρέσκες κλήσεις AddPageLabels. Οι ετικέτες επίσης δένουν σε δείκτες σελίδων, όχι σε αντικείμενα σελίδων, οπότε κάθε λειτουργία που αλλάζει πλήθος ή σειρά σελίδων αφήνει τις περιοχές εκεί που ήταν. Ανταλλαγή in-place όπως ο αντικατάσταση σελίδων διατηρώντας αριθμούς objects κρατά το πλήθος και άρα τις ετικέτες ευθυγραμμισμένες, ενώ μια συγχώνευση όπως ο collate μπλεγμένων duplex σκαν παράγει νέα σειρά σελίδων που αξίζει φρεσκογραμμένο σύνολο περιοχών
Οι κλήσεις ετικετών σελίδων, ο χειρισμός δέντρου αριθμών και η εξαγωγή και εισαγωγή δεδομένων εγγράφου που περιγράφονται εδώ κυκλοφορούν όλα στο PDF Library for Delphi για Delphi, C++Builder και Lazarus, με την εγγραφή αναφοράς του AddPageLabels να τεκμηριώνει τις τιμές στυλ και τους κωδικούς επιστροφής