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

HotPDF CompressDocument: συμπαγή font subsets στο Delphi

Το HotPDF THotPDF.CompressDocument είναι ένας μοναδικός διακόπτης που κάνει το BeginDoc να παράγει το μικρότερο lossless PDF που μπορεί να γράψει το component: FlateDecode στο μέγιστο επίπεδο, ένα cross-reference stream με object streams, font subsetting και συμπαγή font subsets που αριθμούν εκ νέου τα κρατούμενα glyphs πίσω από ένα ρητό /CIDToGIDMap. Το EndDoc μετά βάζει πίσω τις δικές σας ρυθμίσεις. Ένα τεστ εγγράφου τριών σελίδων Arial και SimSun έπεσε από 10.2 MB σε 20 KB με πανομοιότυπη απεικόνιση

Τι ανάβει πραγματικά το CompressDocument;

Το CompressDocument παρακάμπτει έξι ρυθμίσεις του writer, συν το όριο object-stream, για ένα έγγραφο και τις επαναφέρει όλες μετά. Στο BeginDoc, πριν σταθερώσει η έκδοση PDF, το HotPDF καταγράφει τις τιμές σας και θέτει το Compression σε cmFlateDecode, το CompressionLevel σε clMaximum, ανάβει το EnableFontSubsetting και το CompactFontSubsetting, και ενεργοποιεί UseXRefStream συν UseObjectStreams (ISO 32000-1 §7.5.7 και §7.5.8). Τα object streams θέλουν PDF 1.5, οπότε μια παλαιότερη Version ανεβαίνει σε 1.5 όταν δεν είναι κλειδωμένη. Το PDF/A-1 απαγορεύει και τις δύο δομές, οπότε ένα έγγραφο PDF/A-1 κρατά τον κλασικό πίνακα cross-reference και παίρνει μόνο τη δουλειά Flate και γραμματοσειρών. Οι εικόνες μένουν ακριβώς όπως τις ενσωματώσατε

Διάγραμμα του κύκλου ζωής CompressDocument του HotPDF στο Delphi: το BeginDoc καταγράφει τις δικές του τιμές του writer, παρακάμπτει έξι ρυθμίσεις συμπεριλαμβανομένων των Compression και UseObjectStreams για ένα έγγραφο, και το EndDoc επαναφέρει κάθε δανεική τιμή στο εξώτατο finally του ενώ η ιδιότητα CompressDocument μένει True
Έξι ρυθμίσεις του writer και το όριο object-stream δανείζονται για ακριβώς ένα έγγραφο και παραδίδονται πίσω όταν τρέχει το EndDoc, οπότε μια αποτυχημένη αναφορά δεν αφήνει ποτέ το component κολλημένο στη μέγιστη συμπίεση
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // εφαρμόζεται από το BeginDoc, αναιρείται από το EndDoc
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Η επαναφορά γίνεται στο εξώτατο finally του EndDoc, οπότε ένα exception στη μέση μιας αναφοράς δεν αφήνει ένα μαχόμενο component κολλημένο στη μέγιστη συμπίεση για την επόμενη δουλειά. Η ιδιότητα CompressDocument η ίδια μένει True· μόνο οι έξι ρυθμίσεις που δάνεικε γυρνούν πίσω. Η έκδοση μεταχειρίζεται με περισσότερη προσοχή. Το HotPDF αναιρεί το δικό του ανέβασμα σε 1.5 μόνο αν το έγγραφο εξακολουθεί να τελειώνει στο 1.5, οπότε όταν άλλο χαρακτηριστικό σπρώχνει το αρχείο σε 1.6 κατά τη διάρκεια του τρεξίματος (ενσωματωμένη γραμματοσειρά OpenType, ας πούμε), η υψηλότερη έκδοση μένει, ακριβώς όπως θα είχε χωρίς συμπίεση

Γιατί τα font subsets μένουν μεγάλα χωρίς συμπύκνωση;

Ένα κλασικό TrueType subset ρίχνει τα outlines που δεν ζωγραφίζετε ποτέ αλλά κρατά κάθε glyph ID εκεί που ήταν, και αυτή η αρίθμηση είναι που το κρατά βαρύ. Το content stream δείχνει CIDs ίσα με τα πρωτότυπα GIDs, οπότε το subset πρέπει να κρατά ένα offset loca και μια εγγραφή hmtx για κάθε θέση μέχρι το υψηλότερο glyph που κρατά, κενή ή όχι. Για μια λατινική γραμματοσειρά αυτό το overhead είναι θόρυβος. Για μια CJK γραμματοσειρά όπως η SimSun, της οποίας τα ideographs κάθονται βαθιά σε έναν πολύ μεγάλο πίνακα glyphs, δύο κινεζικοί χαρακτήρες σέρνουν μαζί τους πίνακες με μέγεθος για ολόκληρη τη γραμματοσειρά. Οι κανόνες κλεισίματος font subset για shaped glyphs αποφασίζουν ποια glyphs επιζούν· η συμπύκνωση αφορά το πόσο κοστίζουν οι επιζώντες

Το CompactFontSubsetting αριθμεί εκ νέου τα κρατούμενα glyphs σε ένα πυκνό εύρος ξεκινώντας από το μηδέν και γράφει ένα stream /CIDToGIDMap πάνω στο CIDFont, που το ISO 32000-1 §9.7.4.2 ορίζει ως πίνακα GIDs δύο bytes δεικτοδοτημένο από CID. Εκείνος ο πίνακας είναι όλο το κόλπο. Τα content streams, ο πίνακας πλατών /W και το CMap ToUnicode κρατούν όλα τα πρωτότυπα CIDs, οπότε τίποτα ήδη γραμμένο δεν χρειάζεται να αλλάξει· μόνο η αναζήτηση από CID σε glyph μετακομίζει στον χάρτη. Στο τεστ που γέννησε το χαρακτηριστικό, η SimSun με δύο χαρακτήρες πήγε από 24.8 KB δεδομένα γραμματοσειράς σε 3.1 KB

Σύγκριση ενός αραιού font subset του HotPDF, που κρατά εγγραφές loca και hmtx για κάθε πρωτότυπο glyph ID μέχρι το υψηλότερο κρατούμενο GID, με την έξοδο CompactFontSubsetting που αριθμεί πυκνά τα κρατούμενα glyphs από το μηδέν και χαρτογραφεί τα CIDs μέσω ενός stream CIDToGIDMap ενώ τα content streams, το /W και το ToUnicode μένουν αμετάβλητα
Η εκ νέου αρίθμηση μετακινεί το κόστος έξω από το font program και σε ένα μικρό map stream — δύο χαρακτήρες SimSun έπεσαν από 24.8 KB σε 3.1 KB χωρίς να αγγίξει ούτε ένα byte ήδη γραμμένου content

Η συμπύκνωση έχει σφιχτά όρια, και υποβαθμίζεται αθόρυβα αντί να αποτυγχάνει. Το HotPDF χτίζει συμπαγή subsets μόνο για faces Type 0 TrueType, τόσο αυτά που ορίζονται μέσω SetFont με αναμμένο subsetting όσο και το face που καταχωρείται μέσω RegisterUnicodeTTF. Μια απλή γραμματοσειρά TrueType βρίσκει τα glyphs της μέσω του cmap μέσα στο font program, που η εκ νέου αρίθμηση θα χαλούσε, οπότε κρατά το αραιό subset. Τα faces OpenType-CFF δεν έχουν ούτε αυτά compact διαδρομή. Ένα συμπαγές build που αποτυγχάνει γυρνά στο αραιό subset αντί να πετάει exception. Η ιδιότητα είναι σβηστή by default, οπότε η υπάρχουσα έξοδος μένει byte-πανομοιότυπη, ενώ κάτω από PDF/A το καταχωρημένο Unicode face παίρνει πάντα συμπαγές subset

Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True;   // χρήσιμο χωρίς CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;

Πώς σφίγγει ο packed writer τη δομή του αρχείου;

Μόλις οι γραμματοσειρές και τα streams μικρύνουν, τα dictionaries και τα δεδομένα cross-reference γίνονται το μεγαλύτερο υπόλοιπο κόστος, οπότε ο object-stream writer πίσω από το CompressDocument κόβει κι αυτά. Ο οδηγός για object streams και incremental updates καλύπτει τη μορφή container την ίδια· η διαδρομή συμπίεσης προσθέτει τέσσερις βελτιώσεις από πάνω:

  • Συμπαγής σύνταξη κατά το ISO 32000-1 §7.2.2: κενό γράφεται μόνο ανάμεσα σε δύο tokens που αλλιώς θα έτρεχαν μαζί ως κανονικοί χαρακτήρες, οπότε το /Type /Page γίνεται /Type/Page
  • Τα πεδία του cross-reference stream παίρνουν οποιοδήποτε πλάτος επιτρέπει το §7.5.8.2, οπότε ένα αρχείο κάτω από 16 MB αποθηκεύει κάθε offset σε 3 bytes αντί για 4
  • Έως 250 objects μπαίνουν σε κάθε object stream αντί για τα συνηθισμένα 100, εκτός αν ορίσετε το δικό σας όριο μέσω ConfigureAdaptiveObjectStreamPacking
  • Όταν το αρχείο δεν είναι κρυπτογραφημένο, ο Catalog και το dictionary Info πακετάρονται κι αυτά σε object streams· η κρυπτογραφημένη έξοδος τα κρατά στο top level

Η συμπαγής σύνταξη ήρθε με μια παγίδα που αξίζει να ξέρετε αν επεκτείνετε τον writer. Η υπογραφή συμπληρώνεται αφού γραφτεί το αρχείο ψάχνοντας τα bytes για τα literal placeholders /ByteRange ( και /Contents <, και η συμπαγής γραφή θα τα γύριζε σε /ByteRange( και /Contents<, που η αναζήτηση δεν βρίσκει ποτέ. Τα dictionaries υπογραφής (Type Sig ή DocTimeStamp, FT Sig) και το dictionary κρυπτογράφησης κρατούν λοιπόν τη διάστιχη με κενά. Ένα συγγενικό ελάττωμα αφορούσε builds πριν το v2.766.41: κάθε αποθήκευση με object streams, CompressDocument συμπεριλαμβανομένης, ξεκινούσε με δύο γραμμές header %PDF-, οπότε αναβαθμίστε αν ένα αυστηρό validator σημειώνει την έξοδό σας

Μπορείτε να συμπιέσετε ένα PDF που είναι ήδη φορτωμένο;

Ναι, μέσω του overload με options CompressLoadedDocument(Options, Info), που τρέχει τα ίδια lossless βήματα σε ένα υπάρχον αρχείο. Με THPDFLoadedDocumentCompressionOptions.Default αφαιρεί αχρησιμοποίητους πόρους σελίδων, συγχωνεύει πανομοιότυπες γραμματοσειρές και forms, κάνει subsetting ενσωματωμένων γραμματοσειρών με αναμένα συμπαγή subsets, ξανασυμπιέζει streams unfiltered, Flate, LZW, ASCII και RunLength με Flate όταν το αποτέλεσμα είναι μικρότερο, και κάνει την επόμενη αποθήκευση να χρησιμοποιήσει object streams. Το HighRatioFlate είναι σβηστό by default, και τα object streams παραλείπονται για PDF/A-1 και incremental αποθηκεύσεις. Το overload CompressLoadedDocument χωρίς παραμέτρους είναι η παλαιότερη, στενότερη κλήση που μόνο Flate-συμπιέζει ασυμπίεστα streams

Ροή του CompressLoadedDocument του HotPDF στο Delphi: η κλήση αφαιρεί αχρησιμοποίητους πόρους σελίδων, συγχωνεύει πανομοιότυπες γραμματοσειρές και forms, κάνει subsetting ενσωματωμένων γραμματοσειρών με συμπαγή subsets, ξανασυμπιέζει streams με Flate μόνο όταν το αποτέλεσμα είναι μικρότερο, και ανάβει τα object streams για την επόμενη αποθήκευση, ενώ τα signature fields ενεργοποιούν RefusedBySignaturePolicy και αφήνουν το αρχείο ανέγγιχτο
Κάθε βήμα ξαναγράφει bytes που καλύπτει μια υπογραφή, οπότε ολόκληρο το έγγραφο απορρίπτεται εκτός αν επιτρέψετε ρητά invalidation — το Info.BytesSaved τότε αθροίζει μόνο τη δουλειά πόρων, γραμματοσειρών και streams
var
  Doc: THotPDF;
  Options: THPDFLoadedDocumentCompressionOptions;
  Info: THPDFLoadedDocumentCompressionInfo;
begin
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    Doc.LoadFromFile('quarterly-report.pdf');
    Options := THPDFLoadedDocumentCompressionOptions.Default;
    Doc.CompressLoadedDocument(Options, Info);
    if Info.RefusedBySignaturePolicy then
      Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
    else
    begin
      Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
        [Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
      Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
    end;
  finally
    Doc.Free;
  end;
end;

Δύο όρια μετράνε στη φορτωμένη διαδρομή. Κάθε βήμα ξαναγράφει bytes που καλύπτει μια υπογραφή, οπότε ένα έγγραφο με signature fields απορρίπτεται ως σύνολο: η κλήση επιστρέφει 0, θέτει RefusedBySignaturePolicy και δεν αλλάζει τίποτα, εκτός αν ορίσετε AllowSignatureInvalidation, μετά από το οποίο το Info.SignaturesInvalidated σας λέει τι παραχωρήσατε. Η συμπύκνωση είναι επίσης πιο συντηρητική εδώ απ' όσο στη διαδρομή δημιουργίας. Το HotPDF συμπυκνώνει μόνο font programs που χρησιμοποιούνται αποκλειστικά από γραμματοσειρές CIDFontType2 με /CIDToGIDMap Identity, όπου CID ισούται με GID, και παραλείπει programs με υπάρχον map stream, ένα /CIDSet, ή πίνακες color glyphs όπως COLR, sbix, CBDT ή SVG, επειδή το συμπαγές rebuild θα ρίχνει τα color layers. Σημειώστε επίσης ότι το Info.BytesSaved αθροίζει μόνο τα βήματα πόρων, γραμματοσειρών και streams· το κέρδος object-stream φαίνεται όταν γράφεται το αρχείο

Τι αποτελέσματα να περιμένετε στην πράξη;

Τα κέρδη ακολουθούν το πόσο από ένα αρχείο είναι ασυμπίεστη δομή και υπερμεγέθη δεδομένα γραμματοσειρών, όχι πόσες σελίδες έχει. Το δείγμα τριών σελίδων Arial και SimSun συρρικνώθηκε από 10.2 MB σε 20 KB όταν παρήχθη με CompressDocument, και από 10.2 MB σε 19.8 KB όταν το ασυμπίεστο πρωτότυπο φορτώθηκε και πέρασε από CompressLoadedDocument, με πανομοιότυπη απεικόνιση και στις δύο διαδρομές. Ένα PDF που είναι ήδη συμπαγές κουνιέται μόλις: στο regression σύνολο, τέτοια αρχεία αποθήκευσαν εντός -0.07% έως +0.06% του αρχικού τους μεγέθους. Τα αρχεία με πολλές φωτογραφίες κερδίζουν λίγο, επειδή καμία διαδρομή δεν αγγίζει τα δεδομένα εικόνων

Αν παράγετε τις ίδιες CJK αναφορές κάθε βράδυ, συνδυάστε τα συμπαγή subsets με την μόνιμη cache font subset στον δίσκο ώστε η δουλειά του subsetting να μην επαναλαμβάνεται ανά τρέξιμο, και κάντε diff τις συμπιεσμένες εξόδους ανά περιεχόμενο object αντί για bytes, αφού ένα αλλαγμένο πεδίο ξανα-Flate ολόκληρο object stream. Οι πλήρεις αναφορές ιδιοτήτων και records βρίσκονται στη σελίδα προϊόντος HotPDF Delphi PDF component