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

Μέτρηση Κειμένου PDF για Διάταξη και Αναδίπλωση Λέξεων στο Delphi

Η κλήση που τοποθετεί κείμενο σε μια σελίδα PDF είναι απλή. Δίνετε στην AddText ένα string, μια γραμματοσειρά, ένα μέγεθος, και μια θέση, και τα γλυφά εμφανίζονται. Αυτό που δεν κάνει είναι να σας πει πόσο πλατύ θα είναι αυτό το string μόλις σχεδιαστεί, και δεν χωρίζει ένα μακρύ string σε πολλές γραμμές. Μία μόνο κλήση ζωγραφίζει ένα run κειμένου σε μία θέση. Αν το run είναι πλατύτερο από τη στήλη στην οποία σκοπεύατε να χωρέσει, απλώς προσπερνά την άκρη, και τίποτα στην κλήση σχεδίασης δεν σας προειδοποιεί. Τη στιγμή που θέλετε μια παράγραφο αντί για μία και μόνη ετικέτα, το κομμάτι που λείπει είναι το πλάτος ενός string στην επιλεγμένη γραμματοσειρά και μέγεθος, μετρημένο πριν το αναθέσετε στη σελίδα

Αυτό είναι το κλασικό πρόβλημα διάταξης. Για να αναδιπλώσετε μια παράγραφο μέσα σε μια στήλη πρέπει να ξέρετε, λέξη προς λέξη, πόσο οριζόντιο χώρο θα καταλάβει κάθε υποψήφια γραμμή, και πρέπει να το ξέρετε πριν σχεδιάσετε οτιδήποτε. Η αναδίπλωση λέξεων είναι ένας βρόχος μέτρησης τυλιγμένος γύρω από μια κλήση σχεδίασης, και μια βιβλιοθήκη που μόνο σχεδιάζει σάς δίνει το δεύτερο μισό. Η υποστήριξη μέτρησης κειμένου στο PDFium component κλείνει αυτό το κενό με δύο συναρτήσεις, τις MeasureText και MeasureTextWidth, που αναφέρουν την αποδιδόμενη έκταση ενός string χωρίς να αφήνουν κανένα ίχνος σε καμία σελίδα

Γιατί η μέτρηση είναι class helper, όχι νέα μέθοδος στην TPdf

Η υποστήριξη μέτρησης έρχεται ως class helper της Delphi για την TPdf, ζώντας στη δική της unit, αντί ως νέες μέθοδοι προσαρτημένες στην κλάση TPdf. Ένας class helper είναι ένα χαρακτηριστικό της γλώσσας που σας επιτρέπει να προσαρτήσετε μεθόδους σε έναν υπάρχοντα τύπο από έξω από τη δήλωσή του. Μόλις η unit βρεθεί στο πεδίο εμβέλειας, οι νέες μέθοδοι καλούνται ακριβώς σαν να ανήκαν στην κλάση, οπότε μια μέθοδος helper διαβάζεται ως Pdf.MeasureTextWidth(...) χωρίς ξεχωριστό αντικείμενο για κατασκευή ή μεταφορά

Ο λόγος για αυτή τη διαστρωμάτωση είναι ο διαχωρισμός. Ο βασικός τύπος TPdf παραμένει όπως είναι, χωρίς κανένα πεδίο να προστίθεται και καμία υπάρχουσα υπογραφή να αγγίζεται, οπότε ένα project που ποτέ δεν χρειάζεται διάταξη ποτέ δεν κουβαλά τον κώδικα μέτρησης. Ένα project που τη χρειάζεται προσθέτει μία unit σε μια ρήτρα uses και οι μέθοδοι ενεργοποιούνται. Η δυνατότητα γίνεται προαιρετική στο επίπεδο μίας μόνο unit, που είναι ο καθαρότερος τρόπος να επεκτείνετε έναν τύπο που δεν σας ανήκει ή δεν θέλετε να διαταράξετε

uses
  PDFium, FPdfView, FPdfEdit,
  FPdfMeasure;   // η unit του helper· φέρνει την MeasureText στο πεδίο εμβέλειας της TPdf

// Με την unit στο πεδίο εμβέλειας οι μέθοδοι διαβάζονται ως μέλη της TPdf:
var
  W, H: Double;
begin
  Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
  // Τα W και H είναι πλέον το αποδιδόμενο πλάτος και ύψος σε μονάδες χρήστη PDF
end;

Μέτρηση χωρίς να αγγίζετε τη σελίδα

Η μέτρηση πρέπει να είναι απαλλαγμένη από παρενέργειες. Πρέπει να αναφέρει ένα πλάτος χωρίς να αφήνει τίποτα πίσω, επειδή την καλείτε πολλές φορές ενώ αποφασίζετε μια διάταξη και η σελίδα πρέπει να φαίνεται ακριβώς όπως θα φαινόταν αν δεν είχατε μετρήσει ποτέ τίποτα. Η τεχνική που το καθιστά δυνατό είναι να κατασκευάσετε ένα αντικείμενο κειμένου, να το ρωτήσετε για το μέγεθός του, και να το πετάξετε πριν συνδεθεί ποτέ σε μια σελίδα

Η ακολουθία είναι τέσσερις κλήσεις PDFium. Η FPDFPageObj_NewTextObj δημιουργεί ένα αντικείμενο κειμένου έναντι του εγγράφου, δεδομένου του ονόματος γραμματοσειράς και του μεγέθους. Η FPDFText_SetText ορίζει το string που κουβαλά εκείνο το αντικείμενο. Η FPDFPageObj_GetBounds διαβάζει πίσω το πλαίσιο οριοθέτησης του αντικειμένου. Η FPDFPageObj_Destroy απελευθερώνει το αντικείμενο. Καθοριστικά, τίποτα σε αυτή την ακολουθία δεν καλεί το API εισαγωγής σε σελίδα. Το αντικείμενο δημιουργείται, ερωτάται, και καταστρέφεται σε απομόνωση, οπότε το έγγραφο παραμένει αμετάβλητο όταν η συνάρτηση επιστρέφει. Είναι μια δοκιμαστική ανίχνευση της οποίας η μόνη έξοδος είναι οι τέσσερις αριθμοί του πλαισίου οριοθέτησής της

Αυτός είναι ο εύρωστος τρόπος για να γίνει, επειδή το PDFium δεν εκθέτει ένα βολικό πλάτος προώθησης ανά γλυφή που θα μπορούσατε να αθροίσετε μόνοι σας. Οι μετρήσεις γλυφών εξαρτώνται από το πρόγραμμα γραμματοσειράς, από την κωδικοποίηση, και από το πώς το PDFium φορτώνει τη γραμματοσειρά, και δεν υπάρχει δημόσια κλήση που σας δίνει την προώθηση κάθε χαρακτήρα σε ένα string. Το πλαίσιο οριοθέτησης ενός πραγματικού αντικειμένου κειμένου, από την άλλη πλευρά, υπολογίζεται από τον ίδιο μηχανισμό που θα διέταζε τα γλυφά για σχεδίαση, οπότε αντικατοπτρίζει την πραγματική αποδιδόμενη έκταση αντί για μια προσέγγιση. Η κατασκευή ενός αναλώσιμου αντικειμένου και η ανάγνωση των ορίων του είναι η πιο αξιόπιστη μέτρηση που μπορεί να δώσει η βιβλιοθήκη

Διάγραμμα των τεσσάρων κλήσεων PDFium πίσω από το MeasureText σε Delphi, ανιχνεύοντας πεταμένο αντικείμενο κειμένου χωρίς να αγγίξει τη σελίδα
Η MeasureText χτίζει αντικείμενο κειμένου μίας χρήσης, διαβάζει το bounding box του και το καταστρέφει, οπότε η μέτρηση αφήνει το έγγραφο PDF ανέγγιχτο
// Το σχήμα της MeasureText, εκφρασμένο πάνω στις επαληθευμένες κλήσεις PDFium.
// Ένα αντικείμενο κειμένου κατασκευάζεται, μετριέται, και καταστρέφεται· καμία σελίδα δεν εμπλέκεται.
procedure TPdfMeasureHelper.MeasureText(const Text, Font: WString;
  FontSize: Single; out Width, Height: Double);
var
  TextObject: FPDF_PAGEOBJECT;
  L, B, R, T: Single;
begin
  Width  := 0;
  Height := 0;
  if Self.Document = nil then
    Exit;
  TextObject := FPDFPageObj_NewTextObj(Self.Document,
    FPDF_BYTESTRING(AnsiString(Font)), FontSize);
  if TextObject = nil then
    Exit;
  try
    if FPDFText_SetText(TextObject, FPDF_WIDESTRING(WideString(Text))) = 0 then
      Exit;
    if FPDFPageObj_GetBounds(TextObject, L, B, R, T) <> 0 then
    begin
      Width  := R - L;
      Height := T - B;
    end;
  finally
    FPDFPageObj_Destroy(TextObject);   // η ανίχνευση απορρίπτεται, η σελίδα παραμένει άθικτη
  end;
end;

Συντεταγμένες και μονάδες του αποτελέσματος

Το πλαίσιο οριοθέτησης επιστρέφει ως τέσσερις άκρες, αριστερή, κάτω, δεξιά, και πάνω, και οι δύο διαστάσεις προκύπτουν με αφαίρεση. Το πλάτος είναι δεξιά μείον αριστερά και το ύψος είναι πάνω μείον κάτω. Και τα δύο εκφράζονται σε μονάδες χρήστη PDF, όπου μία μονάδα είναι το ένα εβδομηκοστό δεύτερο μιας ίντσας, ο ίδιος χώρος συντεταγμένων στον οποίο τοποθετείτε κείμενο στη σελίδα. Δεν υπάρχει καμία κρυφή μονάδα συσκευής και κανένα εικονοστοιχείο εμπλεκόμενο σε αυτό το στάδιο. Ένα πλάτος 36 σημαίνει μισή ίντσα σελίδας, ανεξάρτητα από την τελική ανάλυση απόδοσης

Ο κατακόρυφος άξονας λειτουργεί όπως τον ορίζει το PDF, με το Y να αυξάνεται προς τα πάνω, γι' αυτό το ύψος είναι πάνω μείον κάτω και όχι το αντίστροφο. Αυτή η λεπτομέρεια έχει σημασία όταν προωθείτε έναν δρομέα προς τα κάτω σε μια στήλη. Μετράτε το ύψος μιας γραμμής, και έπειτα το αφαιρείτε από την τρέχουσα γραμμή βάσης για να βρείτε την επόμενη, επειδή η μετακίνηση προς τα κάτω στη σελίδα σημαίνει μετακίνηση προς μικρότερα Y. Αν ο προορισμός σας είναι μια οθόνη αντί για χαρτί, μετατρέπετε τις μονάδες χρήστη σε εικονοστοιχεία συσκευής με την ανάλυση οθόνης: μια τιμή σε μονάδες χρήστη επί τα DPI και διά το 72 δίνει εικονοστοιχεία, οπότε ένα πλάτος στήλης που ορίζετε σε points μπορεί να συγκριθεί με ένα μετρημένο run πριν αποφασίσετε πού πηγαίνει το σπάσιμο

Τι συμβαίνει σε εκφυλισμένη είσοδο

Οι συναρτήσεις είναι γραμμένες ώστε να αποτυγχάνουν σιωπηλά. Αν δεν υπάρχει ανοιχτό έγγραφο, ή αν το αντικείμενο κειμένου δεν μπορεί να δημιουργηθεί, το αποτέλεσμα είναι μηδενική έκταση αντί για εξαίρεση. Το πλάτος και το ύψος αρχικοποιούνται σε μηδέν στην αρχή και αντικαθίστανται μόνο αφού διαβαστεί επιτυχώς ένα πλαίσιο οριοθέτησης. Ένα κενό string, ένα έγγραφο που λείπει, μια γραμματοσειρά που η βιβλιοθήκη δεν μπορεί να επιλύσει σε αντικείμενο, καθένα από αυτά επιστρέφει μηδέν αντί να προκαλεί εξαίρεση

Αυτή η επιλογή κρατά έναν βρόχο μέτρησης απλό, επειδή ένας βρόχος που τρέχει πάνω από χιλιάδες λέξεις δεν είναι το μέρος για χειρισμό εξαιρέσεων σε κάθε επανάληψη. Το κόστος είναι ότι ο καλών κουβαλά τον έλεγχο. Ένα μηδενικό πλάτος είναι μια φρουρός τιμή, όχι ένα γεγονός για το κείμενο, οπότε ο κώδικας που διαιρεί με ένα μετρημένο πλάτος ή υποθέτει θετική τιμή πρέπει να προστατευτεί έναντι του μηδενός πριν το εμπιστευτεί. Αντιμετωπίστε το μηδέν ως «δεν ήταν δυνατή η μέτρηση» και το συμβόλαιο είναι σαφές· αγνοήστε το και μια εκφυλισμένη είσοδος γίνεται σιωπηλά μια διάταξη με στήλη επικαλυπτόμενων γλυφών

Μια άπληστη αναδίπλωση λέξεων χτισμένη πάνω στη μέτρηση

Έχοντας μια συνάρτηση πλάτους στη διάθεσή σας, η αναδίπλωση λέξεων είναι ένας σύντομος άπληστος βρόχος. Χωρίζετε την παράγραφο σε λέξεις, κρατάτε μια τρέχουσα γραμμή, και για κάθε λέξη μετράτε πώς θα ήταν η γραμμή αν προσθέτατε εκείνη τη λέξη. Όσο η δοκιμαστική γραμμή εξακολουθεί να χωρά στο πλάτος της στήλης συνεχίζετε να προσθέτετε· όταν θα ξεχείλιζε, εκκενώνετε την τρέχουσα γραμμή με AddText και ξεκινάτε μια νέα με τη λέξη που δεν χώρεσε. Η συσσώρευση γίνεται εξ ολοκλήρου με την MeasureTextWidth, και το μόνο πράγμα που φτάνει ποτέ στη σελίδα είναι μια γραμμή που έχετε ήδη επιβεβαιώσει ότι χωράει

Διάγραμμα ενός άπληστου βρόχου αναδίπλωσης λέξεων σε Delphi που μετρά δοκιμαστικές γραμμές με MeasureTextWidth και σπάει στην τελευταία λέξη που χωράει
Ο άπληστος βρόχος αναδίπλωσης μετρά κάθε δοκιμαστική γραμμή απέναντι στο πλάτος στήλης και ξεπλένει μόνο γραμμές που επιβεβαιώθηκε ότι χωρούν
procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
  FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
  Words: TArray<string>;
  Line, Trial: WideString;
  I: Integer;
  Y: Double;
begin
  Words := string(Para).Split([' ']);
  Line  := '';
  Y     := TopY;
  for I := 0 to High(Words) do
  begin
    if Line = '' then
      Trial := Words[I]
    else
      Trial := Line + ' ' + Words[I];
    // Μετρήστε την υποψήφια γραμμή πριν σχεδιάσετε οτιδήποτε.
    if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
    begin
      Pdf.AddText(Line, Font, FontSize, X, Y);   // εκκένωση της γραμμής που χώρεσε
      Y    := Y - LineHeight;                    // το Y μειώνεται καθώς κατεβαίνουμε
      Line := Words[I];                          // η λέξη που ξεχείλισε ξεκινά την επόμενη γραμμή
    end
    else
      Line := Trial;
  end;
  if Line <> '' then
    Pdf.AddText(Line, Font, FontSize, X, Y);      // εκκένωση της τελικής γραμμής
end;

Ο βρόχος μετρά τη δοκιμαστική γραμμή αντί να μετρά κάθε λέξη και να αθροίζει, επειδή το πλάτος μιας γραμμής δεν είναι το άθροισμα των πλατών των λέξεών της. Τα κενά ανάμεσα στις λέξεις συνεισφέρουν, και ένα μετρημένο run το συλλαμβάνει αυτό απευθείας. Ο άπληστος κανόνας, χωρέστε όσο περισσότερες λέξεις επιτρέπει η στήλη και σπάστε στην τελευταία που χωράει, είναι ο ίδιος κανόνας που γεμίζει το κενό ανάμεσα σε μια απλή AddText και μια πραγματική παράγραφο. Η κλήση σχεδίασης δεν ήταν ποτέ το δύσκολο κομμάτι. Η μέτρηση που πρέπει να προηγηθεί είναι, και αυτό είναι ακριβώς αυτό που παρέχει ο helper

Πού ταιριάζει αυτό

Η μέτρηση είναι το επίπεδο ανάμεσα στη δημιουργία περιεχομένου και την απόδοσή του, οπότε ταιριάζει φυσικά με την υπόλοιπη ροή εργασίας εγγράφου από το μηδέν. Αν συναρμολογείτε σελίδες και τοποθετείτε κείμενο εξαρχής, τα θεμέλια βρίσκονται στο δημιουργία εγγράφων PDF από το μηδέν με το PDFium component στη Delphi, όπου η AddText και η ρύθμιση σελίδας καλύπτονται πλήρως. Όταν η γραμματοσειρά που μετράτε έχει τόση σημασία όσο το ίδιο το string, επειδή οι μετρήσεις εξαρτώνται από τη γραμματοσειρά, το ανάλυση ιδιοτήτων γραμματοσειράς PDF με το PDFium component στη Delphi δείχνει πώς η βιβλιοθήκη αναφέρει τις πληροφορίες γραμματοσειράς που καθορίζουν εκείνα τα πλαίσια οριοθέτησης. Και τα δύο χτίζονται πάνω στην ίδια βιβλιοθήκη σύνδεσης, το PDFium Component για Delphi και Lazarus, όπου ο helper μέτρησης διατίθεται μαζί με τα APIs εγγράφου, σελίδας, και κειμένου που περιγράφονται σε όλο αυτό το blog