Για τη μείωση του μεγέθους αρχείου PDF στο Delphi, η losLab PDF Library παρέχει τρία API που αντιμετωπίζουν τις τρεις μεγαλύτερες πηγές διόγκωσης: Η SubsetEmbeddedFonts ξαναγράφει κάθε ενσωματωμένο πρόγραμμα γραμματοσειράς TrueType περιορίζοντάς το στις γλύφες που πραγματικά αποδίδει το έγγραφο, η DownsampleImages επαναδειγματοληπτεί ψηφιογραφικές (raster) εικόνες που υπερβαίνουν ένα στοχευμένο DPI, και η NormalizeLZWStreams αντικαθιστά την παλαιά συμπίεση LZWDecode με FlateDecode. Κάθε μία επιστρέφει τον αριθμό των αντικειμένων που τροποποίησε, επομένως το μηδέν σάς ενημερώνει ότι το πέρασμα ήταν μια ανενεργή λειτουργία και όχι μια σιωπηρή αποτυχία
Γιατί το συγχωνευμένο PDF μου είναι μεγαλύτερο από τα αρχεία προέλευσής του;
Ένα συγχωνευμένο ή προγραμματιστικά παραγόμενο PDF είναι συνήθως υπερμεγέθες για έναν από τρεις λόγους: πλήρως ενσωματωμένες γραμματοσειρές, εικόνες με δειγματοληψία πολύ πάνω από την ανάλυση εμφάνισής τους, και ροές που εξακολουθούν να είναι συμπιεσμένες με το παλαιό φίλτρο LZW. Το ISO 32000-1 §9.9 επιτρέπει σε έναν παραγωγό να ενσωματώσει το πλήρες πρόγραμμα γραμματοσειράς, και οι περισσότεροι παραγωγοί κάνουν ακριβώς αυτό επειδή είναι η ασφαλής προεπιλογή. Μια πλήρης Arial FontFile2 φτάνει σε εκατοντάδες kilobytes· ενσωματώστε την σε μια δωδεκάδα αρχείων προέλευσης, συγχωνεύστε τα, και θα μεταφέρετε μια δωδεκάδα αντίγραφα περιγραμμάτων γλυφών για χαρακτήρες που κανείς δεν πληκτρολόγησε. Η ίδια η συγχώνευση δεν δημιουργεί τη σπατάλη, απλώς τη συγκεντρώνει σε ένα μόνο αρχείο όπου το σύνολο γίνεται τελικά ορατό
Οι εικόνες είναι ο δεύτερος ένοχος. Μια σάρωση πλάτους 4800 pixel τοποθετημένη σε ένα πλαίσιο τετάρτου σελίδας στέλνει περίπου 40 φορές περισσότερα δεδομένα εικονοστοιχείων από όσα μπορεί να χρησιμοποιήσει μια ροή εκτύπωσης 300 DPI. Ο τρίτος είναι πιο αθόρυβος: ροές που φιλτράρονται με LZWDecode. Το ISO 32000-1 §7.4.4 καθορίζει τόσο το LZWDecode όσο και το FlateDecode, και σημεύει ότι το Flate συνήθως συμπιέζει τουλάχιστον εξίσου καλά· στην πράξη, η έξοδος Flate είναι σταθερά μικρότερη στα ίδια δεδομένα, και το LZW επιβιώνει κυρίως σε αρχεία που πέρασαν από εργαλεία της δεκαετίας του 1990 σε κάποιο σημείο της ιστορίας τους. Το υπόλοιπο αυτού του άρθρου περιγράφει τα τρία περάσματα της losLab PDF Library που διορθώνουν κάθε πρόβλημα, και στη συνέχεια τα συνδυάζει σε μία ροή εργασίας (pipeline)
Δημιουργία υποσυνόλου γραμματοσειρών με το SubsetEmbeddedFonts
Η SubsetEmbeddedFonts συρρικνώνει κάθε ενσωματωμένη γραμματοσειρά TrueType σε ένα φορτωμένο έγγραφο στους χαρακτήρες που χρησιμοποιεί πραγματικά το έγγραφο, και δεν χρειάζεται ορίσματα επειδή αντλεί τη λίστα διατήρησης από τις ίδιες τις ροές περιεχομένου. Εσωτερικά, το πέρασμα διατρέχει τη ροή περιεχομένου κάθε σελίδας με την GetTextRuns, συλλέγει τους κωδικούς χαρακτήρων που αναφέρονται σε κάθε πόρο γραμματοσειράς, δημιουργεί μια λίστα διατήρησης, και παραδίδει το αρχικό πρόγραμμα γραμματοσειράς στη μηχανή FontSub των Windows (CreateFontPackage) για την παραγωγή ενός υποσυνόλου. Το ξαναγραμμένο πρόγραμμα αντικαθιστά τη ροή FontFile2 επί τόπου, και το όνομα BaseFont αποκτά μια ετικέτα LOSABC+ tag, τη σύμβαση έξι κεφαλαίων γραμμάτων και συμβόλου συν που ορίζει το ISO 32000-1 §9.6.4 για τις γραμματοσειρές υποσυνόλου. Αυτό το πρόθεμα είναι επίσης που καθιστά την κλήση ταυτοδύναμη: εκτελέστε το πέρασμα δύο φορές και οι ήδη υποσυνοδοποιημένες γραμματοσειρές αναγνωρίζονται και παρακάμπτονται, οπότε η ενσωμάτωσή της σε μια μαζική εργασία που μπορεί να επανεξετάσει αρχεία είναι ασφαλής
Δύο λεπτομέρειες υλοποίησης αξίζει να γνωρίζετε επειδή εξηγούν τα όρια του API. Πρώτον, το πέρασμα στοχεύει στο FontFile2, οπότε καλύπτει ενσωματωμένα προγράμματα TrueType· οι γραμματοσειρές που έχουν ενσωματωθεί ως Type 1 ή γυμνό CFF παραμένουν ανέπαφες αντί να διακινδυνεύσουν αλλαγές. Δεύτερον, βασίζεται στο FontSub, γεγονός που καθιστά το SubsetEmbeddedFonts διαθέσιμο μόνο για Windows. Ένα πιο λεπτό σημείο από την υλοποίηση: το αν μια γραμματοσειρά πληροί τις προϋποθέσεις αποφασίζεται με την πραγματική επίλυση της αλυσίδας αναφορών FontDescriptor → FontFile2, όχι με την εμπιστοσύνη σε μια ευρετική σημαία ενσωμάτωσης (embedded-flag heuristic), επειδή οι γραμματοσειρές σε ένα φορτωμένο έγγραφο δεν πέρασαν ποτέ από τη λογιστική τήρηση της πλευράς δημιουργίας που θέτει τέτοιες σημαίες. Εάν η επιλυμένη ροή υπάρχει, η γραμματοσειρά είναι υποψήφια· εάν όχι, παρακάμπτεται χωρίς σφάλμα
Η ειλικρινής ανταλλαγή: μια γραμματοσειρά υποσυνόλου περιέχει μόνο τις γλύφες που ήταν παρούσες κατά τη στιγμή της δημιουργίας του υποσυνόλου. Εάν ένα κατάντη εργαλείο (downstream tool), ή ο δικός σας κώδικας, προσθέσει αργότερα κείμενο στην ίδια γραμματοσειρά, οποιοσδήποτε χαρακτήρας εκτός του υποσυνόλου δεν θα έχει περίγραμμα και θα εμφανίζεται ως ελλιπής γλύφος. Δημιουργήστε υποσύνολο ως το τελευταίο βήμα αλλαγής περιεχομένου, ποτέ πριν από ένα στάδιο επεξεργασίας. Η ίδια προσοχή απαιτείται εάν σχεδιάζετε να εξαγάγετε τη γραμματοσειρά αργότερα για επαναχρησιμοποίηση· το άρθρο σχετικά με την εξαγωγή κειμένου, εικόνων και γραμματοσειρών με το PDFlibPas καλύπτει τι μπορεί και τι δεν μπορεί να σας δώσει ένα εξαγόμενο πρόγραμμα υποσυνόλου
Πώς αποφασίζει το DownsampleImages ποιες εικόνες θα συρρικνώσει;
Το DownsampleImages(MaxDPI, Quality, Filter) επαναδειγματοληπτεί μόνο τις εικόνες που μπορεί με βεβαιότητα να χαρακτηρίσει ως υπερδειγματοληπτημένες, χρησιμοποιώντας μια σκόπιμα συντηρητική εκτίμηση DPI. Ένα αντικείμενο εικόνας PDF (image XObject) αποθηκεύει διαστάσεις εικονοστοιχείων αλλά όχι αξιόπιστη φυσική ανάλυση, και οποιαδήποτε ετικέτα DPI από την εικόνα προέλευσης σπάνια επιβιώνει από έναν κύκλο φόρτωσης-επεξεργασίας-αποθήκευσης. Έτσι, το πέρασμα εκτιμά το SrcDPI = PixelWidth / 8.5, ρωτώντας ουσιαστικά: εάν αυτή η εικόνα κάλυπτε το πλήρες πλάτος μιας σελίδας Letter, ποια θα ήταν η ανάλυσή της; Μόνο οι εικόνες των οποίων η εκτίμηση υπερβαίνει το MaxDPI επηρεάζονται. Η μεροληψία είναι σκόπιμη: μια εικόνα που τοποθετείται μικρή στη σελίδα έχει πραγματικό DPI υψηλότερο από την εκτίμηση, οπότε το πέρασμα υπο-ενεργοποιείται αντί να υποβαθμίσει ένα στοιχείο ποιότητας εκτύπωσης που δεν μπορεί να μετρήσει
Η Quality από 1 έως 100 επιλέγει την ποιότητα επανακωδικοποίησης JPEG, ενώ το 0 διατηρεί την έξοδο ως Flate χωρίς απώλειες τύπου PNG· το Filter επιλέγει τον πυρήνα επαναδειγματοληψίας, 0 για μέσο όρο πλαισίου (box average) και 1 για διγραμμικό (bilinear). Για σαρωμένα έγγραφα γραφείου, το DownsampleImages(150, 75, 1) είναι ένα λογικό σημείο εκκίνησης· για οτιδήποτε μπορεί να επανεκτυπωθεί, αυξήστε το MaxDPI σε 300 ή παρακάμψτε εντελώς το πέρασμα. Η υποδειγματοληψία είναι το μόνο βήμα με απώλειες από τα τρία, οπότε ανήκει πίσω από μια ρύθμιση που οι χρήστες σας μπορούν να απενεργοποιήσουν
Μετατροπή παλαιών ροών LZW με το NormalizeLZWStreams
Το NormalizeLZWStreams είναι η δωρεάν νίκη: αποσυμπιέζει χωρίς απώλειες κάθε ροή LZWDecode και την επανασυμπιέζει με FlateDecode, επί τόπου, επιστρέφοντας τον αριθμό των ροών που μετατράπηκαν. Χειρίζεται τόσο μια μεμονωμένη καταχώριση /Filter /LZWDecode όσο και την εμφάνιση LZW μέσα σε έναν πίνακα αλυσίδας φίλτρων, όπου αντικαθίσταται μόνο ο σύνδεσμος LZW και η υπόλοιπη αλυσίδα διατηρείται. Οι παράμετροι πρόβλεψης (Predictor, Columns, Colors, BitsPerComponent) διαβάζονται από τις DecodeParms της ροής και μεταβιβάζονται στον αποσυμπιεστή, έτσι ώστε τα δεδομένα εικόνας που έχουν κωδικοποιηθεί με πρόβλεψη να πραγματοποιούν σωστά τον κύκλο τους. Επειδή και τα δύο φίλτρα είναι κωδικοποιητές ακριβείας bit (bit-exact), τα αποσυμπιεσμένα bytes είναι πανομοιότυπα πριν και μετά· αλλάζει μόνο η συμπίεση του κοντέινερ, γι' αυτό και αυτό το πέρασμα είναι ασφαλές να εκτελείται ανεπιφύλακτα σε κάθε αρχείο
Σε ένα έγγραφο χωρίς ροές LZW, η κλήση απλώς επιστρέφει 0 και δεν αγγίζει τίποτα, κάτι που η σουίτα ελέγχου παλινδρόμησης της βιβλιοθήκης ελέγχει ρητά: ένα νεοδημιουργηθέν αρχείο μόνο με Flate πρέπει να αναφέρει μηδενικές μετατροπές. Αυτή η εγγύηση ανενεργής λειτουργίας (no-op) έχει σημασία όταν το πέρασμα βρίσκεται σε μια ροή εργασίας που επεξεργάζεται χιλιάδες ετερογενή αρχεία, άλλα από το 2024 και άλλα από το 1998
Η πλήρης ροή εργασίας βελτιστοποίησης μεγέθους στο Delphi
Τα τρία περάσματα συνδυάζονται σε μια ενιαία συνάρτηση φόρτωσης-βελτιστοποίησης-αποθήκευσης, και η σειρά έχει μικρότερη σημασία από ό,τι θα αναμένατε, επειδή λειτουργούν σε διαφορετικούς τύπους αντικειμένων: γραμματοσειρές, αντικείμενα εικόνας (image XObjects) και φίλτρα ροής. Η εκτέλεση της δημιουργίας υποσυνόλου πρώτα παραμένει η πιο τακτοποιημένη επιλογή, καθώς είναι το πέρασμα με περιορισμό στη σειρά επεξεργασίας
function OptimizePDF(const Src, Dst: string): Boolean;
var
Lib: TPDFlib;
Fonts, Images, Streams: Integer;
begin
Result := False;
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(Src, '') <> 1 then
Exit;
Fonts := Lib.SubsetEmbeddedFonts; // TrueType FontFile2 -> subset
Images := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinear
Streams := Lib.NormalizeLZWStreams; // LZWDecode -> FlateDecode
Result := Lib.SaveToFile(Dst) = 1;
// Log Fonts/Images/Streams: three zeros mean the file was already lean
finally
Lib.Free;
end;
end;
Επαληθεύστε τη ροή εργασίας με τον τρόπο που η βιβλιοθήκη επαληθεύει τον εαυτό της: με επιστροφή (round-trip). Οι δοκιμές παλινδρόμησης v3.130 δημιουργούν ένα έγγραφο, το αποθηκεύουν, το επαναφορτώνουν, εκτελούν τη βελτιστοποίηση, το αποθηκεύουν ξανά και στη συνέχεια επιβεβαιώνουν τρία πράγματα: το αποτέλεσμα είναι μικρότερο, οι επιστρεφόμενοι αριθμοί ταιριάζουν με τις προσδοκίες, και μια επαναφόρτωση του βελτιστοποιημένου αρχείου εξακολουθεί να αναλύεται και να αποδίδεται σωστά. Η αναπαραγωγή αυτού του βρόχου δημιουργίας-βελτιστοποίησης-επαναφόρτωσης σε ένα δείγμα των δικών σας αρχείων παραγωγής, και η σύγκριση του εξαγόμενου κειμένου πριν και μετά, είναι μια επένδυση μιας ώρας που προλαμβάνει σφάλματα ενσωμάτωσης πολύ πριν ένας πελάτης ανοίξει ένα κατεστραμμένο τιμολόγιο
// Round-trip check: the optimized file must still load cleanly
Lib := TPDFlib.Create;
try
Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
Assert(Lib.GetPageCount > 0);
finally
Lib.Free;
end;
Πού ταιριάζει η ροή εργασίας σε μια διαδικασία συγχώνευσης; Μετά τη συγχώνευση, όχι κατά τη διάρκειά της. Η συγχώνευση πρώτα και η βελτιστοποίηση του ενιαίου αποτελέσματος σημαίνει ότι κάθε ενσωματωμένη γραμματοσειρά υποβάλλεται σε δημιουργία υποσυνόλου μία φορά έναντι της ένωσης όλων των χρησιμοποιούμενων χαρακτήρων, αντί για κάθε αρχείο προέλευσης ξεχωριστά. Εάν η ταχύτητα συγχώνευσης είναι το σημείο συμφόρησης, το PDFlibPas προσφέρει μια γρήγορη διαδρομή σε επίπεδο byte που αποφεύγει την πλήρη ανάλυση αντικειμένων, η οποία περιγράφεται στο άρθρο σχετικά με τη γρήγορη συγχώνευση PDF με μετατόπιση αναφοράς byte· και για εισόδους πολύ μεγάλες για να χωρέσουν πλήρως στη μνήμη, η άμεση πρόσβαση συγχώνευσης και διαχωρισμού για μεγάλα PDF καλύπτει τη διαδρομή ροής. Και τα δύο συνδυάζονται φυσικά με ένα τελικό πέρασμα βελτιστοποίησης στο συγχωνευμένο αποτέλεσμα
Τι δεν θα κάνουν τα τρία περάσματα
Η τριάδα βελτιστοποίησης της losLab PDF Library αποκλείει σκόπιμα οτιδήποτε αλλάζει τη σημασιολογία του εγγράφου. Η SubsetEmbeddedFonts δεν ενοποιεί διπλότυπες γραμματοσειρές σε συγχωνευμένες πηγές σε ένα πρόγραμμα, αλλά συρρικνώνει την καθεμία ανεξάρτητα· η κατάργηση διπλοτύπων (deduplication) είναι ένας διαφορετικός, πιο επικίνδυνος μετασχηματισμός. Το DownsampleImages θα προσπεράσει μια εικόνα της οποίας η συντηρητική εκτίμηση DPI παραμένει κάτω από το όριο, ακόμη και όταν ένας άνθρωπος θα μπορούσε να καταλάβει ότι είναι υπερμεγέθης για το πλαίσιό της. Και κανένα από τα περάσματα δεν αγγίζει τη δομή του εγγράφου, επομένως ένα αρχείο διογκωμένο από χιλιάδες ορφανά αντικείμενα χρειάζεται μια αποθήκευση τύπου επανεγγραφής (rewrite-style save) αντί για αυτά τα περάσματα σε επίπεδο ροής. Μέσα σε αυτά τα όρια, ο συνδυασμός δημιουργίας υποσυνόλου γραμματοσειρών, υποδειγματοληψίας εικόνων και κανονικοποίησης LZW σε Flate αφαιρεί τις τρεις κλασικές πηγές διόγκωσης PDF με μία προβλέψιμη κλήση API για την καθεμία. Οι τρεις λειτουργίες αποστέλλονται ως μέρος της losLab PDF Library για Delphi, C# και VB.NET, μαζί με τα API συγχώνευσης, εξαγωγής και απόδοσης που συζητήθηκαν παραπάνω