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

PDFlibPas MovePage: όταν τα boxes μοιράζονται instances

Στο PDFlibPas, τη βιβλιοθήκη PDF για Delphi, μια σελίδα που μετακινούνταν με MovePage λάμβανε τα ίδια ακριβώς objects MediaBox, CropBox και Resources που κρατούσε ο παλιός της κόμβος Pages, οπότε ένα μεταγενέστερο SetPageBox ή DrawText στη μετακινημένη σελίδα ξανάγραφε αθόρυβα εκείνον τον κόμβο και κάθε αδελφό που ακόμα κληρονομούσε από αυτόν. Από το v3.539.36 η μετακινημένη σελίδα παίρνει τα δικά της αντίγραφα, και μια indirect reference μένει reference. Η ίδια έκδοση κλείνει δύο συγγενείς διαδρομές: SetPageBox πάνω σε indirect box που μοιράζονται πολλές σελίδες, και CopyPageRanges που άφηνε σελίδες του εγγράφου πηγής δεμένες στον κόμβο Pages τους, με το CropBox δεμένο στο MediaBox

Οι αναφορές που οδηγούν εδώ δεν λένε ποτέ ταυτότητα objects. Λένε πράγματα όπως «έκανα crop τη σελίδα 7 και οι σελίδες 8 έως 12 κόπηκαν κι αυτές», ή «στένωσα το CropBox και το MediaBox μετακινήθηκε μαζί του», ή, η πιο μπερδεμένη, «αντέγραψα μια σελίδα σε νέο έγγραφο και το αρχικό αρχείο άλλαξε». Τίποτα δεν καταρρέει, τίποτα δεν διαρρέει, και το αποθηκευμένο αρχείο είναι απολύτως έγκυρο PDF. Απλώς περιέχει γεωμετρία που κανείς δεν ζήτησε

Γιατί ένα SetPageBox σε μια σελίδα αλλάζει μέγεθος στις αδελφές της;

Το SetPageBox αλλοίωνε αδελφές επειδή δύο εγγραφές του page tree έδειχναν στον ίδιο πίνακα στη μνήμη, και το SetPageBox επεξεργάζεται τον πίνακα στόχο του στη θέση του. Κάθε σελίδα ή κόμβος Pages που κρατούσε το ίδιο instance έβλεπε την αλλαγή. Τρεις διαδρομές κώδικα στο PDFlibPas παρήγαν αυτόν τον μοιρασμό πριν το v3.539.36:

  • Το MovePage υλοποιεί τις κληρονομήσιμες ιδιότητες πάνω στη σελίδα πριν την αποσπάσει από τον γονέα της, και συνάπτε τα objects του προγόνου αντί για αντίγραφα, οπότε η μετακινημένη σελίδα και οι πρώην αδελφές της μοιράζονταν έναν πίνακα box και ένα λεξικό Resources
  • Το SetPageBox ακολουθούσε indirect references και επεξεργαζόταν τον αναφερόμενο πίνακα, οπότε ένα αρχείο όπου πολλές σελίδες δείχνουν σε ένα object /MediaBox 11 0 R είχε όλες εκείνες τις σελίδες να αλλάζουν μέγεθος με μία κλήση, ανεξάρτητα αν το MovePage εμπλεκόταν ποτέ
  • Το CopyPageRanges υλοποιεί τις κληρονομημένες τιμές πάνω στη σελίδα πηγής πριν την κλωνοποιήσει στο έγγραφο στόχο, και συνάπτε τα instances του κόμβου Pages στη σελίδα πηγής, συν το ίδιο το instance του MediaBox ως προεπιλεγμένο CropBox
Aliasing του MovePage στο PDFlibPas όπου η μετακινημένη σελίδα και η πρώην αδελφή της κρατούσαν και οι δύο το ίδιο instance του πίνακα MediaBox του προγόνου, οπότε το SetPageBox επεξεργαζόταν τη μια σελίδα και αλλοίωνε την άλλη· από το v3.539.36 η υλοποίηση συνάπτει αποκωδικοποιημένα αντίγραφα και οι αλλαγές μένουν τοπικές στη σελίδα που αγγίζετε
Δύο εγγραφές page tree που έδειχναν στον ίδιο πίνακα στη μνήμη έκαναν κάθε αλλαγή να προσγειώνεται σε κάθε κάτοχο, και το αποθηκευμένο PDF έμενε έγκυρο όλο το διάστημα

Η περίπτωση του MovePage έχει σύντομη ιστορία. Πριν το v3.539.27, το MovePage μετέφερε μόνο το /Resources, οπότε μια σελίδα που μετακινούνταν κάτω από διαφορετικό γονέα έπαιρνε αθόρυβα μέγεθος και περιστροφή εκείνου του γονέα. Το v3.539.27 διόρθωσε το χαμένο MediaBox, CropBox και Rotate, που είναι και αυτό στο οποίο βασίζεται το CollateDocumentsEx όταν αναταξινομεί σελίδες, αλλά σύναπτε τις τιμές του προγόνου ως κοινά instances. Αυτό είναι το παράθυρο που κλείνει το v3.539.36. Οι διαδρομές SetPageBox και CopyPageRanges είναι παλαιότερες· κάθε build πριν το v3.539.36 τις έχει

Αμεσες τιμές, indirect references και κληρονομιά ιδιοτήτων σελίδας

Ένα σωστό αντίγραφο κληρονομημένης ιδιότητας σελίδας αντιγράφει τις άμεσες τιμές και κρατά τις indirect references ως references, επειδή αυτή είναι η διάκριση που κάνει το ίδιο το ISO 32000-1. Ένα direct object όπως το [0 0 400 300] γραμμένο μέσα σε ένα λεξικό ανήκει μόνο σε εκείνο το λεξικό. Ένα indirect object, ορισμένο μία φορά ως 11 0 obj και αναφερόμενο ως 11 0 R, είναι κοινό by design: το ISO 32000-1 §7.3.10 το καθιστά προσβάσιμο από οπουδήποτε στο αρχείο, και κάθε 11 0 R σημαίνει το ίδιο object

Η κληρονομιά ιδιοτήτων σελίδας, ISO 32000-1 §7.7.3.4, προσθέτει μια τρίτη περίπτωση. Τα Resources, MediaBox, CropBox και Rotate μπορούν να κάθονται σε έναν κόμβο Pages και να ισχύουν για κάθε απόγονο σελίδα που δεν ορίζει δικό της. Η σελίδα δεν κρατά την τιμή· την αναζητά μέσω /Parent. Εκείνη η αλυσίδα αναζήτησης σπάει τη στιγμή που μια σελίδα αλλάζει γονέα, γι αυτό το MovePage και το BalancePageTree πρέπει πρώτα να γράψουν τις δραστικές τιμές πάνω στην ίδια τη σελίδα. Το ερώτημα είναι μόνο πώς να τις γράψουν

Γιατί ένα object pool κρύβει το λάθος

Στο PDFlibPas κάθε parsed ή δημιουργημένο PDF object ανήκει στο pool TPDFStructure του εγγράφου, και τα λεξικά και οι πίνακες αποθηκεύουν γυμνούς δείκτες στις εγγραφές τους. Το TPDFDictionary.Add καταγράφει τον δείκτη και τίποτα άλλο. Η προσθήκη ενός instance σε δύο γονικά containers είναι επομένως νόμιμη σε κάθε επίπεδο που μπορεί να ελέγξει το runtime: κανένα double free στο teardown, κανένας reference count για να πάει στραβά, καμία exception. Η σειριοποίηση είναι εξίσου επιεικής, αφού κάθε container γράφει inline την τρέχουσα τιμή του κοινού instance, και πριν από οποιαδήποτε αλλαγή η έξοδος είναι byte-προς-byte ό,τι θα παρήγε ένα σωστό αντίγραφο

Το aliasing εμφανίζεται μόνο όταν κάποιος μεταβάλλει το κοινό instance στη θέση του. Το SetPageBox το κάνει ακριβώς αυτό μέσω ενός wrapper ορθογωνίου πάνω στον υπάρχοντα πίνακα, και το σχέδιο σε μια σελίδα το κάνει στο λεξικό Resources όταν καταχωρείται γραμματοσειρά ή εικόνα. Η αλλαγή προσγειώνεται, αθόρυβα, σε κάθε άλλο container που κρατά τον δείκτη

Πώς το PDFlibPas v3.539.36 αντιγράφει αντί να μοιράζεται

Το PDFlibPas v3.539.36 διορθώνει το πρόβλημα και στα δύο άκρα: η υλοποίηση συνάπτει πλέον αντίγραφα, και οι εγγραφές box επεξεργάζονται πλέον μόνο πίνακα που κατέχει η σελίδα. Κάθε διόρθωση καλύπτει μια περίπτωση που η άλλη δεν μπορεί

Ο βοηθός υλοποίησης, PLInheritPageAttributes, συνάπτει πλέον Page.Owner.Decode(Value.Output) αντί για το Value. Το round-trip μέσω του serializer είναι ένα αμβλύ αλλά ακριβές τρόπος να πάρετε PDF semantics δωρεάν. Ένας άμεσος πίνακας ή λεξικό σειριοποιείται στο κυριολεκτικό του κείμενο και αποκωδικοποιείται σε φρέσκο, ανεξάρτητο instance. Μια indirect reference σειριοποιείται ως 11 0 R και αποκωδικοποιείται σε νέο object reference που δείχνει στο ίδιο object 11, οπότε η σελίδα εξακολουθεί να αναφέρεται στο κοινό object αντί να λαμβάνει ενσωματωμένο αντίγραφο, που διατηρεί τη συμπεριφορά reference που μπήκε στο v3.539.27. Το αντίγραφο είναι ακριβώς τόσο βαθύ όσο η άμεση δομή: οτιδήποτε φτάνει μέσω reference μέσα σε αντιγραμμένο λεξικό μένει κοινό, όπως προορίζει η μορφή αρχείου. Το BalancePageTree καλεί τον ίδιο βοηθό για κάθε σελίδα που ξαναγονεποιεί, οπότε οι σελίδες που υλοποιούνται εκεί παίρνουν επίσης ξεχωριστά instances

Round-trip υλοποίησης στο PDFlibPas όπου το PLInheritPageAttributes συνάπτει Page.Owner.Decode(Value.Output): ένας άμεσος πίνακας σειριοποιείται σε κυριολεκτικό κείμενο και αποκωδικοποιείται σε φρέσκο instance, ενώ μια indirect 11 0 R σειριοποιείται και αποκωδικοποιείται σε νέο reference που εξακολουθεί να δείχνει στο κοινό object 11
Η σειριοποίηση και επανάληψη parsing δίνει PDF object semantics δωρεάν: οι άμεσες τιμές αντιγράφονται, οι references μένουν references, ακριβώς όπως προορίζει το ISO 32000-1

Η αντιγραφή μόνη της δεν φτάνει, επειδή η περίπτωση reference εξακολουθεί να δείχνει σε κοινό object. Αν το SetPageBox ακολουθούσε εκείνη την reference και επεξεργαζόταν το object 11, η μετακινημένη σελίδα θα άλλαζε ξανά μέγεθος στον παλιό γονέα και στα άλλα παιδιά του. Ο box writer εφαρμόζει λοιπόν τώρα copy-on-write: επεξεργάζεται στη θέση του μόνο όταν η δική του εγγραφή της σελίδας είναι άμεσος πίνακας, και αντικαθιστά indirect ή λείπον box με νέο άμεσο πίνακα. Το object 11 μένει ανέγγιχτο για κάθε άλλη σελίδα που το επικαλείται

Απόφαση copy-on-write του SetPageBox στο PDFlibPas: όταν η δική του εγγραφή της σελίδας είναι άμεσος πίνακας επεξεργάζεται στη θέση της, και όταν είναι indirect reference ή λείπει ο writer την αντικαθιστά με νέο άμεσο πίνακα ώστε το κοινό object 11 κρατά την τιμή του για κάθε άλλη σελίδα που το επικαλείται
Η αντιγραφή στην υλοποίηση δεν φτάνει όσο references εξακολουθούν να δείχνουν σε κοινά objects, οπότε ο box writer επεξεργάζεται μόνο ό,τι κατέχει η σελίδα
Διαδρομή κώδικαΠριν το v3.539.36Από το v3.539.36
MovePage υλοποίησηΗ σελίδα κρατά τα ίδια τα άμεσα instances του προγόνουΗ σελίδα κρατά αποκωδικοποιημένα αντίγραφα· references μένουν references
SetPageBoxΑκολουθεί reference και επεξεργάζεται τον κοινό πίνακαΕπεξεργάζεται μόνο άμεσο πίνακα στη σελίδα, αλλιώς γράφει νέον
CopyPageRanges σελίδα πηγήςΜοιράζεται boxes κόμβου Pages· το CropBox είναι το instance του MediaBoxΚάθε υλοποιημένη τιμή στη σελίδα πηγής είναι αντίγραφο
Προεπιλεγμένα boxes κατά την κλωνοποίηση πόρων σελίδαςCropBox, BleedBox, TrimBox και ArtBox μοιράζονται έναν πίνακαΚάθε προεπιλεγμένο box παίρνει τον δικό του πίνακα

Η τελευταία γραμμή είναι η λανθάνουσα. Όταν η βιβλιοθήκη κλωνοποιεί τους πόρους μιας σελίδας για σύλληψη σελίδας ή συγχώνευση, συμπληρώνει τα λείποντα CropBox, BleedBox, TrimBox και ArtBox, και εκείνα ήταν το ίδιο instance πίνακα. Κανένας σημερινός καλών δεν άφησε εκείνο το alias να επιβιώσει αρκετά ώστε να επεξεργαστεί, αλλά ο επόμενος θα το είχε. Πώς επιλέγονται εκείνες οι προεπιλεγμένες τιμές box είναι δικό του θέμα, καλυμμένο στον οδηγό PDFlibPas για προεπιλέγματα TrimBox, BleedBox και CropBox

Αναπαραγωγή του aliasing του MovePage με χειροποίητο PDF

Ο γρηγορότερος τρόπος να ελέγξετε οποιοδήποτε build του PDFlibPas είναι ένα μικρό χειρόγραφο PDF φορτωμένο με LoadFromString, όπου κάθε αριθμός object είναι γνωστός εκ των προτέρων. Ο βοηθός παρακάτω γράφει κλασικό πίνακα cross-reference με σωστά υπολογισμένα byte offsets, ώστε το test να μην βασίζεται στη συμπεριφορά ανάκτησης του parser για κατεστραμμένα αρχεία

uses
  System.SysUtils, PDFlibrary;

function BuildPdf(const Objects: array of AnsiString): AnsiString;
var
  Offsets: array of Integer;
  I, XRefPos: Integer;
begin
  Result := '%PDF-1.4'#10;
  SetLength(Offsets, Length(Objects));
  for I := 0 to High(Objects) do
  begin
    Offsets[I] := Length(Result);   // byte offset με βάση το 0 του "N 0 obj"
    Result := Result + AnsiString(IntToStr(I + 1)) + ' 0 obj'#10 +
      Objects[I] + #10'endobj'#10;
  end;
  XRefPos := Length(Result);
  Result := Result + 'xref'#10'0 ' + AnsiString(IntToStr(Length(Objects) + 1)) +
    #10'0000000000 65535 f '#10;
  for I := 0 to High(Offsets) do      // κάθε εγγραφή είναι ακριβώς 20 bytes
    Result := Result + AnsiString(Format('%.10d 00000 n ', [Offsets[I]])) + #10;
  Result := Result + 'trailer'#10'<< /Size ' +
    AnsiString(IntToStr(Length(Objects) + 1)) + ' /Root 1 0 R >>'#10 +
    'startxref'#10 + AnsiString(IntToStr(XRefPos)) + #10'%%EOF'#10;
end;

function StreamObj(const Content: AnsiString): AnsiString;
begin
  Result := '<< /Length ' + AnsiString(IntToStr(Length(Content))) +
    ' >>'#10'stream'#10 + Content + #10'endstream';
end;

Το test έγγραφο έχει δύο ενδιάμεσους κόμβους Pages. Ο κόμβος 3 κρατά indirect MediaBox (object 11, 400 επί 300 points), άμεσο CropBox και άμεσο λεξικό Resources, και έχει δύο σελίδες. Ο κόμβος 4 έχει MediaBox μεγέθους Letter και έχει την τρίτη σελίδα. Η μετακίνηση της σελίδας 1 στη θέση 3 την ξαναγονεποιεί κάτω από τον κόμβο 4, που είναι ακριβώς η μετακίνηση που χρειάζεται υλοποίηση: χωρίς αυτήν, η σελίδα θα γινόταν σελίδα Letter

procedure Check(Condition: Boolean; const Msg: string);
begin
  if not Condition then
    raise Exception.Create(Msg);
end;

procedure CheckMovedPageIsIsolated;
var
  Lib: TPDFlib;
  FontID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Check(Lib.LoadFromString(BuildPdf([
      '<< /Type /Catalog /Pages 2 0 R >>',
      '<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>',
      '<< /Type /Pages /Parent 2 0 R /Kids [5 0 R 6 0 R] /Count 2 ' +
        '/MediaBox 11 0 R /CropBox [10 20 390 280] /Resources << >> >>',
      '<< /Type /Pages /Parent 2 0 R /Kids [7 0 R] /Count 1 ' +
        '/MediaBox [0 0 612 792] >>',
      '<< /Type /Page /Parent 3 0 R /Contents 8 0 R >>',
      '<< /Type /Page /Parent 3 0 R /Contents 9 0 R >>',
      '<< /Type /Page /Parent 4 0 R /Contents 10 0 R >>',
      StreamObj('1 w'), StreamObj('2 w'), StreamObj('3 w'),
      '[0 0 400 300]']), '') = 1, 'load failed');

    Lib.SelectPage(1);
    Check(Lib.MovePage(3) = 1, 'MovePage failed');
    Lib.SelectPage(3);                       // η σελίδα που μόλις μετακινήσαμε
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'inherited MediaBox lost');

    Lib.SetPageBox(1, 0, 200, 200, 200);     // MediaBox 200 x 200
    Lib.SetPageBox(2, 0, 100, 100, 100);     // CropBox 100 x 100
    FontID := Lib.AddStandardFont(4);        // Helvetica
    Lib.SelectFont(FontID);
    Lib.SetTextSize(12);
    Lib.DrawText(20, 20, 'MOVED');

    // Ελέγξτε τον παλιό γονέα ΠΡΙΝ επιλέξετε άλλη σελίδα (βλ. παρακάτω)
    Check(Pos(AnsiString('/Font'), Lib.GetObjectToString(3)) = 0,
      'font registered in the old Pages node');

    Lib.SelectPage(1);                       // πρώην σελίδα 2, ακόμα κάτω από τον κόμβο 3
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'sibling MediaBox changed');
    Check(Abs(Lib.GetPageBox(2, 2) - 380) < 0.001, 'sibling CropBox changed');
    Check(Pos(AnsiString('400'), Lib.GetObjectToString(11)) > 0,
      'shared object 11 was rewritten');
  finally
    Lib.Free;
  end;
end;

Το GetPageBox(BoxType, Dimension) παίρνει τύπο box 1 για MediaBox και 2 για CropBox, και διάσταση 2 για πλάτος. Με την προεπιλεγμένη αρχή κάτω αριστερά, το SetPageBox(1, 0, 200, 200, 200) σημαίνει αριστερά 0, πάνω 200, 200 πλάτος και 200 ύψος. Σε builds ανάμεσα στο v3.539.27 και το v3.539.35, οι έλεγχοι αδελφών αποτυγχάνουν: η αλλαγή CropBox προσγειώνεται στον άμεσο πίνακα του κόμβου 3, και η αλλαγή MediaBox ξαναγράφει το object 11 μέσω της reference

Αλλάζει το CopyPageRanges το έγγραφο πηγής;

Από το v3.539.36, το CopyPageRanges εξακολουθεί να γράφει πάνω στις σελίδες πηγής, αλλά κάθε τιμή που γράφει είναι ξεχωριστό αντίγραφο, οπότε μεταγενέστερες αλλαγές στην πηγή μένουν τοπικές στη σελίδα που επεξεργάζεστε. Η ίδια η εγγραφή είναι σκόπιμη: η σελίδα πηγής χρειάζεται ρητά MediaBox, CropBox, Rotate και Resources πριν το λεξικό της κλωνοποιηθεί στον στόχο, αλλιώς το αντίγραφο θα έχανε ό,τι είχε κληρονομήσει. Η επαναρίθμηση και η αντιγραφή της σελίδας στον στόχο καλύπτονται στο cross-document object deep copy στο PDFlibPas· αυτό το bug κάθισε στην πλευρά πηγής, που οι περισσότεροι υποθέτουν ότι ένα αντίγραφο μόνο διαβάζει

Η έξοδος δεν το έδειχνε ποτέ. Κοινά ή αντιγραμμένα, οι υλοποιημένες τιμές σειριοποιούνται πανομοιότυπα, οπότε και τα δύο έγγραφα αποθηκεύονταν byte-προς-byte το ίδιο πριν και μετά τη διόρθωση. Μόνο μια αλλαγή στο έγγραφο πηγής μετά την αντιγραφή αποκάλυπτε το alias:

procedure CheckSourceSurvivesCopy;
var
  Lib: TPDFlib;
  SourceID, TargetID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Check(Lib.LoadFromString(BuildPdf([
      '<< /Type /Catalog /Pages 2 0 R >>',
      '<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 ' +
        '/MediaBox [0 0 400 300] /Resources << >> >>',
      '<< /Type /Page /Parent 2 0 R /Contents 5 0 R >>',
      '<< /Type /Page /Parent 2 0 R /Contents 6 0 R >>',
      StreamObj('1 w'), StreamObj('2 w')]), '') = 1, 'load failed');
    SourceID := Lib.SelectedDocument;

    TargetID := Lib.NewDocument;             // γίνεται το επιλεγμένο έγγραφο
    Check(Lib.CopyPageRanges(SourceID, '1') = 1, 'copy failed');

    Lib.SelectDocument(SourceID);
    Lib.SelectPage(1);
    Lib.SetPageBox(2, 50, 250, 100, 100);    // στένεψε μόνο το CropBox
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'MediaBox followed CropBox');
    Lib.SetPageBox(1, 0, 200, 200, 200);

    Lib.SelectPage(2);
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'sibling page resized');

    Lib.SelectDocument(TargetID);            // το αντίγραφο κρατά το αρχικό του μέγεθος
    Lib.SelectPage(Lib.PageCount);
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'copied page resized');
  finally
    Lib.Free;
  end;
end;

Πριν το v3.539.36 και οι δύο σελίδες εδώ κληρονομούσαν τον άμεσο MediaBox του ριζικού κόμβου, το αντίγραφο σύναπτε εκείνο το instance στη σελίδα πηγής 1, και το σύναπτε ξανά ως CropBox της σελίδας 1. Το στένεμα του CropBox στένωνε επομένως το MediaBox, και η αλλαγή μεγέθους του MediaBox αλλοίωνε τη σελίδα 2 μέσω του ριζικού κόμβου. Ροές εργασίας που αντιγράφουν σελίδες έξω και μετά συνεχίζουν να επεξεργάζονται την πηγή, όπως το collating duplex σαρώσεων σε ένα PDF πριν το κόψιμο των πρωτοτύπων, είναι εκεί όπου αυτό εμφανιζόταν

Γιατί το aliasing instances είναι τόσο δύσκολο να τεσταριστεί;

Το aliasing instances είναι δύσκολο να τεσταριστεί επειδή η παρατηρήσιμη επίδραση χρειάζεται τρία βήματα σε συγκεκριμένη σειρά: δημιουργήστε το alias, μεταβάλλετε τη μία πλευρά, μετά ελέγχετε την άλλη πλευρά πριν οτιδήποτε άλλο την αγγίξει. Τα περισσότερα tests κάνουν μόνο το πρώτο βήμα και συγκρίνουν αποθηκευμένη έξοδο, που είναι πανομοιότυπη με ή χωρίς το alias

Η παγίδα σειράς στο PDFlibPas είναι το SelectPage. Η επιλογή σελίδας εφαρμόζει ξανά την τρέχουσα γραμματοσειρά μέσω SelectFont, που καταχωρεί εκείνη τη γραμματοσειρά στους πόρους της σελίδας. Μια σελίδα χωρίς δικό της /Resources λύνεται στο λεξικό του γονέα της, οπότε η απλή επιλογή μιας τέτοιας σελίδας προσθέτει νόμιμα /Font στον κόμβο Pages. Στο test MovePage παραπάνω, η επιλογή της πρώην σελίδας 2 προσθέτει την εγγραφή Helvetica στον κόμβο 3, που είναι σωστή συμπεριφορά και όχι διαρροή. Γι αυτό ο έλεγχος GetObjectToString(3) τρέχει πριν το SelectPage(1)· αλλάξτε τα δύο και το test αποτυγχάνει σε διορθωμένο build

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

Λίστα ελέγχου για κώδικα μοντέλου objects PDF

Τα μαθήματα γενικεύουν σε κάθε μοντέλο PDF objects χτισμένο πάνω σε pool και pointer containers, σε Delphi ή αλλού:

  • Όταν υλοποιείτε κληρονομημένες ιδιότητες κατά το ISO 32000-1 §7.7.3.4, κάντε deep-copy τις άμεσες τιμές και κρατήστε τις indirect references ως νέες references στο ίδιο object
  • Ποτέ μην κάνετε Add υπάρχον instance σε δεύτερο container εκτός αν ο μοιρασμός είναι σκόπιμος και τεκμηριωμένος· η ιδιοκτησία από pool σημαίνει ότι το runtime δεν θα παραπονεθεί ποτέ
  • Επεξεργαστείτε στη θέση του μόνο ό,τι ο τρέχων κόμβος κατέχει ως direct object· αντικαθιστά indirect ή κληρονομημένες τιμές με φρέσκο direct object (copy-on-write)
  • Προεπιλεγμένες τιμές που παράγονται από άλλη εγγραφή, όπως CropBox από MediaBox, θέλουν δικό τους instance
  • Τεστάρετε το aliasing με αλληλουχίες μεταβολής και ελέγχου στον άλλο κάτοχο, και ελέγχετε τη σειρά κλήσεων που μπορεί να γράφουν νόμιμα εν τω μεταξύ
  • Η σύγκριση αποθηκευμένης εξόδου δεν αποδεικνύει τίποτα εδώ: κοινές και αντιγραμμένες τιμές σειριοποιούνται πανομοιότυπα μέχρι την πρώτη αλλαγή
  • Στο PDFlibPas, αναβαθμίστε σε v3.539.36 ή νεότερο αν καλείτε MovePage, CollateDocumentsEx, BalancePageTree ή CopyPageRanges και μετά επεξεργάζεστε page boxes ή σχεδιάζετε σε σελίδες

Το PDFlibPas εκθέτει επεξεργασία page tree, αντιγραφή μεταξύ εγγράφων και έλεγχο page boxes μέσα από μία κλάση TPDFlib για Delphi, C++Builder και Free Pascal. Δείτε τη σελίδα προϊόντος PDFlibPas Delphi PDF library για εκδόσεις, πλατφόρμες και την πλήρη αναφορά API