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

Ανάγνωση Ιδιοτήτων Γραμματοσειράς PDF με το PDFium Component στο Delphi

Κάθε ορατός χαρακτήρας σε ένα PDF φέρει μια αναφορά (reference) στη γραμματοσειρά που τον σχεδίασε, και το PDFium Component σας επιτρέπει να ακολουθήσετε αυτήν την αναφορά πίσω στο αντικείμενο γραμματοσειράς (font object) και να διαβάσετε όσα γνωρίζει. Η μονάδα πρόσβασης (unit of access) είναι ο χαρακτήρας, όχι το έγγραφο: επιλέγετε έναν χαρακτήρα βάσει του ευρετηρίου (index) του στο κείμενο της σελίδας και ζητάτε το όνομα οικογένειας, το βασικό (base) όνομα, το βάρος (weight), τη γωνία πλάγιων γραμμάτων (italic angle), και αν η υποκείμενη γραμματοσειρά (face) μεταφέρεται πραγματικά μέσα στο αρχείο. Αυτή η τελευταία ιδιότητα είναι αυτή που αναζητούν πραγματικά οι περισσότερες αναλύσεις, επειδή μια ενσωματωμένη γραμματοσειρά ταξιδεύει (travels) μαζί με το έγγραφο και μια μη ενσωματωμένη είναι μια υπόσχεση ότι το μηχάνημα του αναγνώστη τυχαίνει να έχει εγκατεστημένη την ίδια γραμματοσειρά

Το στοιχείο (component) τα εκθέτει (exposes) αυτά μέσω των ίδιων αντικειμένων TPdf και TPdfView που χρησιμοποιείτε για απόδοση (rendering) και εξαγωγή κειμένου. Δεν υπάρχει ξεχωριστό αντικείμενο "πίνακα γραμματοσειρών" (font table) για άνοιγμα. Από τη στιγμή που έχει αναλυθεί το κείμενο μιας σελίδας, οι ιδιότητες γραμματοσειράς "κρέμονται" από το ευρετήριο (index) του χαρακτήρα, και τις διαβάζετε ένα γλύμμα (glyph) τη φορά. Αυτός ο σχεδιασμός ταιριάζει με τον τρόπο που το PDF αποθηκεύει την πληροφορία εξαρχής: μια μόνο σελίδα μπορεί να αλλάξει γραμματοσειρές δεκάδες φορές, και η μόνη ειλικρινής απάντηση στο "σε ποια γραμματοσειρά είναι αυτό το έγγραφο" είναι "εξαρτάται ποιον χαρακτήρα εννοείτε"

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

Η μικρότερη χρήσιμη λειτουργία είναι να πάρετε ένα ευρετήριο χαρακτήρα και να εξάγετε όλα όσα το PDFium μπορεί να σας πει για τη γραμματοσειρά του. Κάθε ιδιότητα γραμματοσειράς στο TPdf και στο TPdfView ευρετηριάζεται βάσει της θέσης χαρακτήρα (character position), οπότε το ευρετήριο διαπερνά όλες. Η σελίδα πρέπει επίσης να είναι η τρέχουσα σελίδα για να επιλυθεί (resolve) το ευρετήριο στο σωστό κείμενο, το οποίο έχει σημασία μόλις προχωρήσετε πέρα από τη σελίδα ένα

procedure DescribeFontAt(Pdf: TPdf; CharIndex: Integer);
var
  Report: TStringList;
  PtSize: Single;
begin
  Report := TStringList.Create;
  try
    PtSize := Pdf.FontSize[CharIndex];

    Report.Add('Character : ' + Pdf.Character[CharIndex]);
    Report.Add('Family    : ' + Pdf.FontFamilyName[CharIndex]);
    Report.Add('Base name : ' + Pdf.FontBaseName[CharIndex]);
    Report.Add('Weight    : ' + IntToStr(Pdf.FontWeight[CharIndex]));
    Report.Add('Italic    : ' + IntToStr(Pdf.FontItalicAngle[CharIndex]) + ' deg');
    Report.Add('Size      : ' + FormatFloat('0.0', PtSize) + ' pt');
    Report.Add('Ascent    : ' + FormatFloat('0.0', Pdf.FontAscent[CharIndex, PtSize]));
    Report.Add('Descent   : ' + FormatFloat('0.0', Pdf.FontDescent[CharIndex, PtSize]));
    Report.Add('Embedded  : ' + BoolToStr(Pdf.FontIsEmbedded[CharIndex], True));

    ShowMessage(Report.Text);
  finally
    Report.Free;
  end;
end;

Μερικές από τις υπογραφές (signatures) εκπλήσσουν τους ανθρώπους που προέρχονται από άλλες βιβλιοθήκες. Το FontAscent (άνοδος) και το FontDescent (κάθοδος) παίρνουν δύο ορίσματα (arguments), το ευρετήριο χαρακτήρα και ένα μέγεθος σε στιγμές (point size), επειδή το PDFium αναφέρει αυτές τις μετρήσεις (metrics) σε μονάδες χώρου συμβόλου (glyph-space units) που γίνονται pixel μόνο όταν τις κλιμακώσετε (scale) με το μέγεθος στο οποίο ορίστηκε το κείμενο. Περάστε την τιμή που ήδη διαβάσατε από το FontSize[CharIndex] και θα λάβετε άνοδο και κάθοδο στις ίδιες στιγμές (points) με την υπόλοιπη διάταξη (layout). Η κάθοδος επιστρέφεται αρνητική, καθώς μετρά κάτω από τη γραμμή βάσης (baseline). Το όνομα οικογένειας (family name) και το βασικό όνομα (base name) είναι ξεχωριστές συμβολοσειρές (strings) επίτηδες: το βασικό όνομα είναι η ακατέργαστη (raw) καταχώριση /BaseFont από το PDF, φέροντας συχνά ένα πρόθεμα (prefix) υποσυνόλου όπως ABCDEF+, ενώ το όνομα οικογένειας είναι το καθαρισμένο όνομα (cleaned-up name) στο οποίο ο αποδοτέας (renderer) το επιλύει

Μετατροπή ενός κλικ σε ευρετήριο χαρακτήρα

Σε ένα πρόγραμμα προβολής, σπάνια γνωρίζετε το ευρετήριο εκ των προτέρων. Ο χρήστης κάνει κλικ σε ένα γλύμμα (glyph) και πρέπει να μεταφράσετε τη συντεταγμένη (coordinate) pixel στον χαρακτήρα από κάτω της. Το CharacterIndexAtPos κάνει ακριβώς αυτό, λαμβάνοντας τη θέση του ποντικιού και μια ανοχή (tolerance) και επιστρέφοντας το ευρετήριο του πλησιέστερου χαρακτήρα ή μια αρνητική τιμή (negative value) όταν το κλικ προσγειωθεί σε κενό χώρο (whitespace) ή άδεια σελίδα

procedure TfrmMain.PdfViewMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  Index: Integer;
begin
  if not PdfView.Active then
    Exit;

  // 4 px of slack in each direction so a near-miss still hits the glyph.
  Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
  if Index < 0 then
    Exit;                      // clicked between glyphs; leave the panel alone

  PdfView.CurrentCharIndex := Index;
  DescribeFontAt(PdfView.Pdf, Index);
end;

Η ανοχή αξίζει να ρυθμιστεί. Πολύ σφιχτή, και οι χρήστες νιώθουν ότι πρέπει να προσγειωθούν στον ακριβή μίσχο (stem) ενός γράμματος· πολύ χαλαρή, και ένα κλικ σε ένα περιθώριο (margin) κάνει snap σε κάποιον μακρινό χαρακτήρα που δεν έχει καμία σχέση με αυτό που εννοούσαν. Τρία έως πέντε pixel συσκευής είναι ένα λογικό σημείο εκκίνησης για προβολή στην οθόνη. Το ευρετήριο (index) που επιστρέφεται είναι μέσα στο αναλυμένο (parsed) κείμενο της τρέχουσας σελίδας, ο ίδιος χώρος ευρετηρίου που περιμένει κάθε ιδιότητα γραμματοσειράς, οπότε μπορείτε να το περάσετε (hand it) απευθείας στη ρουτίνα παραπάνω. Η αποθήκευσή του στο CurrentCharIndex είναι προαιρετική αλλά βολική: το view το διατηρεί ως τη δική του αντίληψη για το εστιασμένο γλύμμα, το οποίο είναι βολικό αν άλλα μέρη του UI θέλουν να διαβάσουν την επιλογή χωρίς να την παράγουν ξανά

Η ενσωμάτωση είναι η ιδιότητα που έχει σημασία

Για τις περισσότερες πραγματικές δουλειές, η μόνη ερώτηση που αξίζει να απαντηθεί είναι εάν κάθε γραμματοσειρά είναι ενσωματωμένη. Ένα έγγραφο του οποίου οι γραμματοσειρές ταξιδεύουν (ride) όλες μέσα του, αποδίδεται (renders) το ίδιο στο RIP ενός γραφείου εκτυπώσεων, στον φορητό υπολογιστή ενός συναδέλφου και σε έναν διακομιστή (server) χωρίς καθόλου GUI. Ένα έγγραφο που βασίζεται (leans) σε ένα μη ενσωματωμένο Helvetica τζογάρει ότι κάθε ένα από αυτά τα μηχανήματα έχει μια ταιριαστή γραμματοσειρά, και όταν ο τζόγος αποτύχει, ο αναγνώστης (reader) υποκαθιστά (substitutes) κάτι κοντινό, οι μετρήσεις (metrics) μετατοπίζονται (shift) και μια προσεκτικά σχεδιασμένη φόρμα αναδιατάσσεται (reflows) ακριβώς τόσο όσο να σπάσει. Η περιήγηση (Walking) στο κείμενο της σελίδας και η κατηγοριοποίηση (bucketing) των γραμματοσειρών βάσει της κατάστασης ενσωμάτωσης σας δίνει αυτή την απάντηση φθηνά (cheaply)

procedure ReportNonEmbeddedFonts(Pdf: TPdf);
var
  Embedded, External: TStringList;
  I: Integer;
  Name: string;
begin
  Embedded := TStringList.Create;
  External := TStringList.Create;
  try
    Embedded.Sorted := True;
    Embedded.Duplicates := dupIgnore;
    External.Sorted := True;
    External.Duplicates := dupIgnore;

    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      Name := Pdf.FontBaseName[I];
      if Name = '' then
        Continue;              // generated spaces and the like have no font
      if Pdf.FontIsEmbedded[I] then
        Embedded.Add(Name)
      else
        External.Add(Name);
    end;

    if External.Count > 0 then
      ShowMessage(IntToStr(External.Count) +
        ' non-embedded font(s):' + sLineBreak + External.Text)
    else
      ShowMessage('All ' + IntToStr(Embedded.Count) +
        ' font(s) on this page are embedded.');
  finally
    Embedded.Free;
    External.Free;
  end;
end;

Δύο λεπτομέρειες το κρατούν αυτό ειλικρινές (honest). Πρώτον, το CharacterCount είναι ανά σελίδα, οπότε ένας έλεγχος (audit) ολόκληρου του εγγράφου σημαίνει ρύθμιση του Pdf.PageNumber σε κάθε σελίδα διαδοχικά (in turn) και εκτέλεση του βρόχου (loop) ξανά, συγχωνεύοντας (merging) τα αποτελέσματα. Δεύτερον, το επίπεδο κειμένου περιέχει παραγόμενους χαρακτήρες, όπως τα κενά που συνάγει ένας αναγνώστης (reader infers) μεταξύ λέξεων, και αυτοί δεν έχουν αντικείμενο γραμματοσειράς (font object) από πίσω τους· ο έλεγχος του άδειου-βασικού-ονόματος (empty-base-name) τους παραλείπει αντί να καταγράφει ένα φάντασμα. Το βασικό όνομα είναι το σωστό κλειδί για την κατάργηση διπλότυπων (de-duplication) εδώ, επειδή το πρόθεμα υποσυνόλου (subset prefix) που φέρει, διακρίνει δύο διαφορετικά υποσύνολα (subsets) της ίδιας οικογένειας, το οποίο συνήθως είναι αυτό που θέλετε να μάθετε

Εξαγωγή της ενσωματωμένης γραμματοσειράς

Όταν μια γραμματοσειρά είναι ενσωματωμένη, μπορείτε να διαβάσετε τα byte της (bytes) άμεσα. Το FontData επιστρέφει το ακατέργαστο πρόγραμμα γραμματοσειράς (raw font program), τα ίδια δεδομένα TrueType ή CFF που φέρει το PDF, το οποίο αρκεί (enough) για να γραφτεί (write) ένα αυτόνομο (standalone) αρχείο γραμματοσειράς ή για να γίνει λήψη "δακτυλικού αποτυπώματος" (fingerprint) της γραμματοσειράς (face) έναντι μιας γνωστής βιβλιοθήκης. Επιστρέφει έναν άδειο πίνακα (empty array) όταν η γραμματοσειρά δεν είναι ενσωματωμένη, οπότε ο έλεγχος ενσωμάτωσης και ο έλεγχος μήκους μαζί φρουρούν την εγγραφή

procedure SaveEmbeddedFont(Pdf: TPdf; CharIndex: Integer;
  const OutputFile: string);
var
  Data: TBytes;
  Stream: TFileStream;
begin
  if not Pdf.FontIsEmbedded[CharIndex] then
  begin
    ShowMessage('That glyph''s font is not embedded; nothing to extract.');
    Exit;
  end;

  Data := Pdf.FontData[CharIndex];
  if Length(Data) = 0 then
    Exit;

  Stream := TFileStream.Create(OutputFile, fmCreate);
  try
    Stream.WriteBuffer(Data[0], Length(Data));
  finally
    Stream.Free;
  end;
  ShowMessage('Wrote ' + IntToStr(Length(Data)) + ' bytes.');
end;

Τα byte είναι το ενσωματωμένο υποσύνολο (subset), όχι η αρχική (original) εμπορική γραμματοσειρά (retail font), επομένως αυτό που παίρνετε πίσω συνήθως καλύπτει μόνο τα γλύμματα που το έγγραφο χρησιμοποίησε πραγματικά. Αυτό είναι ακριβώς το σωστό για ιατροδικαστική (forensics) και επαλήθευση, και ακατάλληλο (poor fit) για επαναχρησιμοποίηση· ένα υποσύνολο της Times New Roman που περιέχει τριάντα γλύμματα δεν είναι μια γραμματοσειρά που μπορείτε να εγκαταστήσετε και να πληκτρολογήσετε με αυτή. Αντιμετωπίστε (Treat) την εξαγωγή ως έναν τρόπο επιθεώρησης του τι εστάλη (shipped), όχι ως εργαλείο ανάκτησης γραμματοσειράς. Αν χρειάζεστε το αντίστοιχο βασικό όνομα (base name) για να επισημάνετε (label) την έξοδο (output), διαβάστε το FontBaseName[CharIndex] μαζί με τα δεδομένα (data), και αφαιρέστε (strip) την αρχική ετικέτα υποσυνόλου (subset tag) αν θέλετε τη γυμνή οικογένεια (bare family)

Κατανοώντας τον αριθμό βάρους

Το FontWeight επιστρέφει την αριθμητική (numeric) κλάση βάρους, την ίδια κλίμακα 100-προς-900 που χρησιμοποιεί το CSS, όπου 400 είναι regular και 700 είναι bold. Το PDFium αναφέρει ό,τι δηλώνει η γραμματοσειρά, το οποίο δεν είναι πάντα ένα στρογγυλό (round) εκατό· μια γραμματοσειρά μπορεί να ανακοινώσει 350 ή 650, και η αντιμετώπιση οτιδήποτε στο 600 ή παραπάνω ως "αρκετά έντονο για να έχει σημασία" (bold enough to matter) αντέχει (holds up) καλύτερα από τον έλεγχο (testing) για ακριβώς 700. Η γωνία των πλάγιων γραμμάτων (italic angle) είναι ένα συνοδευτικό σήμα (companion signal): μια μη μηδενική τιμή (non-zero value), συνήθως αρνητική, σημαίνει ότι η γραμματοσειρά (face) είναι ένα πλάγιο (oblique) ή πραγματικό italic σχέδιο, και το μηδέν σημαίνει όρθιο (upright). Μαζί σας επιτρέπουν να ξεχωρίσετε (tell) μια ροή (run) bold-italic από μια κανονική (regular) χωρίς να αποδώσετε (rendering) τίποτα, κάτι που είναι το είδος του ελέγχου που ένα προεκτυπωτικό πέρασμα (preflight pass) ή ένας έλεγχος προσβασιμότητας (accessibility audit) θέλει να κάνει μαζικά (in bulk)

Καμία από αυτές τις αναγνώσεις (reads) δεν απαιτεί ένα αποδομένο bitmap (rendered bitmap). Προέρχονται από το αναλυμένο επίπεδο κειμένου, επομένως ένα ανοιχτό έγγραφο (open document) στη σωστή σελίδα είναι όλη η ρύθμιση (setup) που χρειάζεστε, πράγμα που κάνει την επιθεώρηση γραμματοσειράς φθηνή (cheap) για να εκτελεστεί (run) σε ένα ολόκληρο αρχείο (archive). Εάν το συνδυάζετε αυτό (pairing this) με εξαγωγή κειμένου, οι ίδιοι δείκτες χαρακτήρων (character indices) ευθυγραμμίζονται (line up) με το κείμενο που εξάγετε (pull out), επομένως η γραμματοσειρά ενός γράμματος (glyph's font) και η τιμή Unicode του είναι δύο αναγνώσεις έναντι ενός δείκτη. Το συνοδευτικό (companion) άρθρο για την εξαγωγή κειμένου από έγγραφα PDF με το PDFium Component καλύπτει αυτή την πλευρά του επιπέδου κειμένου (text layer) σε μεγαλύτερο βάθος

Οι ιδιότητες γραμματοσειράς που φαίνονται (shown) εδώ είναι μέρος του PDFium Delphi VCL Component