Το PDF Library for Delphi αφαιρεί έναν τελεστή identity text matrix, 1 0 0 1 0 0 Tm, κατά τη βελτιστοποίηση peephole του content stream την ώρα της αποθήκευσης μόνο όταν η text matrix και η text line matrix είναι ήδη identity: αμέσως μετά BT, ή αμέσως μετά ένα προγενέστερο identity Tm. Ένα identity cm πετιέται ακόμα πάντα, επειδή το cm πολλαπλασιάζει τον CTM ενώ το Tm αντικαθιστά και τις δύο text matrices ολοκληρωτικά. Από το v3.539.28 κάθε άλλο identity Tm μένει στο stream
Το bug που αυτό διορθώνει είναι το ήσυχο είδος. Μια γεννήτρια αναφορών εκπέμπει BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET, βασιζόμενη στο identity Tm να στείλει τη δεύτερη συμβολοσειρά πίσω στην αρχή του text space πριν εφαρμόσει τη δική της λογική τοποθέτησης. Ο παλιότερος optimizer είδε έξι αριθμούς που γράφουν τον identity matrix, αποφάσισε πως ο τελεστής δεν μπορεί να αλλάξει τίποτα, και τον σβήσε. Τίποτα δεν απέτυχε, τίποτα δεν κατέγραψε προειδοποίηση, και η αποθηκευμένη σελίδα σχεδίασε «Total» αμέσως μετά το «Invoice» στην ίδια baseline, που είναι ακριβώς η κατηγορία ελαττώματος που κανείς δεν προσέχει μέχρι ένας πελάτης να τυπώσει το PDF
Γιατί το 1 0 0 1 0 0 Tm δεν είναι πάντα no-op;
Ένα identity Tm είναι no-op μόνο όταν θα αντικαθιστούσε δύο matrices που κρατούν ήδη την ταυτότητα, και αυτό είναι ιδιότητα των τελεστών πριν από αυτό, όχι των δικών του τελεστών. Το ISO 32000-1 §9.4.1 λέει ότι το BT αρχικοποιεί και την text matrix (Tm) και την text line matrix (Tlm) στην ταυτότητα, και το §9.4.2 ορίζει το Tm ως το να θέτει και τις δύο στις δοσμένες τιμές, όχι να κάνει concatenation πάνω τους. Συγκρίνετε αυτό με το cm (§8.4.4), που δεξιοπολλαπλασιάζει τον current transformation matrix: ο πολλαπλασιασμός με την ταυτότητα αφήνει κάθε CTM αμετάβλητο, οπότε το 1 0 0 1 0 0 cm είναι ασφαλές να σβηστεί οπουδήποτε. Μέσα σε ένα text object η εικόνα είναι διαφορετική. Τα Td, TD, T* και ένα μη identity Tm μετακινούν όλα το Tlm, και κάθε text-showing τελεστής (Tj, TJ, ', ") προωθεί το Tm κατά το πλάτος των glyphs που ζωγράφισε. Μετά από οποιονδήποτε από αυτούς, ένα identity Tm είναι μια γνήσια επαναφορά στην αρχή. Αν έχειτε ποτέ ιχνηλατήσει θέσεις κειμένου στο χέρι με τον state tracker CTM και text matrix του content stream, αυτή είναι η ίδια διάκριση μεταξύ concatenation κατάστασης και αντικατάστασής της
Πώς η αντίστροφη σάρωση αποφασίζει ποιο identity Tm σβήνει
Το TPDFContentPeepholeOptimizer.RemoveIdentityMatrices τώρα περπατά προς τα πίσω από κάθε identity Tm και το σβήνει μόνο αν η σάρωση φτάσει πρώτα BT ή άλλο identity Tm. Το προγενέστερο identity Tm μετράει είτε κρατείται είτε μόλις προγραμματίστηκε το ίδιο για διαγραφή, επειδή και στις δύο περιπτώσεις άφησε και τις δύο matrices στην ταυτότητα, ακριβώς όπως κάνει το BT. Ο κανόνας ταξινομεί κάθε τελεστή που μπορεί να συναντήσει σε μία από δύο ομάδες:
- Σταματά και κρατά το Tm:
Td,TD,T*, ένα μη identityTm,Tj,TJ,',",ET, κάθε τελεστή που ο parser δεν αναγνωρίζει, ή την αρχή του stream - Προσπερνά και συνεχίζει τη σάρωση: τελεστές που δεν αγγίζουν ποτέ Tm ή Tlm, όπως
Tf,Tc, color setters,gs, marked-content τελεστές καιcm
Οι συντηρητικές περιπτώσεις είναι επίτηδες. Ένας άγνωστος τελεστής θα μπορούσε να είναι οτιδήποτε, οπότε η σάρωση αρνείται να συλλογιστεί πέρα από αυτόν. Το ET κλείνει το text object, οπότε ένα Tm μετά από αυτό δεν έχει BT να εγγυηθεί τις τιμές των matrices. Η σάρωση δουλεύει επίσης ένα content stream τη φορά, που έχει σημασία για σελίδες των οποίων το /Contents είναι πίνακας: ένα layer που ξεκινά στη μέση ενός text object, χωρίς δικό του BT, κρατά το identity Tm του ακόμα κι όταν το προηγούμενο layer θα το είχε κάνει περιττό. Κοστίζει μερικά bytes σε περίεργα αρχεία και δεν μετακινεί ποτέ glyph. Αν επεξεργάζεστε κείμενο σελίδας σε επίπεδο οδηγίας, όπως στο walkthrough αντιστοίχισης χαρακτήρα σε byte του content, το ίδιο parsed μοντέλο TPDFContentProgram είναι αυτό που ξαναγράφει ο optimizer
uses
PDFlibContentModel, PDFlibContentOptimize;
function OptimizeSnippet(const Source: AnsiString): AnsiString;
var
Prog: TPDFContentProgram;
Optimizer: TPDFContentPeepholeOptimizer;
begin
Result := Source;
Prog := TPDFContentProgram.Create;
try
if not Prog.Parse(Source) then
Exit; // χαλασμένο stream: άφησε τα bytes στην ησυχία τους
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // επιστρέφει τον αριθμό των οδηγιών που αφαιρέθηκαν
finally
Optimizer.Free;
end;
Result := Prog.Emit; // μία οδηγία ανά γραμμή
finally
Prog.Free;
end;
end;
// Αφαιρέθηκε: Tm απευθείας μετά το BT, το δεύτερο από δύο identity Tm στη σειρά
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Κρατήθηκε: Tm μετά από Td, μετά από Tj, μετά από μη identity Tm, ή έξω από BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Τρέξτε τον helper στο invoice stream από την αρχή του άρθρου και το identity Tm επιζεί, επειδή η αντίστροφη σάρωση πέφτει σε Tj πριν φτάσει το BT. Βάλτε /F1 12 Tf, 2 Tc και 0 g ανάμεσα στο BT και το identity Tm και πάλι φεύγει, αφού κανένας από αυτούς δεν αγγίζει τις text matrices. Μια αλληλουχία όπως BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm χάνει ακριβώς έναν τελεστή: το πρώτο identity Tm επαναφέρει τη matrix που μετακίνησε το Td, και μόνο το δεύτερο είναι περιττό
Πότε τρέχει πραγματικά ο peephole optimizer;
Ο optimizer τρέχει μόνο κατά το πέρασμα συμπίεσης, μέσα στο TPDFPageTree.Compress, και μόνο σε content streams που δεν είναι ήδη Flate-συμπιεσμένα. Το TPDFlib.SetOptimizeContentStreams(1) είναι το default, και ο ίδιος διακόπτης εκτίθεται ως το πεδίο OptimizeContentStreams του TPDFlibSaveOptions· τόσο το CompressContent όσο και το CompressPage τον σέβονται. Ένα stream του οποίου το /Filter είναι ήδη /FlateDecode παραλείπεται ολότελα, οπότε το άνοιγμα ενός υπάρχοντος συμπιεσμένου PDF και η ξανααποθήκευσή του δεν ξαναγράφει τους τελεστές του. Αν το stream αποτύχει να γίνει parse, τα πρωτότυπα αποκωδικοποιημένα bytes συμπιέζονται αμετάβλητα. Το TPDFlib.NormalizeContentStreams κάνει parse και ξαναεκπέμπει content με κανονικοποιημένα διαστήματα και αριθμούς αλλά δεν καλεί ποτέ τον optimizer, που το κάνει χρήσιμο baseline όταν θέλετε να δείτε πόση διαφορά μεγέθους συνεισφέρουν οι peephole κανόνες, δίπλα στα μεγαλύτερα κέρδη που καλύπτονται στο βελτιστοποίηση μεγέθους PDF αρχείων με font subsetting
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Τα ασυμπίεστα streams περνούν από τους peephole κανόνες, μετά Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// Η ίδια επιλογή μέσω των bundled save options· το False βγαίνει εκτός
Options.CompressContent := True;
Options.CompressFonts := True;
Options.CompressImages := True;
Options.Linearize := False;
Options.KeepModDate := False;
Options.OptimizeContentStreams := False;
Options.GarbageCollect := False;
Options.PackObjectStreams := True;
Lib.SaveToFileOptions('report-plain.pdf', Options);
finally
Lib.Free;
end;
end;
Τι εγγυόταν πραγματικά το παλιό regression test;
Το παλιό regression test εγγυόταν ένα μόνο σχήμα: ένα identity Tm απευθείας μετά BT αφαιρείται. Το Peephole_RemovesIdentityTextMatrix τροφοδοτεί τον optimizer με BT 1 0 0 1 0 0 Tm (hello) Tj ET και κάνει assert πως δεν μένει κανένα Tm. Μια προγενέστερη έκδοση είχε ήδη επισημάνει ότι το σβήσιμο ενός identity Tm είναι ανασφαλές όταν το Tlm δεν είναι η ταυτότητα, και μετά κράτησε ούτως ή άλλως τη συμπεριφορά επειδή το τεστ την «κλείδωνε». Διαβασμένο προσεκτικά, το τεστ δεν λέει τίποτα για ένα identity Tm μετά από Td ή μετά από εμφανισμένο κείμενο· η μεταχείριση της κάλυψης ενός δείγματος ως του συμβολαίου όλου του κανόνα ήταν το πραγματικό λάθος. Το fix κρατά εκείνη την αρχική περίπτωση πράσινη και προσθέτει έξι περιπτώσεις που καρφώνουν τόσο τα αφαιρήσιμα σχήματα όσο και τα κρατούμενα, συμπεριλαμβανομένου ενός Tm έξω από κάθε text object και ενός που έπεται του ET
Το trade-off είναι εύκολο να το δεχτείτε μόλις γραφτεί. Γεννήτριες που τυλίγουν κάθε text object ως BT 1 0 0 1 0 0 Tm ... εξακολουθούν να παίρνουν τον περιττό τελεστή αφαιρεμένο, και εκεί ερχόταν σχεδόν όλο το κέρδος. Αυτό που παραδίνει ο optimizer είναι το περιστασιακό identity Tm στη μέση ενός text object, μια χούφτα bytes ανά σελίδα πριν τα δει καν το Flate, σε αντάλλαγμα μιας εγγύησης που ο header της μονάδας δηλώνει ξεκάθαρα: κάθε transform είναι output-ισοδύναμο και δεν αλλάζει ποτέ την ορατή σελίδα. Ένας size optimizer που μετακινεί κείμενο δεν είναι optimizer, είναι ένα rendering bug με καλούς λόγους συμπίεσης
Ο parser του content stream, ο peephole optimizer και οι επιλογές συμπίεσης της αποθήκευσης που περιγράφονται εδώ κυκλοφορούν όλοι στο PDF Library for Delphi και C++Builder, που εκθέτει επίσης NormalizeContentStreams, CompressContent και TPDFlibSaveOptions για ρύθμιση του πώς γράφεται κάθε έγγραφο