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

Έλεγχος Μεγέθους PDF σε Delphi: Ανάλυση Byte ανά Κατηγορία

Για να ανακαλύψετε πού πραγματικά πηγαίνει το μέγεθος ενός αρχείου PDF, η losLab PDF Library εκθέτει την AuditDocumentSpace, η οποία ταξινομεί κάθε έμμεσο αντικείμενο σε δώδεκα κατηγορίες — εικόνες, προγράμματα γραμματοσειράς, λεξικά γραμματοσειράς, ρεύματα περιεχομένου, form XObjects, object streams, ενσωματωμένα αρχεία, metadata, δέντρο δομής, σημειώσεις, δέντρο σελίδων, άλλο — και αναφέρει τον αριθμό αντικειμένων, τα αποθηκευμένα bytes και το ποσοστό μεριδίου κάθε μιας

Η κατάσταση για την οποία υπάρχει αυτό είναι οικεία. Μια αναφορά 40 σελίδων βγαίνει από τη γεννήτριά σας στα 80 MB, ο πελάτης ρωτά γιατί, και το μόνο που μπορείτε να προσφέρετε είναι μια εικασία. Μάλλον οι εικόνες. Ίσως οι γραμματοσειρές. Οπότε ενεργοποιείτε το downsampling, το στέλνετε, και το αρχείο καταλήγει στα 74 MB επειδή το πραγματικό βάρος ήταν κάπου εντελώς αλλού. Το συνοδευτικό μας άρθρο για subsetting γραμματοσειρών και downsampling εικόνων καλύπτει πώς να συρρικνώσετε ένα PDF· αυτό εδώ καλύπτει το βήμα που πρέπει να έρθει πρώτο, δηλαδή τη μέτρηση αυτού που πρόκειται να συρρικνώσετε

Γιατί να μετράτε πριν συμπιέσετε;

Επειδή τα τρία τυπικά περάσματα βελτιστοποίησης έχουν εξαιρετικά διαφορετική απόδοση σε κάθε δεδομένο αρχείο, και τίποτα στο αρχείο δεν σας λέει ποιο ισχύει μέχρι να μετρήσετε. Το subsetting γραμματοσειρών σε ένα έγγραφο του οποίου οι γραμματοσειρές είναι ήδη 2% των bytes του είναι ένα απόγευμα που ξοδεύεται μετακινώντας ένα σφάλμα στρογγυλοποίησης. Το downsampling εικόνων σε ένα αρχείο του οποίου ο όγκος είναι ασυμπίεστα ρεύματα περιεχομένου παράγει την ίδια απογοήτευση. Ο βελτιστοποιητής δεν είναι το δύσκολο μέρος — κάθε βιβλιοθήκη έχει έναν. Το να ξέρετε ποιον βελτιστοποιητή να στρέψετε σε αυτό το αρχείο είναι το δύσκολο μέρος, και αυτό είναι ζήτημα λογιστικής, όχι ζήτημα συμπίεσης. Ένας έλεγχος πιάνει επίσης τις περιπτώσεις όπου η απάντηση δεν είναι κανένας βελτιστοποιητής: ένα αρχείο που αποδεικνύεται 60% ενσωματωμένα συνημμένα δεν χρειάζεται καλύτερη συμπίεση, χρειάζεται μια συζήτηση για το αν αυτά τα συνημμένα ανήκουν στο έγγραφο, και ένα αρχείο που είναι 30% δέντρο δομής πληρώνει για επισήμανση προσβασιμότητας, που συνήθως είναι εσκεμμένο κόστος που δεν πρέπει να αφαιρέσετε σιωπηλά. Μόλις τα bytes αποδοθούν παίρνετε μια απόφαση προϊόντος με αριθμούς από πίσω αντί να απλώνετε το χέρι στον πλησιέστερο διακόπτη

Τι περιέχει η αναφορά δώδεκα κατηγοριών

Η AuditDocumentSpace επιστρέφει έναν χειριστή λίστας strings αντί για εγγραφή, ώστε η αναφορά να επιβιώνει αμετάβλητη μέσα από τις flat DLL και COM προσόψεις. Η λίστα περιέχει μια γραμμή σύνοψης Total,Objects,Bytes,100.0 ακολουθούμενη από ακριβώς δώδεκα γραμμές Category,Objects,Bytes,Percent σε σταθερή σειρά που είναι μέρος του συμβολαίου: Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. Δεκατρείς γραμμές, πάντα, ακόμη κι όταν μια κατηγορία είναι κενή

var
  Lib: TPDFlib;
  ListID, I: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('report.pdf', '') <> 1 then
      Exit;
    ListID := Lib.AuditDocumentSpace;   // 0 when no document is selected
    if ListID = 0 then
      Exit;
    try
      // GetStringListItem is 1-based: items run 1..GetStringListCount
      for I := 1 to Lib.GetStringListCount(ListID) do
        Memo1.Lines.Add(Lib.GetStringListItem(ListID, I));
    finally
      Lib.ReleaseStringList(ListID);
    end;
  finally
    Lib.Free;
  end;
end;

Μια λεπτομέρεια Delphi σε αυτόν τον βρόχο θα σας δαγκώσει ακριβώς μία φορά. Η GetStringListItem χρησιμοποιεί δείκτες στοιχείων βασισμένους στο ένα, ταιριάζοντας με την GetStringListCount, και ένας δείκτης εκτός εύρους επιστρέφει κενό string αντί να προκαλεί εξαίρεση. Γράψτε τον βρόχο for I := 0 to Count - 1 από συνήθεια και παίρνετε μια κενή πρώτη γραμμή, μια σιωπηλά χαμένη τελευταία γραμμή, και καμία εξαίρεση πουθενά να σας πει ότι η δεικτοδότηση είναι λάθος. Η ίδια η αναφορά θα φαίνεται σχεδόν σωστή, που είναι η χειρότερη κατάσταση αποτυχίας που μπορεί να έχει ένα διαγνωστικό εργαλείο

Γιατί ο έλεγχος χρησιμοποιεί αποθηκευμένο μήκος αντί για αποκωδικοποιημένο μέγεθος;

Επειδή το αποθηκευμένο μήκος είναι τόσο ο αριθμός που θέλετε όσο και ο αριθμός που είναι φθηνό να αποκτηθεί. Κάθε έμμεσο αντικείμενο φέρει το TPDFIndObj.FLength, το ακατέργαστο μήκος byte που καταλαμβάνει το αντικείμενο στο αρχείο όπως αναλύθηκε. Η χρήση του σημαίνει ότι μια εικόνα DCTDecode 900 KB αναφέρεται ως 900 KB — τα bytes που σας κοστίζει στον δίσκο — αντί για τα 40 MB δειγμάτων RGB στα οποία αποκωδικοποιείται. Σημαίνει επίσης ότι ο έλεγχος ποτέ δεν χρειάζεται να αποκωδικοποιήσει τίποτα: τα οκνηρά φορτωμένα αντικείμενα παραμένουν οκνηρά, τα φίλτρα παραμένουν ανεκτέλεστα, και ο έλεγχος ενός αρχείου 500 MB είναι ένα πέρασμα πάνω από κεφαλίδες αντικειμένων αντί για έναν πλήρη κύκλο αποσυμπίεσης

Ο δεύτερος κανόνας είναι άμυνα έναντι διπλής καταμέτρησης. Όταν ένα αντικείμενο ζει μέσα σε ένα συμπιεσμένο object stream, όπως υποδεικνύεται από ένα μη μηδενικό FObjStrNum, ο αριθμός byte του καταγράφεται ως μηδέν. Η αποθήκευσή του έχει ήδη πληρωθεί μία φορά από το stream-container, το οποίο το ISO 32000-1 §7.5.7 ορίζει ως ένα stream /Type /ObjStm που κρατά πολλά αντικείμενα σε ένα Flate-συμπιεσμένο payload. Η χρέωση κάθε μέλους με το δικό του μερίδιο και μετά η χρέωση του container ξανά θα φούσκωνε το σύνολο πέρα από το πραγματικό μέγεθος αρχείου. Αυτό έχει άμεση συνέπεια στο πώς διαβάζετε την έξοδο, που καλύπτεται παρακάτω και σε μεγαλύτερο βάθος στο άρθρο μας για τα object streams και cross-reference streams

Γιατί ένα πρόγραμμα γραμματοσειράς δεν μπορεί να ταξινομήσει τον εαυτό του;

Επειδή ένα αρχείο γραμματοσειράς TrueType ενσωματωμένο σε ένα PDF δεν έχει κανέναν δείκτη που να το λέει. Το ISO 32000-1 §9.8.1 ορίζει το ενσωματωμένο πρόγραμμα γραμματοσειράς ως την τιμή του /FontFile, /FontFile2 ή /FontFile3 σε έναν περιγραφέα γραμματοσειράς, και το λεξικό stream στην άλλη άκρη αυτής της αναφοράς φέρει /Length1 και κλειδιά φίλτρου αλλά κανένα /Type και κανένα /Subtype που να το προσδιορίζει ως γραμματοσειρά. Κοιτάζοντάς το μεμονωμένα είναι ένα ανώνυμο δυαδικό stream. Μόνο ο περιγραφέας που δείχνει σε αυτό ξέρει τι είναι. Η ίδια ασυμμετρία εμφανίζεται για τις σημειώσεις: το §12.5.2 κάνει το /Type /Annot προαιρετικό σε ένα λεξικό σημείωσης, οπότε το αξιόπιστο σήμα είναι η συμμετοχή σε έναν πίνακα σελίδας /Annots, όχι το ίδιο το λεξικό

Έτσι η ταξινόμηση τρέχει δύο φορές. Το πρώτο πέρασμα διαβάζει τα δικά του /Type και /Subtype κάθε αντικειμένου και παίρνει τις εύκολες νίκες: /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font και /Type /FontDescriptor, /Metadata, /EmbeddedFile και /Filespec, /StructTreeRoot και /StructElem, /Annot, /Page και /Pages. Όλα τα υπόλοιπα καταλήγουν προσωρινά στο Other. Το δεύτερο πέρασμα μετά διατρέχει την πλευρά αναφοράς και παρακάμπτει: κάθε λεξικό σελίδας επαναναθέτει το /Contents του σε ρεύματα περιεχομένου, τις καταχωρίσεις /Annots του σε σημειώσεις, και το /Thumb του σε εικόνες, ενώ κάθε λεξικό γραμματοσειράς διατρέχει τη δική του αλυσίδα περιγραφέα

// Shape of the second pass: the referrer names the object
Descriptor := DictOf(FontDict.FindValueByKeyName('FontDescriptor'));
if Assigned(Descriptor) then
begin
  MarkRef(FontDict.FindValueByKeyName('FontDescriptor'), catFontDicts);
  MarkRef(Descriptor.FindValueByKeyName('FontFile'),  catFontPrograms);
  MarkRef(Descriptor.FindValueByKeyName('FontFile2'), catFontPrograms);
  MarkRef(Descriptor.FindValueByKeyName('FontFile3'), catFontPrograms);
end;
// Type0 fonts keep the descriptor one level down
Descendants := FontDict.FindValueByKeyName('DescendantFonts', True);
if (Descendants is TPDFArray) and (TPDFArray(Descendants).Count > 0) then
  MarkFontProgramRefs(DictOf(TPDFArray(Descendants).Item[0]));

Ανάγνωση της αναφοράς και επιλογή της επόμενης κίνησης

Διαβάστε πρώτα τα μερίδια, τους αριθμούς αντικειμένων δεύτερους, και αντιμετωπίστε κάθε μεγάλο χάσμα ανάμεσά τους ως σήμα. Ένα σύγχρονο PDF βάζει τα περισσότερα από τα μικρά λεξικά του μέσα σε object streams, οπότε το Page tree και το Structure tree δείχνουν συχνά δεκάδες αντικείμενα έναντι σχεδόν μηδενικών bytes — το πραγματικό τους κόστος έχει ενσωματωθεί στη γραμμή Object streams. Αν το Object streams είναι από μόνο του μεγάλο, το αρχείο είναι πυκνό σε δομή τύπου metadata παρά σε περιεχόμενο, και ο μοχλός είναι το κλάδεμα αντικειμένων, όχι η συμπίεσή τους. Τα appearance streams σημειώσεων συμπεριφέρονται παρόμοια: φέρουν /Subtype /Form, οπότε ένα έγγραφο βαριά σφραγισμένο δείχνει το βάρος του κάτω από Form XObjects ενώ η γραμμή Annotations παραμένει μικρή

function CategoryShare(Lib: TPDFlib; ListID: Integer;
  const Category: string): Double;
var
  I: Integer;
  Parts: TArray<string>;
  Inv: TFormatSettings;
begin
  Result := 0;
  Inv := FormatSettings;
  Inv.DecimalSeparator := '.';   // the report is locale-independent
  for I := 2 to Lib.GetStringListCount(ListID) do   // line 1 is Total
  begin
    Parts := string(Lib.GetStringListItem(ListID, I)).Split([',']);
    if (Length(Parts) = 4) and SameText(Parts[0], Category) then
      Exit(StrToFloatDef(Parts[3], 0, Inv));
  end;
end;

Δύο γεγονότα μορφοποίησης έχουν σημασία αν αναλύετε τα ποσοστά αντί να τα εμφανίζετε απλώς. Ο δεκαδικός διαχωριστής είναι πάντα μια κυριολεκτική τελεία ανεξάρτητα από τα locale του μηχανήματος, οπότε η ανάλυση με το περιβάλλον FormatSettings σε έναν γερμανικό ή γαλλικό σταθμό εργασίας θα αποτύχει ή, χειρότερα, θα διαβάσει λάθος. Και τα μηδενικά στο τέλος περικόπτονται, οπότε μια κατηγορία που κατέχει ακριβώς 40% των bytes εκτυπώνεται ως 40, όχι 40.0 — ποτέ μην υποθέτετε σταθερό αριθμό δεκαδικών ψηφίων. Με το μερίδιο στο χέρι η δρομολόγηση είναι μηχανική: ένα κυρίαρχο μερίδιο Images δείχνει στην DownsampleImages, ένα κυρίαρχο μερίδιο Font programs στην SubsetEmbeddedFonts, και ογκώδη Content streams στην CompressContent

Τι δεν σας λέει σκόπιμα ο έλεγχος

Το σύνολο είναι ένα άθροισμα πάνω από έμμεσα αντικείμενα, και ένα αρχείο PDF είναι ελαφρώς περισσότερο από τα αντικείμενά του. Η κεφαλίδα αρχείου, ο trailer, ο διαστιχικός κενός χώρος μεταξύ αντικειμένων και ένας κλασικός πίνακας cross-reference δεν είναι έμμεσα αντικείμενα, οπότε αυτά τα bytes δεν αποδίδονται σε τίποτα και το σύνολο του ελέγχου καταλήγει λίγο κάτω από το μέγεθος στον δίσκο. Ένα cross-reference stream είναι διαφορετικό — είναι πραγματικό αντικείμενο με /Type /XRef, οπότε σε ένα σύγχρονο αρχείο αυτά τα bytes εμφανίζονται, στην κατηγορία Other. Καμία από τις δύο συμπεριφορές δεν είναι ελάττωμα, αλλά αν συμφιλιώνετε τον έλεγχο με έναν αριθμό byte από το σύστημα αρχείων, από εκεί προέρχεται το χάσμα

Δύο ακόμη όρια αξίζει να δηλωθούν καθαρά. Πρώτον, οι αριθμοί περιγράφουν ένα αρχείο που φορτώθηκε, όχι ένα που δημιουργείται: για αντικείμενα χτισμένα στη μνήμη που δεν έχουν ακόμη αποθηκευμένο μήκος, το μέγεθος επιστρέφει στη σειριοποιημένη έξοδο με μια ονομαστική αποζημίωση για το λεξικό stream, που είναι μια εκτίμηση της τελικής εγγραφής αντί για μέτρηση. Ελέγξτε μετά από αποθήκευση-και-επαναφόρτωση αν θέλετε ακριβή στοιχεία. Δεύτερον, μια χοντρή γραμμή Other είναι εύρημα, όχι αναφορά σφάλματος — συνήθως σημαίνει ορφανά αντικείμενα που τίποτα πια δεν αναφέρει, που είναι δουλειά για mark-and-sweep garbage collection παρά για οποιοδήποτε πέρασμα συμπίεσης

Χρησιμοποιημένος με αυτόν τον τρόπο, ο έλεγχος αλλάζει το σχήμα της συζήτησης. Αντί να μαντεύετε την αναφορά των 80 MB την ανοίγετε, τρέχετε μία κλήση, και διαβάζετε ότι οι εικόνες είναι 8%, τα προγράμματα γραμματοσειράς είναι 61%, και το έγγραφο ενσωματώνει εννέα πλήρη προγράμματα γραμματοσειράς για ένα εταιρικό στυλ που χρησιμοποιεί τρεις γραμματοσειρές. Αυτή είναι μια διορθώσιμη απάντηση με έναν αριθμό προσαρτημένο σε αυτήν. Η AuditDocumentSpace, μαζί με τα περάσματα βελτιστοποίησης προς τα οποία σας κατευθύνει, διατίθεται στη losLab PDF Library για Delphi και C++Builder, όπου οι σελίδες αναφοράς τεκμηριώνουν την πλήρη λίστα κατηγοριών και το γύρω API λίστας strings